modelutils

This module provides utilities to build Cython models based on Python models automatically.

Most model developers do not need to be aware of the features implemented in module modelutils, except that they need to initialise class Cythonizer within the main modules of their base and application models (see, for example, the source code of base model hland and application model hland_96).

However, when implementing models with functionalities not envisaged so far, problems might arise. Please contact the HydPy developer team then, preferably by opening an issue on GitHub. Potentially, problems could occur when defining parameters or sequences with larger dimensionality than anticipated. The following example shows the Cython code lines for the get_point_states() method of class ELSModel, used for deriving the test model. By now, we did only implement 0-dimensional and 1-dimensional sequences requiring this method. After hackishly changing the dimensionality of sequences S, we still seem to get plausible results, but these are untested in model applications:

>>> from hydpy.models.test import cythonizer
>>> pyxwriter = cythonizer.pyxwriter
>>> from hydpy.cythons.modelutils import PyxPxdLines
>>> lines = PyxPxdLines()
>>> pyxwriter.get_point_states(lines)
            . get_point_states
>>> lines.pyx  
    cpdef inline void get_point_states(self) noexcept nogil:
        cdef ...int... idx0
        self.sequences.states.s = self.sequences.states._s_points[self.numvars.idx_stage]
        for idx0 in range(self.sequences.states._sv_length):
            self.sequences.states.sv[idx0] = self.sequences.states._sv_points[self.numvars.idx_stage][idx0]
>>> pyxwriter.model.sequences.states.s.NDIM = 2
>>> lines.pyx.clear()
>>> pyxwriter.get_point_states(lines)
            . get_point_states
>>> lines.pyx  
    cpdef inline void get_point_states(self) noexcept nogil:
        cdef ...int... idx0, idx1
        for idx0 in range(self.sequences.states._s_length0):
            for idx1 in range(self.sequences.states._s_length1):
                self.sequences.states.s[idx0, idx1] = self.sequences.states._s_points[self.numvars.idx_stage][idx0, idx1]
        for idx0 in range(self.sequences.states._sv_length):
            self.sequences.states.sv[idx0] = self.sequences.states._sv_points[self.numvars.idx_stage][idx0]
>>> pyxwriter.model.sequences.states.s.NDIM = 3
>>> pyxwriter.get_point_states(lines)
Traceback (most recent call last):
...
NotImplementedError: NDIM of sequence `s` is higher than expected.

The following examples show the results for some methods which are also related to numerical integration but deal with FluxSequence objects. We start with the method integrate_fluxes():

>>> lines.pyx.clear()
>>> pyxwriter.integrate_fluxes(lines)
            . integrate_fluxes
>>> lines.pyx  
    cpdef inline void integrate_fluxes(self) noexcept nogil:
        cdef ...int... jdx, idx0
        self.sequences.fluxes.q = 0.
        for jdx in range(self.numvars.idx_method):
            self.sequences.fluxes.q = self.sequences.fluxes.q +self.numvars.dt * self.numconsts.a_coefs[self.numvars.idx_method-1, self.numvars.idx_stage, jdx]*self.sequences.fluxes._q_points[jdx]
        for idx0 in range(self.sequences.fluxes._qv_length):
            self.sequences.fluxes.qv[idx0] = 0.
            for jdx in range(self.numvars.idx_method):
                self.sequences.fluxes.qv[idx0] = self.sequences.fluxes.qv[idx0] + self.numvars.dt * self.numconsts.a_coefs[self.numvars.idx_method-1, self.numvars.idx_stage, jdx]*self.sequences.fluxes._qv_points[jdx, idx0]
>>> pyxwriter.model.sequences.fluxes.q.NDIM = 2
>>> lines.pyx.clear()
>>> pyxwriter.integrate_fluxes(lines)
            . integrate_fluxes
>>> lines.pyx  
    cpdef inline void integrate_fluxes(self) noexcept nogil:
        cdef ...int... jdx, idx0, idx1
        for idx0 in range(self.sequences.fluxes._q_length0):
            for idx1 in range(self.sequences.fluxes._q_length1):
                self.sequences.fluxes.q[idx0, idx1] = 0.
                for jdx in range(self.numvars.idx_method):
                    self.sequences.fluxes.q[idx0, idx1] = self.sequences.fluxes.q[idx0, idx1] + self.numvars.dt * self.numconsts.a_coefs[self.numvars.idx_method-1, self.numvars.idx_stage, jdx]*self.sequences.fluxes._q_points[jdx, idx0, idx1]
        for idx0 in range(self.sequences.fluxes._qv_length):
            self.sequences.fluxes.qv[idx0] = 0.
            for jdx in range(self.numvars.idx_method):
                self.sequences.fluxes.qv[idx0] = self.sequences.fluxes.qv[idx0] + self.numvars.dt * self.numconsts.a_coefs[self.numvars.idx_method-1, self.numvars.idx_stage, jdx]*self.sequences.fluxes._qv_points[jdx, idx0]
>>> pyxwriter.model.sequences.fluxes.q.NDIM = 3
>>> pyxwriter.integrate_fluxes(lines)
Traceback (most recent call last):
...
NotImplementedError: NDIM of sequence `q` is higher than expected.

Method reset_sum_fluxes():

>>> pyxwriter.model.sequences.fluxes.q.NDIM = 0
>>> lines.pyx.clear()
>>> pyxwriter.reset_sum_fluxes(lines)
            . reset_sum_fluxes
>>> lines.pyx  
    cpdef inline void reset_sum_fluxes(self) noexcept nogil:
        cdef ...int... idx0
        self.sequences.fluxes._q_sum = 0.
        for idx0 in range(self.sequences.fluxes._qv_length):
            self.sequences.fluxes._qv_sum[idx0] = 0.
>>> pyxwriter.model.sequences.fluxes.q.NDIM = 2
>>> lines.pyx.clear()
>>> pyxwriter.reset_sum_fluxes(lines)
            . reset_sum_fluxes
>>> lines.pyx  
    cpdef inline void reset_sum_fluxes(self) noexcept nogil:
        cdef ...int... idx0, idx1
        for idx0 in range(self.sequences.fluxes._q_length0):
            for idx1 in range(self.sequences.fluxes._q_length1):
                self.sequences.fluxes._q_sum[idx0, idx1] = 0.
        for idx0 in range(self.sequences.fluxes._qv_length):
            self.sequences.fluxes._qv_sum[idx0] = 0.
>>> pyxwriter.model.sequences.fluxes.q.NDIM = 3
>>> pyxwriter.reset_sum_fluxes(lines)
Traceback (most recent call last):
...
NotImplementedError: NDIM of sequence `q` is higher than expected.

Method addup_fluxes():

>>> pyxwriter.model.sequences.fluxes.q.NDIM = 0
>>> lines.pyx.clear()
>>> pyxwriter.addup_fluxes(lines)
            . addup_fluxes
>>> lines.pyx  
    cpdef inline void addup_fluxes(self) noexcept nogil:
        cdef ...int... idx0
        self.sequences.fluxes._q_sum = self.sequences.fluxes._q_sum + self.sequences.fluxes.q
        for idx0 in range(self.sequences.fluxes._qv_length):
            self.sequences.fluxes._qv_sum[idx0] = self.sequences.fluxes._qv_sum[idx0] + self.sequences.fluxes.qv[idx0]
>>> pyxwriter.model.sequences.fluxes.q.NDIM = 2
>>> lines.pyx.clear()
>>> pyxwriter.addup_fluxes(lines)
            . addup_fluxes
>>> lines.pyx  
    cpdef inline void addup_fluxes(self) noexcept nogil:
        cdef ...int... idx0, idx1
        for idx0 in range(self.sequences.fluxes._q_length0):
            for idx1 in range(self.sequences.fluxes._q_length1):
                self.sequences.fluxes._q_sum[idx0, idx1] = self.sequences.fluxes._q_sum[idx0, idx1] + self.sequences.fluxes.q[idx0, idx1]
        for idx0 in range(self.sequences.fluxes._qv_length):
            self.sequences.fluxes._qv_sum[idx0] = self.sequences.fluxes._qv_sum[idx0] + self.sequences.fluxes.qv[idx0]
>>> pyxwriter.model.sequences.fluxes.q.NDIM = 3
>>> pyxwriter.addup_fluxes(lines)
Traceback (most recent call last):
...
NotImplementedError: NDIM of sequence `q` is higher than expected.

Method calculate_error():

>>> pyxwriter.model.sequences.fluxes.q.NDIM = 0
>>> lines.pyx.clear()
>>> pyxwriter.calculate_error(lines)
            . calculate_error
>>> lines.pyx  
    cpdef inline void calculate_error(self) noexcept nogil:
        cdef ...int... idx0
        cdef double abserror
        self.numvars.abserror = 0.
        if self.numvars.use_relerror:
            self.numvars.relerror = 0.
        else:
            self.numvars.relerror = inf
        abserror = fabs(self.sequences.fluxes._q_results[self.numvars.idx_method]-self.sequences.fluxes._q_results[self.numvars.idx_method-1])
        self.numvars.abserror = max(self.numvars.abserror, abserror)
        if self.numvars.use_relerror:
            if self.sequences.fluxes._q_results[self.numvars.idx_method] == 0.:
                self.numvars.relerror = inf
            else:
                self.numvars.relerror = max(self.numvars.relerror, fabs(abserror/self.sequences.fluxes._q_results[self.numvars.idx_method]))
        for idx0 in range(self.sequences.fluxes._qv_length):
            abserror = fabs(self.sequences.fluxes._qv_results[self.numvars.idx_method, idx0]-self.sequences.fluxes._qv_results[self.numvars.idx_method-1, idx0])
            self.numvars.abserror = max(self.numvars.abserror, abserror)
            if self.numvars.use_relerror:
                if self.sequences.fluxes._qv_results[self.numvars.idx_method, idx0] == 0.:
                    self.numvars.relerror = inf
                else:
                    self.numvars.relerror = max(self.numvars.relerror, fabs(abserror/self.sequences.fluxes._qv_results[self.numvars.idx_method, idx0]))
>>> pyxwriter.model.sequences.fluxes.q.NDIM = 2
>>> lines.pyx.clear()
>>> pyxwriter.calculate_error(lines)
            . calculate_error
>>> lines.pyx  
    cpdef inline void calculate_error(self) noexcept nogil:
        cdef ...int... idx0, idx1
        cdef double abserror
        self.numvars.abserror = 0.
        if self.numvars.use_relerror:
            self.numvars.relerror = 0.
        else:
            self.numvars.relerror = inf
        for idx0 in range(self.sequences.fluxes._q_length0):
            for idx1 in range(self.sequences.fluxes._q_length1):
                abserror = fabs(self.sequences.fluxes._q_results[self.numvars.idx_method, idx0, idx1]-self.sequences.fluxes._q_results[self.numvars.idx_method-1, idx0, idx1])
                self.numvars.abserror = max(self.numvars.abserror, abserror)
                if self.numvars.use_relerror:
                    if self.sequences.fluxes._q_results[self.numvars.idx_method, idx0, idx1] == 0.:
                        self.numvars.relerror = inf
                    else:
                        self.numvars.relerror = max(self.numvars.relerror, fabs(abserror/self.sequences.fluxes._q_results[self.numvars.idx_method, idx0, idx1]))
        for idx0 in range(self.sequences.fluxes._qv_length):
            abserror = fabs(self.sequences.fluxes._qv_results[self.numvars.idx_method, idx0]-self.sequences.fluxes._qv_results[self.numvars.idx_method-1, idx0])
            self.numvars.abserror = max(self.numvars.abserror, abserror)
            if self.numvars.use_relerror:
                if self.sequences.fluxes._qv_results[self.numvars.idx_method, idx0] == 0.:
                    self.numvars.relerror = inf
                else:
                    self.numvars.relerror = max(self.numvars.relerror, fabs(abserror/self.sequences.fluxes._qv_results[self.numvars.idx_method, idx0]))
>>> pyxwriter.model.sequences.fluxes.q.NDIM = 3
>>> pyxwriter.calculate_error(lines)
Traceback (most recent call last):
...
NotImplementedError: NDIM of sequence `q` is higher than expected.

Module modelutils implements the following members:

  • get_dllextension() Return the DLL file extension for the current operating system.

  • Lines Handles the code lines for a .pyx or a pxd file.

  • PyxPxdLines Handles the code lines for a .pyx and a pxd file.

  • get_methodheader() Returns the Cython method header for methods without arguments except`self`.

  • decorate_method() The decorated method returns a Lines object including a method header. However, the Lines object is empty if the respective model does not implement a method with the same name as the wrapped method.

  • compile_() Translate Cython code to C code and compile it.

  • move_dll() Try to find the DLL file created by function compile_() and try to move it to the autogen folder of the cythons subpackage.

  • Cythonizer Handles the writing, compiling and initialisation of Cython models.

  • PyxWriter Translates the source code of Python models into Cython source code.

  • FuncConverter Helper class for class PyxWriter that analyses Python functions and provides the required Cython code via property pyxlines.

  • get_callbackcymodule() Return the cython module containing the required callback module after, if necessary, creating or updating.

  • exp() Cython wrapper for the exp() function of module numpy applied on a single float object.

  • log() Cython wrapper for the log() function of module numpy applied on a single float object.

  • fabs() Cython wrapper for the exp() function of module math applied on a single float object.

  • sin() Cython wrapper for the sin() function of module numpy applied on a single float object.

  • cos() Cython wrapper for the cos() function of module numpy applied on a single float object.

  • tan() Cython wrapper for the tan() function of module numpy applied on a single float object.

  • asin() Cython wrapper for the arcsin() function of module numpy applied on a single float object.

  • acos() Cython wrapper for the arccos() function of module numpy applied on a single float object.

  • atan() Cython wrapper for the arctan() function of module numpy applied on a single float object.

  • isnan() Cython wrapper for the isnan() function of module numpy applied on a single float object.

  • isinf() Cython wrapper for the isinf() function of module numpy applied on a single float object.


hydpy.cythons.modelutils.get_dllextension() str[source]

Return the DLL file extension for the current operating system.

The returned value depends on the response of function system() of module platform. get_dllextension() returns .pyd if system() returns the string “windows” and .so for all other strings:

>>> from hydpy.cythons.modelutils import get_dllextension
>>> import platform
>>> from unittest import mock
>>> with mock.patch.object(
...     platform, "system", side_effect=lambda: "Windows") as mocked:
...     get_dllextension()
'.pyd'
>>> with mock.patch.object(
...     platform, "system", side_effect=lambda: "Linux") as mocked:
...     get_dllextension()
'.so'
hydpy.cythons.modelutils.TYPE2STR: dict[type[Any] | str | None, str] = {'IntConstant': 'numpy.int64_t', 'None': 'void', 'Vector': 'double[:]', 'VectorFloat': 'double[:]', 'bool': 'numpy.npy_bool', 'float': 'double', 'int': 'numpy.int64_t', 'parametertools.IntConstant': 'numpy.int64_t', 'str': 'str', <class 'NoneType'>: 'void', <class 'bool'>: 'numpy.npy_bool', <class 'float'>: 'double', <class 'hydpy.core.parametertools.IntConstant'>: 'numpy.int64_t', <class 'int'>: 'numpy.int64_t', <class 'str'>: 'str', None: 'void', numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]: 'double[:]', numpy.ndarray[typing.Any, numpy.dtype[~T]]: 'double[:]'}

Maps Python types to Cython compatible type declarations.

The Cython type belonging to Python’s int is selected to agree with numpy’s default integer type on the current platform/system.

hydpy.cythons.modelutils.CHECKABLE_TYPES: tuple[type[Any], ...] = (<class 'bool'>, <class 'int'>, <class 'hydpy.core.parametertools.IntConstant'>, <class 'float'>, <class 'str'>, <class 'NoneType'>)

“Real types” of TYPE2STR allowed as second arguments of function isinstance().

class hydpy.cythons.modelutils.Lines(*args: str)[source]

Bases: list[str]

Handles the code lines for a .pyx or a pxd file.

add(indent: int, line: str | Iterable[str]) None[source]

Append the given text line with prefixed spaces following the given number of indentation levels.

class hydpy.cythons.modelutils.PyxPxdLines[source]

Bases: object

Handles the code lines for a .pyx and a pxd file.

pyx: Lines
pxd: Lines
add(indent: int, line: str) None[source]

Pass the given data to method add() of the pyx and pxd Lines instances.

hydpy.cythons.modelutils.get_methodheader(methodname: str, nogil: bool = False, idxarg: bool = False, inline: bool = True) str[source]

Returns the Cython method header for methods without arguments except`self`.

Note the influence of the configuration flag FASTCYTHON:

>>> from hydpy.cythons.modelutils import get_methodheader
>>> from hydpy import config
>>> config.FASTCYTHON = False
>>> print(get_methodheader("test", nogil=True, idxarg=False, inline=True))
cpdef inline void test(self):
>>> config.FASTCYTHON = True
>>> methodheader = get_methodheader("test", nogil=True, idxarg=True, inline=False)
>>> print(methodheader)  
cpdef void test(self, ...int... idx) noexcept nogil:
hydpy.cythons.modelutils.decorate_method(wrapped: Callable[[PyxWriter], Iterator[str]]) Callable[[PyxWriter, PyxPxdLines], None][source]

The decorated method returns a Lines object including a method header. However, the Lines object is empty if the respective model does not implement a method with the same name as the wrapped method.

hydpy.cythons.modelutils.compile_(cyname: str, pyxfilepath: str, buildpath: str) None[source]

Translate Cython code to C code and compile it.

hydpy.cythons.modelutils.move_dll(pyname: str, cyname: str, cydirpath: str, buildpath: str) None[source]

Try to find the DLL file created by function compile_() and try to move it to the autogen folder of the cythons subpackage.

Usually, one does not need to apply move_dll() directly. However, if you are a model developer, you might see one of the following error messages from time to time:

>>> from hydpy.cythons.modelutils import move_dll
>>> from hydpy.models.hland_96 import cythonizer as c
>>> move_dll(pyname=c.pyname, cyname=c.cyname,
...          cydirpath=c.cydirpath, buildpath=c.buildpath)  
Traceback (most recent call last):
...
OSError: After trying to cythonize `hland_96`, the resulting file `c_hland_96...` could not be found in directory `.../hydpy/cythons/autogen/_build` nor any of its subdirectories.  The distutil report should tell whether the file has been stored somewhere else, is named somehow else, or could not be build at all.
>>> import os
>>> from unittest import mock
>>> from hydpy import TestIO
>>> with TestIO():   
...     with mock.patch.object(
...             type(c), "buildpath", new_callable=mock.PropertyMock
...     ) as mocked_buildpath:
...         mocked_buildpath.return_value = "_build"
...         os.makedirs("_build/subdir", exist_ok=True)
...         filepath = f"_build/subdir/c_hland_96{get_dllextension()}"
...         with open(filepath, "w"):
...             pass
...         with mock.patch(
...                 "shutil.move",
...                 side_effect=PermissionError("Denied!")):
...             move_dll(pyname=c.pyname, cyname=c.cyname,
...                      cydirpath=c.cydirpath, buildpath=c.buildpath)
Traceback (most recent call last):
...
PermissionError: After trying to cythonize `hland_96`, when trying to move the final cython module `c_hland_96...` from directory `_build` to directory `.../hydpy/cythons/autogen`, the following error occurred: Denied! A likely error cause is that the cython module `c_hland_96...` does already exist in this directory and is currently blocked by another Python process.  Maybe it helps to close all Python processes and restart the cythonization afterwards.
class hydpy.cythons.modelutils.Cythonizer[source]

Bases: object

Handles the writing, compiling and initialisation of Cython models.

Model: type[Model]
Parameters: type[Parameters]
Sequences: type[Sequences]
tester: Tester
pymodule: str
cythonize() None[source]

Translate Python source code of the relevant model first into Cython and then into C, compile it, and move the resulting dll file to the autogen subfolder of subpackage cythons.

property pyname: str

Name of the original Python module or package.

>>> from hydpy.models.hland import cythonizer
>>> cythonizer.pyname
'hland'
>>> from hydpy.models.hland_96 import cythonizer
>>> cythonizer.pyname
'hland_96'
property cyname: str

Name of the compiled module.

>>> from hydpy.models.hland import cythonizer
>>> cythonizer.cyname
'c_hland'
>>> from hydpy.models.hland_96 import cythonizer
>>> cythonizer.cyname
'c_hland_96'
property cydirpath: str

The absolute path of the directory containing the compiled modules.

>>> from hydpy.models.hland import cythonizer
>>> from hydpy import repr_
>>> repr_(cythonizer.cydirpath)   
'.../hydpy/cythons/autogen'
>>> import os
>>> os.path.exists(cythonizer.cydirpath)
True
property cymodule: ModuleType

The compiled module.

Property cymodule returns the relevant DLL module:

>>> from hydpy.models.hland_96 import cythonizer
>>> from hydpy.cythons.autogen import c_hland_96
>>> c_hland_96 is cythonizer.cymodule
True

However, if this module is missing for some reasons, it tries to create the module first and returns it afterwards. For demonstration purposes, we define a wrong cyname:

>>> from hydpy.cythons.modelutils import Cythonizer
>>> cyname = Cythonizer.cyname
>>> Cythonizer.cyname = "wrong"
>>> cythonizer._cymodule = None
>>> from unittest import mock
>>> with mock.patch.object(Cythonizer, "cythonize") as mock:
...     cythonizer.cymodule
Traceback (most recent call last):
...
ModuleNotFoundError: No module named 'hydpy.cythons.autogen.wrong'
>>> mock.call_args_list
[call()]
>>> Cythonizer.cyname = cyname
property pyxfilepath: str

The absolute path of the compiled module.

>>> from hydpy.models.hland_96 import cythonizer
>>> from hydpy import repr_
>>> repr_(cythonizer.pyxfilepath)   
'.../hydpy/cythons/autogen/c_hland_96.pyx'
>>> import os
>>> os.path.exists(cythonizer.pyxfilepath)
True
property dllfilepath: str

The absolute path of the compiled module.

>>> from hydpy.models.hland_96 import cythonizer
>>> from hydpy import repr_
>>> repr_(cythonizer.dllfilepath)   
'.../hydpy/cythons/autogen/c_hland_96...'
>>> import os
>>> os.path.exists(os.path.split(cythonizer.dllfilepath)[0])
True
property buildpath: str

The absolute path for temporarily build files.

>>> from hydpy.models.hland_96 import cythonizer
>>> from hydpy import repr_
>>> repr_(cythonizer.buildpath)   
'.../hydpy/cythons/autogen/_build'
property pyxwriter: PyxWriter

A new PyxWriter instance.

>>> from hydpy.models.hland_96 import cythonizer
>>> pyxwriter = cythonizer.pyxwriter
>>> from hydpy import classname
>>> classname(pyxwriter)
'PyxWriter'
>>> cythonizer.pyxwriter is pyxwriter
False
class hydpy.cythons.modelutils.PyxWriter(cythonizer: Cythonizer, model: Model, pyxpath: str)[source]

Bases: object

Translates the source code of Python models into Cython source code.

Method PyxWriter serves as a master method, which triggers the complete writing process. The other properties and methods supply the required code lines. Their names are selected to match the names of the original Python models as closely as possible.

cythonizer: Cythonizer
model: Model
pyxpath: str
pxdpath: str
write() None[source]

Collect the source code and write it into a Cython extension file (“pyx”) and its definition file (“pxd”).

cythondistutilsoptions(lines: PyxPxdLines) None[source]

Cython and Distutils option lines.

Use the configuration options “FASTCYTHON” and “PROFILECYTHON” to configure the cythonization processes as follows:

>>> from hydpy.cythons.modelutils import PyxWriter
>>> pyxwriter = PyxWriter(None, None, "file.pyx")
>>> from hydpy.cythons.modelutils import PyxPxdLines
>>> lines = PyxPxdLines()
>>> pyxwriter.cythondistutilsoptions(lines)
>>> lines.pyx  
#!python
# distutils: define_macros=NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION
# cython: language_level=3
# cython: cpow=True
# cython: boundscheck=False
# cython: wraparound=False
# cython: initializedcheck=False
# cython: cdivision=True
>>> from hydpy import config
>>> config.FASTCYTHON = False
>>> config.PROFILECYTHON = True
>>> lines.pyx.clear()
>>> pyxwriter.cythondistutilsoptions(lines)
>>> lines.pyx  
#!python
# distutils: define_macros=NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION
# cython: language_level=3
# cython: cpow=True
# cython: boundscheck=True
# cython: wraparound=True
# cython: initializedcheck=True
# cython: cdivision=False
# cython: linetrace=True
# distutils: define_macros=CYTHON_TRACE=1
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
>>> config.FASTCYTHON = True
>>> config.PROFILECYTHON = False
cimports(lines: PyxPxdLines) None[source]

Import command lines.

constants(lines: PyxPxdLines) None[source]

Constants declaration lines.

parameters(lines: PyxPxdLines) None[source]

Parameter declaration lines.

sequences(lines: PyxPxdLines) None[source]

Sequence declaration lines.

static iosequence(lines: PyxPxdLines, seq: IOSequence) None[source]

Declaration lines for the given IOSequence object.

reset_reuseflags(lines: PyxPxdLines) None[source]

Reset reuse flag statements.

classmethod load_data(lines: PyxPxdLines, subseqs: IOSequences[Any, Any, Any]) None[source]

Load data statements.

classmethod save_data(lines: PyxPxdLines, subseqs: IOSequences[Any, Any, Any]) None[source]

Save data statements.

set_pointer(lines: PyxPxdLines, subseqs: InputSequences | OutputSequences[Any] | LinkSequences[Any]) None[source]

Set pointer statements for all input, output, and link sequences.

static set_pointer0d(lines: PyxPxdLines, subseqs: LinkSequences[Any]) None[source]

Set pointer statements for 0-dimensional link sequences.

static get_value(lines: PyxPxdLines, subseqs: LinkSequences[Any]) None[source]

Get value statements for link sequences.

static set_value(lines: PyxPxdLines, subseqs: LinkSequences[Any]) None[source]

Set value statements for link sequences.

static alloc(lines: PyxPxdLines, subseqs: LinkSequences[Any]) None[source]

Allocate memory statements for 1-dimensional link sequences.

static dealloc(lines: PyxPxdLines, subseqs: LinkSequences[Any]) None[source]

Deallocate memory statements for 1-dimensional link sequences.

static set_pointer1d(lines: PyxPxdLines, subseqs: LinkSequences[Any]) None[source]

Set_pointer statements for 1-dimensional link sequences.

classmethod set_pointerinput(lines: PyxPxdLines, subseqs: InputSequences) None[source]

Set pointer statements for input sequences.

classmethod set_pointeroutput(lines: PyxPxdLines, subseqs: OutputSequences[Any]) None[source]

Set pointer statements for output sequences.

numericalparameters(lines: PyxPxdLines) None[source]

Numeric parameter declaration lines.

submodels(lines: PyxPxdLines) None[source]

Submodel declaration lines.

modeldeclarations(lines: PyxPxdLines) None[source]

The attribute declarations of the model class.

modelstandardfunctions(lines: PyxPxdLines) None[source]

The standard functions of the model class.

modelnumericfunctions(lines: PyxPxdLines) None[source]

Numerical integration functions of the model class.

simulate(lines: PyxPxdLines) None[source]

Simulation statements.

iofunctions(lines: PyxPxdLines) None[source]

Input/output functions of the model class.

The result of property iofunctions() depends on the availability of different types of sequences. So far, the models implemented in HydPy do not reflect all possible combinations, which is why we modify the hland_96 application model in the following examples:

>>> from hydpy.models.hland_96 import cythonizer
>>> pyxwriter = cythonizer.pyxwriter
>>> from hydpy.cythons.modelutils import PyxPxdLines
>>> lines = PyxPxdLines()
>>> pyxwriter.iofunctions(lines)
            . load_data
            . save_data
>>> lines.pyx  
    cpdef void load_data(self, ...int... idx) noexcept nogil:
        self.idx_sim = idx
        self.sequences.inputs.load_data(idx)
        if (self.aetmodel is not None) and not self.aetmodel_is_mainmodel:
            self.aetmodel.load_data(idx)
        if (self.rconcmodel is not None) and not self.rconcmodel_is_mainmodel:
            self.rconcmodel.load_data(idx)
    cpdef void save_data(self, ...int... idx) noexcept nogil:
        self.idx_sim = idx
        self.sequences.inputs.save_data(idx)
        self.sequences.factors.save_data(idx)
        self.sequences.fluxes.save_data(idx)
        self.sequences.states.save_data(idx)
        if (self.aetmodel is not None) and not self.aetmodel_is_mainmodel:
            self.aetmodel.save_data(idx)
        if (self.rconcmodel is not None) and not self.rconcmodel_is_mainmodel:
            self.rconcmodel.save_data(idx)
>>> pyxwriter.model.sequences.factors = None
>>> pyxwriter.model.sequences.fluxes = None
>>> pyxwriter.model.sequences.states = None
>>> lines.pyx.clear()
>>> pyxwriter.iofunctions(lines)
            . load_data
            . save_data
>>> lines.pyx  
    cpdef void load_data(self, ...int... idx) noexcept nogil:
        self.idx_sim = idx
        self.sequences.inputs.load_data(idx)
        if (self.aetmodel is not None) and not self.aetmodel_is_mainmodel:
            self.aetmodel.load_data(idx)
        if (self.rconcmodel is not None) and not self.rconcmodel_is_mainmodel:
            self.rconcmodel.load_data(idx)
    cpdef void save_data(self, ...int... idx) noexcept nogil:
        self.idx_sim = idx
        self.sequences.inputs.save_data(idx)
        if (self.aetmodel is not None) and not self.aetmodel_is_mainmodel:
            self.aetmodel.save_data(idx)
        if (self.rconcmodel is not None) and not self.rconcmodel_is_mainmodel:
            self.rconcmodel.save_data(idx)
>>> pyxwriter.model.sequences.inputs = None
>>> lines.pyx.clear()
>>> pyxwriter.iofunctions(lines)
>>> lines.pyx  

new2old(lines: PyxPxdLines) None[source]

Old states to new states statements.

update_receivers(lines: PyxPxdLines) None[source]

Lines of the model method with the same name.

update_inlets(lines: PyxPxdLines) None[source]

Lines of the model method with the same name.

run(lines: PyxPxdLines, model: RunModel) None[source]

Return the lines of the model method with the same name.

update_outlets(lines: PyxPxdLines) None[source]

Lines of the model method with the same name.

update_senders(lines: PyxPxdLines) None[source]

Lines of the model method with the same name.

update_outputs_model(lines: PyxPxdLines) None[source]

Lines of the model method with the same name (except the _model suffix).

update_outputs(lines: PyxPxdLines, subseqs: OutputSequences[Any]) None[source]

Lines of the subsequences method with the same name.

calculate_single_terms(lines: PyxPxdLines, model: SolverModel) None[source]

Return the lines of the model method with the same name.

calculate_full_terms(lines: PyxPxdLines, model: SolverModel) None[source]

Return the lines of the model method with the same name.

property name2function_method: dict[str, MethodType]

Functions defined by Method subclasses.

property automethod2name: dict[str, tuple[type[Method], ...]]

Submethods selected by AutoMethod and SetAutoMethod subclasses.

property interfacemethods: set[str]

The full and abbreviated names of the selected model’s interface methods.

modeluserfunctions(lines: PyxPxdLines) None[source]

Model-specific functions.

callbackfeatures(lines: PyxPxdLines) None[source]

Features to let users define callback functions.

automethod(lines: PyxPxdLines, name: str, submethods: tuple[type[Method], ...]) None[source]

Lines of a method defined by a AutoMethod or SetAutoMethod subclass.

solve(lines: PyxPxdLines) None[source]

Lines of the model method with the same name.

get_point_states() Iterator[str][source]

Get point statements for state sequences.

set_point_states() Iterator[str][source]

Set point statements for state sequences.

set_result_states() Iterator[str][source]

Get results statements for state sequences.

get_sum_fluxes() Iterator[str][source]

Get sum statements for flux sequences.

set_point_fluxes() Iterator[str][source]

Set point statements for flux sequences.

set_result_fluxes() Iterator[str][source]

Set result statements for flux sequences.

integrate_fluxes() Iterator[str][source]

Integrate statements for flux sequences.

reset_sum_fluxes() Iterator[str][source]

Reset sum statements for flux sequences.

addup_fluxes() Iterator[str][source]

Add up statements for flux sequences.

calculate_error() Iterator[str][source]

Calculate error statements.

extrapolate_error(lines: PyxPxdLines) None[source]

Extrapolate error statements.

write_stubfile() None[source]

Write a stub file for the actual base or application model.

At the moment, HydPy creates model objects quite dynamically. In many regards, this comes with lots of conveniences. However, there two critical drawbacks compared to more static approaches: some amount of additional initialisation time and, more important, much opaqueness for code inspection tools. In this context, we experiment with “stub files” at the moment. These could either contain typing information only or define statically predefined model classes. The following example uses method write_stubfile() to write a (far from perfect) prototype stub file for base model hland:

>>> from hydpy.models.hland import *
>>> cythonizer.pyxwriter.write_stubfile()

This is the path to the written file:

>>> import os
>>> import hydpy
>>> filepath = os.path.join(hydpy.__path__[0], "hland.py")
>>> os.path.exists(filepath)
True

However, it’s just an experimental prototype, so we better remove it:

>>> os.remove(filepath)
>>> os.path.exists(filepath)
False
class hydpy.cythons.modelutils.FuncConverter(model: Model, funcname: str, func: MethodType | Callable[[Model], None], inline: bool = True)[source]

Bases: object

Helper class for class PyxWriter that analyses Python functions and provides the required Cython code via property pyxlines.

model: Model
funcname: str
func: MethodType | Callable[[Model], None]
inline: bool
property realfunc: Callable

The “real” function, as as defined by the model developer or user.

property argnames: list[str]

The argument names of the current function.

>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")
>>> FuncConverter(model, None, model.calc_tc_v1).argnames
['model']
property varnames: tuple[str, ...]

The variable names of the current function.

>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")
>>> FuncConverter(model, None, model.calc_tc_v1).varnames
('self', 'con', 'der', 'inp', 'fac', 'k')
property locnames: list[str]

The variable names of the handled function except for the argument names.

>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")
>>> FuncConverter(model, None, model.calc_tc_v1).locnames
['self', 'con', 'der', 'inp', 'fac', 'k']
property subgroupnames: list[str]

The complete names of the subgroups relevant for the current function.

>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")
>>> FuncConverter(model, None, model.calc_tc_v1).subgroupnames
['parameters.control', 'parameters.derived', 'sequences.inputs', 'sequences.factors']
property subgroupshortcuts: list[str]

The abbreviated names of the subgroups relevant for the current function.

>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")
>>> FuncConverter(model, None, model.calc_tc_v1).subgroupshortcuts
['con', 'der', 'inp', 'fac']
property untypedvarnames: list[str]

The names of the untyped variables used in the current function.

>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")
>>> FuncConverter(model, None, model.calc_tc_v1).untypedvarnames
['k']
property untypedarguments: list[str]

The names of the untyped arguments used by the current function.

>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")
>>> FuncConverter(model, None, model.calc_tc_v1).untypedarguments
[]
property untypedinternalvarnames: list[str]

The names of the untyped variables used in the current function except for those of the arguments.

>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")
>>> FuncConverter(model, None, model.calc_tc_v1).untypedinternalvarnames
['k']
property reusablemethod: type[ReusableMethod] | None

If the currently handled function object is a reusable method, return the corresponding subclass of ReusableMethod.

property cleanlines: list[str]

The leaned code lines of the current function.

The implemented cleanups:
  • eventually, remove method version

  • remove all docstrings

  • remove all comments

  • remove all empty lines

  • remove line bracks within brackets

  • remove the phrase modelutils

  • remove all lines containing the phrase fastaccess

  • replace all shortcuts with complete reference names

  • replace “ model.” with “ self.”

  • remove “.values” and “value”

  • remove the “: float” annotation

static remove_linebreaks_within_equations(code: str) str[source]

Remove line breaks within equations.

The following example is not an exhaustive test but shows how the method works in principle:

>>> code = "asdf = \\\n(a\n+b)"
>>> from hydpy.cythons.modelutils import FuncConverter
>>> FuncConverter.remove_linebreaks_within_equations(code)
'asdf = (a+b)'
static remove_imath_operators(lines: list[str]) None[source]

Remove mathematical expressions that require Pythons global interpreter locking mechanism.

The following example is not an exhaustive test but shows how the method works in principle:

>>> lines = ["    x += 1*1"]
>>> from hydpy.cythons.modelutils import FuncConverter
>>> FuncConverter.remove_imath_operators(lines)
>>> lines
['    x = x + (1*1)']
property pyxlines: Lines

Cython code lines of the current function.

Assumptions:
  • The function shall be a method.

  • Annotations specify all argument and return types.

  • Non-default argument and return types are translate to “modulename.classname” strings.

  • Local variables are generally of type int but of type double when their name starts with d_.

  • Identical type names in Python and Cython when casting.

We import some classes and prepare a pure-Python instance of application model hland_96:

>>> from types import MethodType
>>> from hydpy.core.modeltools import Method, Model
>>> from hydpy.core.typingtools import Vector
>>> from hydpy.cythons.modelutils import FuncConverter
>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("hland_96")

First, we show an example of a standard method without additional arguments and returning nothing but requiring two local variables:

>>> class Calc_Test_V1(Method):
...     @staticmethod
...     def __call__(model: Model) -> None:
...         con = model.parameters.control.fastaccess
...         flu = model.sequences.fluxes.fastaccess
...         inp = model.sequences.inputs.fastaccess
...         for k in range(con.nmbzones):
...             d_pc = con.kg[k]*inp.p[k]
...             flu.pc[k] = d_pc
>>> model.calc_test_v1 = MethodType(Calc_Test_V1.__call__, model)
>>> FuncConverter(
...     model, "calc_test_v1", model.calc_test_v1
... ).pyxlines   
cpdef inline void calc_test_v1(self) noexcept nogil:
    cdef double d_pc
    cdef ...int... k
    for k in range(self.parameters.control.nmbzones):
        d_pc = self.parameters.control.kg[k]*self.sequences.inputs.p[k]
        self.sequences.fluxes.pc[k] = d_pc

The second example shows that float and Vector annotations translate into double and double[:] types, respectively:

>>> class Calc_Test_V2(Method):
...     @staticmethod
...     def __call__(model: Model, value: float, values: Vector) -> float:
...         con = model.parameters.control.fastaccess
...         return con.kg[0]*value*values[1]
>>> model.calc_test_discontinous = MethodType(Calc_Test_V2.__call__, model)
>>> FuncConverter(
...     model, "calc_test_discontinous", model.calc_test_discontinous
... ).pyxlines
cpdef inline double calc_test_discontinous(self, double value, double[:] values) noexcept nogil:
    return self.parameters.control.kg[0]*value*values[1]

Third, Python’s standard cast function translates into Cython’s cast syntax:

>>> from hydpy.interfaces import routinginterfaces
>>> class Calc_Test_V3(Method):
...     @staticmethod
...     def __call__(model: Model) -> routinginterfaces.StorageModel_V1:
...         return cast(routinginterfaces.StorageModel_V1, model.soilmodel)
>>> model.calc_test_stiff1d = MethodType(Calc_Test_V3.__call__, model)
>>> FuncConverter(model, "calc_test_stiff1d", model.calc_test_stiff1d).pyxlines
cpdef inline masterinterface.MasterInterface calc_test_stiff1d(self) noexcept nogil:
    return (<masterinterface.MasterInterface>self.soilmodel)
>>> class Calc_Test_V4(Method):
...     @staticmethod
...     def __call__(model: Model) -> None:
...         cast(
...             Union[
...                 routinginterfaces.RoutingModel_V1,
...                 routinginterfaces.RoutingModel_V2,
...             ],
...             model.routingmodels[0],
...         ).get_partialdischargedownstream()
>>> model.calc_test_v4 = MethodType(Calc_Test_V4.__call__, model)
>>> FuncConverter(model, "calc_test_v4", model.calc_test_v4).pyxlines
cpdef inline void calc_test_v4(self) noexcept nogil:
    (<masterinterface.MasterInterface>self.routingmodels[0]).get_partialdischargedownstream()
hydpy.cythons.modelutils.get_callbackcymodule(model: Model, parameter: CallbackParameter, callback: Callable[[Model], None]) ModuleType[source]

Return the cython module containing the required callback module after, if necessary, creating or updating.

hydpy.cythons.modelutils.exp(double: float) float[source]

Cython wrapper for the exp() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import exp
>>> from unittest import mock
>>> with mock.patch("numpy.exp") as func:
...     _ = exp(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.log(double: float) float[source]

Cython wrapper for the log() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import log
>>> from unittest import mock
>>> with mock.patch("numpy.log") as func:
...     _ = log(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.fabs(double: float) float[source]

Cython wrapper for the exp() function of module math applied on a single float object.

>>> from hydpy.cythons.modelutils import fabs
>>> from unittest import mock
>>> with mock.patch("math.fabs") as func:
...     _ = fabs(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.sin(double: float) float[source]

Cython wrapper for the sin() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import sin
>>> from unittest import mock
>>> with mock.patch("numpy.sin") as func:
...     _ = sin(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.cos(double: float) float[source]

Cython wrapper for the cos() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import cos
>>> from unittest import mock
>>> with mock.patch("numpy.cos") as func:
...     _ = cos(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.tan(double: float) float[source]

Cython wrapper for the tan() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import tan
>>> from unittest import mock
>>> with mock.patch("numpy.tan") as func:
...     _ = tan(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.asin(double: float) float[source]

Cython wrapper for the arcsin() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import asin
>>> from unittest import mock
>>> with mock.patch("numpy.arcsin") as func:
...     _ = asin(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.acos(double: float) float[source]

Cython wrapper for the arccos() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import acos
>>> from unittest import mock
>>> with mock.patch("numpy.arccos") as func:
...     _ = acos(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.atan(double: float) float[source]

Cython wrapper for the arctan() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import atan
>>> from unittest import mock
>>> with mock.patch("numpy.arctan") as func:
...     _ = atan(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.isnan(double: float) float[source]

Cython wrapper for the isnan() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import isnan
>>> from unittest import mock
>>> with mock.patch("numpy.isnan") as func:
...     _ = isnan(123.4)
>>> func.call_args
call(123.4)
hydpy.cythons.modelutils.isinf(double: float) float[source]

Cython wrapper for the isinf() function of module numpy applied on a single float object.

>>> from hydpy.cythons.modelutils import isnan
>>> from unittest import mock
>>> with mock.patch("numpy.isinf") as func:
...     _ = isinf(123.4)
>>> func.call_args
call(123.4)