modeltools

This module provides features for applying and implementing hydrological models.

Module modeltools implements the following members:

  • TypeModel_co Type variable.

  • TypeModel_contra Type variable.

  • TypeSubmodelInterface Type variable.

  • Method Base class for defining (hydrological) calculation methods.

  • AutoMethod Base class for defining methods that only call their submethods in the specified order without passing any arguments or other customisations.

  • SetAutoMethod Base class for defining setter methods that also use the given data to calculate other properties.

  • ReusableMethod Base class for defining methods that need not or must not be called multiple times for the same simulation step.

  • abstractmodelmethod() Alternative for Python’s abstractmethod().

  • SubmodelProperty Descriptor for submodel attributes.

  • SubmodelsProperty Descriptor for handling multiple submodels that follow defined interfaces.

  • SubmodelIsMainmodelProperty Descriptor for boolean “submodel_is_mainmodel” attributes.

  • SubmodelTypeIDProperty Descriptor for integer “submodel_typeid” attributes.

  • IndexProperty Base class for index descriptors like Idx_Sim.

  • Idx_Sim The simulation step index.

  • Idx_HRU The hydrological response unit index.

  • Idx_Segment The segment index.

  • Idx_Run The run index.

  • DocName Definitions for the documentation names of specific base or application models.

  • Model Base class for all hydrological models.

  • RunModel Base class for AdHocModel and SegmentModel that introduces so-called “run methods”, which need to be executed in the order of their positions in the RUN_METHODS tuple.

  • AdHocModel Base class for models solving the underlying differential equations in an “ad hoc manner”.

  • SegmentModel Base class for (routing) models that solve the underlying differential equations “segment-wise”.

  • SubstepModel Base class for (routing) models that solve the underlying differential equations “substep-wise”.

  • SolverModel Base class for hydrological models, which solve ordinary differential equations with numerical integration algorithms.

  • NumConstsELS Configuration options for using the “Explicit Lobatto Sequence” implemented by class ELSModel.

  • NumVarsELS Intermediate results of the “Explicit Lobatto Sequence” implemented by class ELSModel.

  • ELSModel Base class for hydrological models using the “Explicit Lobatto Sequence” for solving ordinary differential equations.

  • SubmodelInterface Base class for defining interfaces for submodels.

  • SharableSubmodelInterface Base class for defining interfaces for submodels designed as “sharable”.

  • Submodel Base class for implementing “submodels” that serve to deal with (possibly complicated) general mathematical algorithms (e.g. root-finding algorithms) within hydrological model methods.

  • CoupleModels Specification for defining custom “couple_models” functions to be wrapped by function define_modelcoupler().

  • define_modelcoupler() Wrap a model-specific function for creating a composite model based given on Node and Element objects and their handled “normal” Model instances.

  • ModelCoupler Wrapper that extends the functionality of model-specific functions for coupling “normal” models to composite models.


class hydpy.core.modeltools.Method[source]

Bases: object

Base class for defining (hydrological) calculation methods.

class hydpy.core.modeltools.AutoMethod[source]

Bases: Method

Base class for defining methods that only call their submethods in the specified order without passing any arguments or other customisations.

class hydpy.core.modeltools.SetAutoMethod[source]

Bases: Method

Base class for defining setter methods that also use the given data to calculate other properties.

SetAutoMethod calls its submethods in the specified order. If, for example, the first two submethods are setters, it requires precisely two parameter values. It passes the first value to the first setter and the second value to the second setter. After that, it executes the remaining methods without exchanging any data.

class hydpy.core.modeltools.ReusableMethod[source]

Bases: Method

Base class for defining methods that need not or must not be called multiple times for the same simulation step.

ReusableMethod helps to implement “sharable” submodels, of which single instances can be used by multiple main model instances. See SharableSubmodelInterface for further information.

REUSEMARKER: str

Name of an additional model attribute for marking if the respective method has already been called and should not be called again for the same simulation step and its results can be reused.

classmethod call_reusablemethod(model: Model, *args, **kwargs) None[source]

Execute the “normal” model-specific __call__ method only when indicated by the REUSEMARKER attribute and update this attribute when necessary.

hydpy.core.modeltools.abstractmodelmethod(method: Callable[[P], T]) Callable[[P], T][source]

Alternative for Python’s abstractmethod().

We currently use it to mark abstract methods in submodel interfaces that are not statically overridden by concrete implementations but dynamically added during model initialisation (either in a pure Python or a Cython version).

So far, the only functionality of abstractmodelmethod() is to collect all decorated functions in the set abstractmodelmethods so that one can find out which methods are “abstract model methods” and which are not. We might also use it later to extend our model consistency checks.

class hydpy.core.modeltools.SubmodelProperty(*interfaces: type[TypeSubmodelInterface], optional: bool = False, sidemodel: bool = False)[source]

Bases: _SubmodelPropertyBase[TypeSubmodelInterface]

Descriptor for submodel attributes.

SubmodelProperty instances link main models and their submodels. They follow the attribute convention described in the documentation on class SubmodelInterface. Behind the scenes, they build the required connections both on the Python and the Cython level and perform some type-related tests (to avoid errors due to selecting submodels following the wrong interfaces).

We prepare the main model and its submodel in Cython and pure Python mode to test that SubmodelProperty works for all possible combinations:

>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     mainmodel_python = prepare_model("lland")
...     submodel_python = prepare_model("ga_garto_submodel1")
>>> with pub.options.usecython(True):
...     mainmodel_cython = prepare_model("lland")
...     submodel_cython = prepare_model("ga_garto_submodel1")

By default, the main model handles no submodel:

>>> mainmodel_python.soilmodel
>>> mainmodel_cython.soilmodel

For pure Python main models, it makes no difference how the submodel is initialised:

>>> mainmodel_python.soilmodel = submodel_python
>>> type(mainmodel_python.soilmodel)
<class 'hydpy.models.ga_garto_submodel1.Model'>
>>> mainmodel_python.cymodel
>>> mainmodel_python.soilmodel = submodel_cython
>>> type(mainmodel_python.soilmodel)
<class 'hydpy.models.ga_garto_submodel1.Model'>
>>> mainmodel_python.cymodel

If both models are initialised in Cython mode, SubmodelProperty connects the instances of the Cython extension classes on the fly:

>>> mainmodel_cython.soilmodel = submodel_cython
>>> type(mainmodel_cython.soilmodel)
<class 'hydpy.models.ga_garto_submodel1.Model'>
>>> type(mainmodel_cython.cymodel.get_soilmodel())
<class 'hydpy.cythons.autogen.c_ga_garto_submodel1.Model'>

Combining a Cython main model with a pure Python submodel causes a RuntimeError, as using such a mix could result in hard-to-find errors:

>>> mainmodel_cython.soilmodel = submodel_python
Traceback (most recent call last):
...
RuntimeError: While trying to assign submodel `ga_garto_submodel1` to property `soilmodel` of the main model `lland`, the following error occurred: The main model is initialised in Cython mode, but the submodel is initialised in pure Python mode so that the main model's cythonized methods could apply the submodel's methods.

Disconnecting a submodel from its main model works by assigning None as well as using the del statement:

>>> mainmodel_python.soilmodel = None
>>> mainmodel_python.soilmodel
>>> del mainmodel_cython.soilmodel
>>> mainmodel_cython.soilmodel
>>> mainmodel_cython.cymodel.get_soilmodel()

Trying to assign an unsuitable submodel results in the following error:

>>> mainmodel_python.soilmodel = mainmodel_python
Traceback (most recent call last):
...
ValueError: While trying to assign submodel `lland` to property `soilmodel` of the main model `lland`, the following error occurred: The given submodel is not an instance of any of the following supported interfaces: SoilModel_V1.

The automatically generated docstrings list the supported interfaces:

>>> print(type(mainmodel_python).soilmodel.__doc__)
Optional submodel that complies with the following interface: SoilModel_V1.
name: str

The addressed submodels’ group name.

interfaces: tuple[type[TypeSubmodelInterface], ...]

The supported interfaces.

optional: Final[bool]

Flag indicating whether a submodel is optional or strictly required.

sidemodel: Final[bool]

Flag indicating whether the handled submodel is more a “side model” than a submodel. Usually, two models consider each other as side models if they are “real” submodels of a third model but need direct references.

class hydpy.core.modeltools.SubmodelsProperty(*interfaces: type[TypeSubmodelInterface], sidemodels: bool = False)[source]

Bases: _SubmodelPropertyBase[TypeSubmodelInterface]

Descriptor for handling multiple submodels that follow defined interfaces.

SubmodelsProperty supports the len operator and is iterable and indexable:

>>> from hydpy import prepare_model
>>> main = prepare_model("sw1d_channel")
>>> sub1 = prepare_model("sw1d_q_in")
>>> sub2 = prepare_model("sw1d_lias")
>>> from hydpy.core.modeltools import SubmodelsProperty
>>> assert isinstance(type(main).routingmodels, SubmodelsProperty)
>>> main.routingmodels.append_submodel(submodel=sub1, typeid=1)
>>> main.routingmodels.append_submodel(submodel=sub2, typeid=1)
>>> len(main.routingmodels)
2
>>> for submodel in main.routingmodels:
...     print(submodel.name)
sw1d_q_in
sw1d_lias
>>> main.routingmodels[0] is sub1
True
>>> main.routingmodels[1] is sub2
True
name: str

The addressed submodels’ group name.

interfaces: tuple[type[TypeSubmodelInterface], ...]

The supported interfaces.

sidemodels: bool

Flag indicating whether the handled submodel is more a “side model” than a submodel. Usually, two models consider each other as side models if they are “real” submodels of a third model but need direct references.

property number: int

The maximum number of handled submodels.

Initially, the maximum number of submodels is zero:

>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     model = prepare_model("sw1d_channel")
>>> model.storagemodels.number
0
>>> model.storagemodels.submodels
()
>>> model.storagemodels.typeids
()

Setting it to another value automatically prepares typeids and submodels:

>>> model.storagemodels.number = 2
>>> model.storagemodels.number
2
>>> model.storagemodels.typeids
(0, 0)
>>> model.storagemodels.submodels
(None, None)

When working in Cython mode, property number also prepares the analogue vectors of the cythonized model:

>>> with pub.options.usecython(True):
...     model = prepare_model("sw1d_channel")
>>> model.storagemodels.number
0
>>> model.storagemodels.submodels
()
>>> model.storagemodels.typeids
()
>>> model.storagemodels.number = 2
>>> model.storagemodels.number
2
>>> model.storagemodels.submodels
(None, None)
>>> model.storagemodels.typeids
(0, 0)
>>> model.cymodel.storagemodels._get_number()
2
>>> model.cymodel.storagemodels._get_typeid(0)
0
>>> model.cymodel.storagemodels._get_submodel(0)
put_submodel(submodel: TypeSubmodelInterface, typeid: int, position: int) None[source]

Put a submodel and its relevant type ID to the given position.

We prepare the main model and its submodel in Cython and pure Python mode to test that put_submodel() works for all possible combinations:

>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     main_py = prepare_model("sw1d_channel")
...     sub_py = prepare_model("sw1d_storage")
>>> with pub.options.usecython(True):
...     main_cy = prepare_model("sw1d_channel")
...     sub_cy = prepare_model("sw1d_storage")

For two pure Python models, there is no need to bother with synchronising cythonized models:

>>> main_py.storagemodels.number = 2
>>> main_py.storagemodels.put_submodel(submodel=sub_py, typeid=1, position=0)
>>> assert main_py.storagemodels.typeids[0] == 1
>>> assert main_py.storagemodels.submodels[0] is sub_py
>>> assert main_py.storagemodels.typeids[1] == 0
>>> assert main_py.storagemodels.submodels[1] is None

If both models are initialised in Cython mode, put_submodel() updates typeids and submodels as well as the corresponding vectors of the cythonized models:

>>> main_cy.storagemodels.number = 2
>>> main_cy.storagemodels.put_submodel(submodel=sub_cy, typeid=1, position=0)
>>> assert main_cy.storagemodels.typeids[0] == 1
>>> assert main_cy.cymodel.storagemodels._get_typeid(0) == 1
>>> assert main_cy.storagemodels.submodels[0] is sub_cy
>>> assert main_cy.cymodel.storagemodels._get_submodel(0) is sub_cy.cymodel
>>> assert main_cy.storagemodels.typeids[1] == 0
>>> assert main_cy.cymodel.storagemodels._get_typeid(1) == 0
>>> assert main_cy.storagemodels.submodels[1] is None
>>> assert main_cy.cymodel.storagemodels._get_submodel(1) is None

Connecting a pure Python mode main model with a Cython mode submodel causes no harm:

>>> main_py.storagemodels.number = 0
>>> main_py.storagemodels.number = 2
>>> main_py.storagemodels.put_submodel(submodel=sub_cy, typeid=1, position=0)
>>> assert main_py.storagemodels.typeids[0] == 1
>>> assert main_py.storagemodels.submodels[0] is sub_cy
>>> assert main_py.storagemodels.typeids[1] == 0
>>> assert main_py.storagemodels.submodels[1] is None

However, connecting a Cython mode main model with a pure Python mode submodel would result in erroneous calculations and thus raises the following error:

>>> main_cy.storagemodels.number = 0
>>> main_cy.storagemodels.number = 2
>>> main_cy.storagemodels.put_submodel(submodel=sub_py, typeid=1, position=0)
Traceback (most recent call last):
...
RuntimeError: While trying to put submodel `sw1d_storage` to position `0` of property `storagemodels` of the main model `sw1d_channel`, the following error occurred: The main model is initialised in Cython mode, but the submodel is initialised in pure Python mode so that the main model's cythonized methods could apply the submodel's methods.
>>> assert main_cy.storagemodels.typeids[0] == 0
>>> assert main_cy.cymodel.storagemodels._get_typeid(0) == 0
>>> assert main_cy.storagemodels.submodels[0] is None
>>> assert main_cy.cymodel.storagemodels._get_submodel(0) is None
>>> assert main_cy.storagemodels.typeids[1] == 0
>>> assert main_cy.cymodel.storagemodels._get_typeid(1) == 0
>>> assert main_cy.storagemodels.submodels[1] is None
>>> assert main_cy.cymodel.storagemodels._get_submodel(1) is None

Method put_submodel() checks if the given submodel follows at least one supported interface:

>>> sub_py = prepare_model("sw1d_lias")
>>> main_py.storagemodels.number = 0
>>> main_py.storagemodels.number = 2
>>> main_py.storagemodels.put_submodel(submodel=sub_py, typeid=1, position=0)
Traceback (most recent call last):
...
ValueError: While trying to put submodel `sw1d_lias` to position `0` of property `storagemodels` of the main model `sw1d_channel`, the following error occurred: The given submodel is not an instance of any of the following supported interfaces: StorageModel_V1.
>>> assert main_py.storagemodels.typeids[0] == 0
>>> assert main_py.storagemodels.submodels[0] is None
>>> assert main_py.storagemodels.typeids[1] == 0
>>> assert main_py.storagemodels.submodels[1] is None
delete_submodel(position: int) None[source]

Delete the submodel at the given position.

We prepare the main model and its submodel in Cython and pure Python mode to test that delete_submodel() works both in Cython and pure Python Cython mode:

>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     main_py = prepare_model("sw1d_channel")
...     sub_py = prepare_model("sw1d_storage")
>>> with pub.options.usecython(True):
...     main_cy = prepare_model("sw1d_channel")
...     sub_cy = prepare_model("sw1d_storage")

In pure Python mode, delete_submodel() resets the entry in the submodel vector to None and the type ID to zero:

>>> main_py.storagemodels.number = 3
>>> main_py.storagemodels.put_submodel(submodel=sub_py, typeid=1, position=1)
>>> assert main_py.storagemodels.typeids[1] == 1
>>> assert main_py.storagemodels.submodels[1] is sub_py
>>> main_py.storagemodels.delete_submodel(position=1)
>>> assert main_py.storagemodels.typeids[1] == 0
>>> assert main_py.storagemodels.submodels[1] is None

In Cython mode, delete_submodel() does the same for the analogue C vectors:

>>> main_cy.storagemodels.number = 3
>>> main_cy.storagemodels.put_submodel(submodel=sub_cy, typeid=1, position=1)
>>> assert main_cy.storagemodels.typeids[1] == 1
>>> assert main_cy.cymodel.storagemodels._get_typeid(1) == 1
>>> assert main_cy.storagemodels.submodels[1] is sub_cy
>>> assert main_cy.cymodel.storagemodels._get_submodel(1) is sub_cy.cymodel
>>> main_cy.storagemodels.delete_submodel(position=1)
>>> assert main_cy.storagemodels.typeids[1] == 0
>>> assert main_cy.cymodel.storagemodels._get_typeid(1) == 0
>>> assert main_cy.storagemodels.submodels[1] is None
>>> assert main_cy.cymodel.storagemodels._get_submodel(1) is None

Calling delete_submodel() for a position with an existing submodel does not raise a warning or error:

>>> main_cy.storagemodels.delete_submodel(position=1)

Potential errors are reported like this:

>>> main_cy.storagemodels.delete_submodel(position=3)
Traceback (most recent call last):
...
IndexError: While trying to delete a submodel at position `3` of property `storagemodels` of the main model `sw1d_channel`, the following error occurred: list assignment index out of range
append_submodel(submodel: TypeSubmodelInterface, typeid: int | None = None) None[source]

Append a submodel and its relevant type ID to the already available ones.

We prepare the main model and its submodel in Cython and pure Python mode to test that append_submodel() works for all possible combinations:

>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(False):
...     main_py = prepare_model("sw1d_channel")
...     sub_py = prepare_model("sw1d_storage")
>>> with pub.options.usecython(True):
...     main_cy = prepare_model("sw1d_channel")
...     sub_cy = prepare_model("sw1d_storage")

For two pure Python models, there is no need to bother with synchronising cythonized models:

>>> main_py.storagemodels.append_submodel(submodel=sub_py, typeid=1)
>>> assert main_py.storagemodels.number == 1
>>> assert main_py.storagemodels.typeids[0] == 1
>>> assert main_py.storagemodels.submodels[0] is sub_py

If both models are initialised in Cython mode, append_submodel() updates typeids and submodels as well as the corresponding vectors of the cythonized models:

>>> main_cy.storagemodels.append_submodel(submodel=sub_cy, typeid=1)
>>> assert main_cy.storagemodels.number == 1
>>> assert main_cy.storagemodels.typeids[0] == 1
>>> assert main_cy.cymodel.storagemodels._get_typeid(0) == 1
>>> assert main_cy.storagemodels.submodels[0] is sub_cy
>>> assert main_cy.cymodel.storagemodels._get_submodel(0) is sub_cy.cymodel

Connecting a pure Python mode main model with a Cython mode submodel causes no harm:

>>> main_py.storagemodels.append_submodel(submodel=sub_cy, typeid=1)
>>> assert main_py.storagemodels.number == 2
>>> assert main_py.storagemodels.typeids[0] == 1
>>> assert main_py.storagemodels.submodels[0] is sub_py
>>> assert main_py.storagemodels.typeids[1] == 1
>>> assert main_py.storagemodels.submodels[1] is sub_cy

However, connecting a Cython mode main model with a pure Python mode submodel would result in erroneous calculations and thus raises the following error:

>>> main_cy.storagemodels.append_submodel(submodel=sub_py, typeid=1)
Traceback (most recent call last):
...
RuntimeError: While trying to append submodel `sw1d_storage` to property `storagemodels` of the main model `sw1d_channel`, the following error occurred: The main model is initialised in Cython mode, but the submodel is initialised in pure Python mode so that the main model's cythonized methods could apply the submodel's methods.
>>> assert main_cy.storagemodels.number == 1
>>> assert main_cy.storagemodels.typeids[0] == 1
>>> assert main_cy.cymodel.storagemodels._get_typeid(0) == 1
>>> assert main_cy.storagemodels.submodels[0] is sub_cy
>>> assert main_cy.cymodel.storagemodels._get_submodel(0) is sub_cy.cymodel

Method append_submodel() checks if the given submodel follows at least one supported interface:

>>> sub_wrong = prepare_model("sw1d_lias")
>>> main_py.storagemodels.append_submodel(submodel=sub_wrong, typeid=1)
Traceback (most recent call last):
...
ValueError: While trying to append submodel `sw1d_lias` to property `storagemodels` of the main model `sw1d_channel`, the following error occurred: The given submodel is not an instance of any of the following supported interfaces: StorageModel_V1.
>>> assert main_py.storagemodels.number == 2
>>> assert main_py.storagemodels.typeids[0] == 1
>>> assert main_py.storagemodels.submodels[0] is sub_py
>>> assert main_py.storagemodels.typeids[1] == 1
>>> assert main_py.storagemodels.submodels[1] is sub_cy

For convenience, you can omit to pass the type ID. append_submodel() then detects the first suitable ID automatically:

>>> main_py.routingmodels.append_submodel(prepare_model("sw1d_weir_out"))
>>> main_py.routingmodels.append_submodel(prepare_model("sw1d_q_in"))
>>> main_py.routingmodels.append_submodel(prepare_model("sw1d_lias"))
>>> assert main_py.routingmodels.number == 3
>>> assert main_py.routingmodels.typeids == (3, 1, 2)

Method append_submodel() checks if the given submodel follows at least one supported interface:

>>> main_py.routingmodels[0].routingmodelsupstream.append_submodel(
...     prepare_model("sw1d_q_out"))
Traceback (most recent call last):
...
ValueError: While trying to append submodel `sw1d_q_out` to property `routingmodelsupstream` of the main model `sw1d_weir_out`, the following error occurred: The given submodel is not an instance of any of the following supported interfaces: RoutingModel_V1 and RoutingModel_V2.
property submodels: tuple[TypeSubmodelInterface | None, ...]

The currently handled submodels.

>>> from hydpy import prepare_model
>>> main = prepare_model("sw1d_channel")
>>> sub1 = prepare_model("sw1d_q_in")
>>> sub2 = prepare_model("sw1d_lias")
>>> main.routingmodels.append_submodel(submodel=sub1, typeid=1)
>>> main.routingmodels.append_submodel(submodel=sub2, typeid=1)
>>> assert main.routingmodels.submodels == (sub1, sub2)
property typeids: tuple[int, ...]

The interface-specific type IDs of the currently handled submodels.

>>> from hydpy import prepare_model
>>> main = prepare_model("sw1d_channel")
>>> sub1 = prepare_model("sw1d_q_in")
>>> sub2 = prepare_model("sw1d_lias")
>>> main.routingmodels.append_submodel(submodel=sub1, typeid=1)
>>> main.routingmodels.append_submodel(submodel=sub2, typeid=1)
>>> assert main.routingmodels.typeids == (1, 1)
class hydpy.core.modeltools.SubmodelIsMainmodelProperty(doc: str | None = None)[source]

Bases: object

Descriptor for boolean “submodel_is_mainmodel” attributes.

SubmodelIsMainmodelProperty instances work like simple boolean attributes but silently synchronise the equally named boolean attributes of the corresponding cython model, if available:

>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(True):
...     model = prepare_model("hland_96")
>>> type(model).aetmodel_is_mainmodel._name
'aetmodel_is_mainmodel'
>>> model.aetmodel_is_mainmodel
False
>>> model.cymodel.aetmodel_is_mainmodel
0
>>> model.aetmodel_is_mainmodel = True
>>> model.aetmodel_is_mainmodel
True
>>> model.cymodel.aetmodel_is_mainmodel
1
class hydpy.core.modeltools.SubmodelTypeIDProperty(doc: str | None = None)[source]

Bases: object

Descriptor for integer “submodel_typeid” attributes.

SubmodelTypeIDProperty instances work like simple integer attributes but silently synchronise the equally named integer attributes of the corresponding cython model, if available:

>>> from hydpy import prepare_model, pub
>>> with pub.options.usecython(True):
...     model = prepare_model("hland_96")
>>> type(model).aetmodel_typeid._name
'aetmodel_typeid'
>>> model.aetmodel_typeid
0
>>> model.cymodel.aetmodel_typeid
0
>>> model.aetmodel_typeid = 1
>>> model.aetmodel_typeid
1
>>> model.cymodel.aetmodel_typeid
1
class hydpy.core.modeltools.IndexProperty[source]

Bases: object

Base class for index descriptors like Idx_Sim.

name: str
class hydpy.core.modeltools.Idx_Sim[source]

Bases: IndexProperty

The simulation step index.

Some model methods require knowing the index of the current simulation step (with respect to the initialisation period), which one usually updates by passing it to simulate(). However, you can change it manually via the Idx_Sim descriptor, which is often beneficial during testing:

>>> from hydpy.models.hland_96 import *
>>> parameterstep("1d")
>>> model.idx_sim
0
>>> model.idx_sim = 1
>>> model.idx_sim
1

Like other objects of IndexProperty subclasses, Idx_Sim objects are aware of their name:

>>> Model.idx_sim.name
'idx_sim'
class hydpy.core.modeltools.Idx_HRU[source]

Bases: IndexProperty

The hydrological response unit index.

The documentation on class Idx_Sim explains the general purpose and handling of IndexProperty instances.

class hydpy.core.modeltools.Idx_Segment[source]

Bases: IndexProperty

The segment index.

The documentation on class Idx_Sim explains the general purpose and handling of IndexProperty instances.

class hydpy.core.modeltools.Idx_Run[source]

Bases: IndexProperty

The run index.

The documentation on class Idx_Sim explains the general purpose and handling of IndexProperty instances.

class hydpy.core.modeltools.DocName(short: str, description: str = 'base model')[source]

Bases: NamedTuple

Definitions for the documentation names of specific base or application models.

short: str

Short name of a model, e.g. “W-Wag”.

description: str

Description of a model, e.g. “extended version of the original Wageningen WALRUS model”.

property long

Long name of a model.

>>> from hydpy.models.wland_wag import Model
>>> Model.DOCNAME.long
'HydPy-W-Wag'
property complete: str

Complete presentation of a model.

>>> from hydpy.models.wland_wag import Model
>>> Model.DOCNAME.complete
'HydPy-W-Wag (extended version of the original Wageningen WALRUS model)'
property family: str

Family name of a model.

>>> from hydpy.models.wland_wag import Model
>>> Model.DOCNAME.family
'HydPy-W'
class hydpy.core.modeltools.Model[source]

Bases: object

Base class for all hydrological models.

Class Model provides everything to create a usable application model, except method simulate(). See classes AdHocModel and ELSModel, which implement this method.

Class Model does not prepare the strongly required attributes parameters and sequences during initialisation. You need to add them manually whenever you want to prepare a workable Model object on your own (see the factory functions prepare_model() and parameterstep(), which do this regularly).

Similar to parameters and sequences, there is also the dynamic masks attribute, making all predefined masks of the actual model type available within a Masks object:

>>> from hydpy.models.hland_96 import *
>>> parameterstep("1d")
>>> model.masks
complete of module hydpy.models.hland.hland_masks
land of module hydpy.models.hland.hland_masks
upperzone of module hydpy.models.hland.hland_masks
snow of module hydpy.models.hland.hland_masks
soil of module hydpy.models.hland.hland_masks
field of module hydpy.models.hland.hland_masks
forest of module hydpy.models.hland.hland_masks
ilake of module hydpy.models.hland.hland_masks
glacier of module hydpy.models.hland.hland_masks
sealed of module hydpy.models.hland.hland_masks
noglacier of module hydpy.models.hland.hland_masks

You can use these masks, for example, to average the zone-specific precipitation values handled by sequence PC. When passing no argument, method average_values() applies the complete mask. For example, pass mask land to average the values of all zones except those of type ILAKE:

>>> nmbzones(4)
>>> zonetype(FIELD, FOREST, GLACIER, ILAKE)
>>> zonearea.values = 1.0
>>> fluxes.pc = 1.0, 3.0, 5.0, 7.0
>>> fluxes.pc.average_values()
4.0
>>> fluxes.pc.average_values(model.masks.land)
3.0
parameters: parametertools.Parameters
sequences: sequencetools.Sequences
masks: masktools.Masks
idx_sim

The simulation step index.

METHOD_GROUPS: ClassVar[tuple[str, ...]]
REUSABLE_METHODS: ClassVar[tuple[type[ReusableMethod], ...]]
COMPOSITE: bool = False

Flag for informing whether the respective Model subclass is usually not directly applied by model users but behind the scenes for compositing all models owned by elements belonging to the same collective (see method unite_collectives()).

DOCNAME: DocName
cymodel: CyModelProtocol | None
property element: Element

The model instance’s master element.

Usually, one assigns a Model instance to an Element instance, but the other way round works as well (for more information, see the documentation on property model of class Element):

>>> from hydpy import Element, prepare_model
>>> from hydpy.core.modeltools import Model
>>> model = prepare_model("musk_classic")
>>> model.element
Traceback (most recent call last):
...
hydpy.core.exceptiontools.AttributeNotReady: Model `musk_classic` is not connected to an `Element` so far.
>>> e = Element("e")
>>> model.element = e
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `outlet` sequences of the model handled by element `e`, the following error occurred: Sequence `q` of element `e` cannot be connected due to no available node handling variable `Q`.
>>> model.element
Element("e")
>>> e.model.name
'musk_classic'
>>> del model.element
>>> model.element
Traceback (most recent call last):
...
hydpy.core.exceptiontools.AttributeNotReady: Model `musk_classic` is not connected to an `Element` so far.
>>> e.model
Traceback (most recent call last):
...
hydpy.core.exceptiontools.AttributeNotReady: The model object of element `e` has been requested but not been prepared so far.
connect() None[source]

Connect all LinkSequence objects and the selected InputSequence and OutputSequence objects of the actual model to the corresponding NodeSequence objects.

You cannot connect any sequences until the Model object itself is connected to an Element object referencing the required Node objects:

>>> from hydpy import prepare_model
>>> prepare_model("musk_classic").connect()
Traceback (most recent call last):
...
hydpy.core.exceptiontools.AttributeNotReady: While trying to build the node connection of the `input` sequences of the model handled by element `?`, the following error occurred: Model `musk_classic` is not connected to an `Element` so far.

The application model musk_classic can receive inflow from an arbitrary number of upstream nodes and passes its outflow to a single downstream node (note that property model of class Element calls method connect() automatically):

>>> from hydpy import Element, Node
>>> in1 = Node("in1", variable="Q")
>>> in2 = Node("in2", variable="Q")
>>> out1 = Node("out1", variable="Q")
>>> element1 = Element("element1", inlets=(in1, in2), outlets=out1)
>>> element1.model = prepare_model("musk_classic")

Now all connections work as expected:

>>> in1.sequences.sim = 1.0
>>> in2.sequences.sim = 2.0
>>> out1.sequences.sim = 3.0
>>> element1.model.sequences.inlets.q
q(1.0, 2.0)
>>> element1.model.sequences.outlets.q
q(3.0)
>>> element1.model.sequences.inlets.q *= 2.0
>>> element1.model.sequences.outlets.q *= 2.0
>>> in1.sequences.sim
sim(2.0)
>>> in2.sequences.sim
sim(4.0)
>>> out1.sequences.sim
sim(6.0)

To show some possible errors and related error messages, we define three additional nodes, two handling variables different from discharge (Q):

>>> in3 = Node("in3", variable="X")
>>> out2 = Node("out2", variable="Q")
>>> out3 = Node("out3", variable="X")

Link sequence names must match the variable a node is handling:

>>> element2 = Element("element2", inlets=(in1, in2), outlets=out3)
>>> element2.model = prepare_model("musk_classic")
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `outlet` sequences of the model handled by element `element2`, the following error occurred: Sequence `q` of element `element2` cannot be connected due to no available node handling variable `Q`.

One can connect a 0-dimensional link sequence to a single node sequence only:

>>> element3 = Element("element3", inlets=(in1, in2), outlets=(out1, out2))
>>> element3.model = prepare_model("musk_classic")
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `outlet` sequences of the model handled by element `element3`, the following error occurred: Sequence `q` cannot be connected as it is 0-dimensional but multiple nodes are available which are handling variable `Q`.

Method connect() generally reports about unusable node sequences:

>>> element4 = Element("element4", inlets=(in1, in2), outlets=(out1, out3))
>>> element4.model = prepare_model("musk_classic")
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `outlet` sequences of the model handled by element `element4`, the following error occurred: The following nodes have not been connected to any sequences: out3.
>>> element5 = Element("element5", inlets=(in1, in2, in3), outlets=out1)
>>> element5.model = prepare_model("musk_classic")
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `inlet` sequences of the model handled by element `element5`, the following error occurred: The following nodes have not been connected to any sequences: in3.
>>> element6 = Element("element6", inlets=in1, outlets=out1, receivers=in2)
>>> element6.model = prepare_model("musk_classic")
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `receiver` sequences of the model handled by element `element6`, the following error occurred: The following nodes have not been connected to any sequences: in2.
>>> element7 = Element("element7", inlets=in1, outlets=out1, senders=in2)
>>> element7.model = prepare_model("musk_classic")
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `sender` sequences of the model handled by element `element7`, the following error occurred: The following nodes have not been connected to any sequences: in2.

The above examples explain how to connect link sequences to their nodes. Such connections are relatively hard requirements (musk_classic definitively needs inflow provided from a node, which the node itself typically receives from another model). In contrast, connections between input or output sequences and nodes are optional. If one defines such a connection for an input sequence, it receives data from the related node; otherwise, it uses its individually managed data, usually read from a file. If one defines such a connection for an output sequence, it passes its internal data to the related node; otherwise, nothing happens.

We demonstrate this functionality by focussing on the input sequences T and P and the output sequences Q0 and UZ of application model hland_96. T uses its own data (which we define manually, but we could read it from a file as well), whereas P gets its data from node inp1. Flux sequence Q0 and state sequence UZ pass their data to two separate output nodes, whereas all other fluxes and states do not. This functionality requires telling each node which sequence it should connect to, which we do by passing the sequence types (or the globally available aliases hland_inputs_P, hland_fluxes_Q0, and hland_states_UZ) to the variable keyword of different node objects:

>>> from hydpy import pub
>>> from hydpy.aliases import hland_inputs_P, hland_fluxes_Q0, hland_states_UZ
>>> pub.timegrids = "2000-01-01", "2000-01-06", "1d"
>>> inp1 = Node("inp1", variable=hland_inputs_P)
>>> outp1 = Node("outp1", variable=hland_fluxes_Q0)
>>> outp2 = Node("outp2", variable=hland_states_UZ)
>>> element8 = Element("element8", outlets=out1, inputs=inp1,
...                    outputs=[outp1, outp2])
>>> element8.model = prepare_model("hland_96")
>>> element8.prepare_inputseries()
>>> element8.model.sequences.inputs.t.series = 1.0, 2.0, 3.0, 4.0, 5.0
>>> inp1.sequences.sim(9.0)
>>> element8.model.load_data(2)
>>> element8.model.sequences.inputs.t
t(3.0)
>>> element8.model.sequences.inputs.p
p(9.0)
>>> element8.model.sequences.fluxes.q0 = 99.0
>>> element8.model.sequences.states.uz = 999.0
>>> element8.model.update_outputs()
>>> outp1.sequences.sim
sim(99.0)
>>> outp2.sequences.sim
sim(999.0)

Instead of using single InputSequence and OutputSequence subclasses, one can create and apply fused variables, combining multiple subclasses (see the documentation on class FusedVariable for more information and a more realistic example):

>>> from hydpy import FusedVariable
>>> from hydpy.aliases import lland_inputs_Nied, lland_fluxes_QDGZ
>>> Precip = FusedVariable("Precip", hland_inputs_P, lland_inputs_Nied)
>>> inp2 = Node("inp2", variable=Precip)
>>> FastRunoff = FusedVariable("FastRunoff", hland_fluxes_Q0, lland_fluxes_QDGZ)
>>> outp3 = Node("outp3", variable=FastRunoff)
>>> element9 = Element("element9", outlets=out1, inputs=inp2, outputs=outp3)
>>> element9.model = prepare_model("hland_96")
>>> inp2.sequences.sim(9.0)
>>> element9.model.load_data(0)
>>> element9.model.sequences.inputs.p
p(9.0)
>>> element9.model.sequences.fluxes.q0 = 99.0
>>> element9.model.update_outputs()
>>> outp3.sequences.sim
sim(99.0)

Method connect() reports if one of the given fused variables does not find a fitting sequence:

>>> from hydpy.aliases import lland_inputs_TemL
>>> Wrong = FusedVariable("Wrong", lland_inputs_Nied, lland_inputs_TemL)
>>> inp3 = Node("inp3", variable=Wrong)
>>> element10 = Element("element10", outlets=out1, inputs=inp3)
>>> element10.model = prepare_model("hland_96")
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `input` sequences of the model handled by element `element10`, the following error occurred: The following nodes have not been connected to any sequences: inp3.
>>> outp4 = Node("outp4", variable=Wrong)
>>> element11 = Element("element11", outlets=out1, outputs=outp4)
>>> element11.model = prepare_model("hland_96")
Traceback (most recent call last):
...
TypeError: While trying to build the node connection of the `output` sequences of the model handled by element `element11`, the following error occurred: None of the output sequences of model `hland_96` is among the sequences of the fused variable `Wrong` of node `outp4`.

Selecting the wrong sequences results in the following error messages:

>>> outp5 = Node("outp5", variable=hland_fluxes_Q0)
>>> element12 = Element("element12", outlets=out1, inputs=outp5)
>>> element12.model = prepare_model("hland_96")
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `input` sequences of the model handled by element `element12`, the following error occurred: The following nodes have not been connected to any sequences: outp5.
>>> inp5 = Node("inp5", variable="P")
>>> element13 = Element("element13", outlets=out1, outputs=inp5)
>>> element13.model = prepare_model("hland_96")
Traceback (most recent call last):
...
TypeError: While trying to build the node connection of the `output` sequences of the model handled by element `element13`, the following error occurred: No factor, flux, or state sequence of model `hland_96` is named `p`.

So far, you can build connections to 0-dimensional output sequences only:

>>> from hydpy.models.hland.hland_fluxes import PC
>>> outp6 = Node("outp6", variable=PC)
>>> element14 = Element("element14", outlets=out1, outputs=outp6)
>>> element14.model = prepare_model("hland_96")
Traceback (most recent call last):
...
TypeError: While trying to build the node connection of the `output` sequences of the model handled by element `element14`, the following error occurred: Only connections with 0-dimensional output sequences are supported, but sequence `pc` is 1-dimensional.

FusedVariable also supports ReceiverSequence for passing information from output nodes to receiver sequences (instead of input sequences, which we demonstrated in the above examples). We take the receiver sequences OWL (outer water level) and RWL (remote water level) used by the application model dam_pump as an example:

>>> from hydpy.aliases import dam_receivers_OWL, dam_receivers_RWL

One dam_pump instance (handled by element dam1) shall receive the water level (WaterLevel) of two independent dam_pump instances. dam1 interprets the water level of dam2 as its outer water level and the water level of dam3 as its remote water level:

>>> from hydpy.aliases import dam_factors_WaterLevel
>>> owl = FusedVariable("OWL", dam_receivers_OWL, dam_factors_WaterLevel)
>>> rwl = FusedVariable("RWL", dam_receivers_RWL, dam_factors_WaterLevel)
>>> n21, n31 = Node("n21", variable=owl), Node("n31", variable=rwl)
>>> x, y = Node("x", variable=owl), Node("y", variable=rwl)
>>> dam1 = Element("dam1", inlets="n01", outlets="n12",
...                receivers=(n21, n31))
>>> dam2 = Element("dam2", inlets="n12", outlets="n23",
...                receivers=(x,y), outputs=n21)
>>> dam3 = Element("dam3", inlets="n23", outlets="n34",
...                receivers=(x, y), outputs=n31)
>>> dam1.model = prepare_model("dam_pump")
>>> dam2.model = prepare_model("dam_pump")
>>> dam3.model = prepare_model("dam_pump")

We confirm that all connections are correctly built by letting dam2 and dam3 send different water levels:

>>> dam2.model.sequences.factors.waterlevel = 2.0
>>> dam2.model.update_outputs()
>>> dam3.model.sequences.factors.waterlevel = 3.0
>>> dam3.model.update_outputs()
>>> dam1.model.sequences.receivers.owl
owl(2.0)
>>> dam1.model.sequences.receivers.rwl
rwl(3.0)
property name: str

Name of the model type.

For base models, name corresponds to the package name:

>>> from hydpy import prepare_model
>>> hland = prepare_model("hland")
>>> hland.name
'hland'

For application models, name to corresponds the module name:

>>> hland_96 = prepare_model("hland_96")
>>> hland_96.name
'hland_96'

This last example has only technical reasons:

>>> hland.name
'hland'
prepare_allseries(allocate_ram: bool = True, jit: bool = False) None[source]

Call method prepare_inputseries() with read_jit=jit and methods prepare_factorseries(), prepare_fluxseries(), and prepare_stateseries() with write_jit=jit.

prepare_inputseries(allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False) None[source]

Call method prepare_series() of all directly handled InputSequence objects.

prepare_factorseries(allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False) None[source]

Call method prepare_series() of all directly handled FactorSequence objects.

prepare_fluxseries(allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False) None[source]

Call method prepare_series() of all directly handled FluxSequence.

prepare_stateseries(allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False) None[source]

Call method prepare_series() of all directly handled StateSequence objects and.

load_allseries() None[source]

Call method load_inputseries(), load_factorseries(), load_fluxseries(), and load_stateseries().

load_inputseries() None[source]

Call method load_series() of all directly handled InputSequence objects.

load_factorseries() None[source]

Call method load_series() of all directly handled FactorSequence objects.

load_fluxseries() None[source]

Call method load_series() of all directly handled FluxSequence objects.

load_stateseries() None[source]

Call method load_series() of all directly handled StateSequence objects.

save_allseries() None[source]

Call method save_inputseries(), save_factorseries(), save_fluxseries(), and save_stateseries().

save_inputseries() None[source]

Call method save_series() of all directly handled InputSequence objects.

save_factorseries() None[source]

Call method save_series() of all directly handled FactorSequence objects.

save_fluxseries() None[source]

Call method save_series() of all directly handled FluxSequence objects.

save_stateseries() None[source]

Call method save_series() of all directly handled StateSequence objects.

get_controlfileheader(import_submodels: bool = True, parameterstep: timetools.PeriodConstrArg | None = None, simulationstep: timetools.PeriodConstrArg | None = None) str[source]

Return the header of a parameter control file.

The header contains the default coding information, the model import commands and the actual parameter and simulation step sizes:

>>> from hydpy import prepare_model, pub
>>> model = prepare_model("hland_96")
>>> model.aetmodel = prepare_model("evap_aet_hbv96")
>>> pub.timegrids = "2000.01.01", "2001.01.01", "1h"
>>> print(model.get_controlfileheader())
# -*- coding: utf-8 -*-

from hydpy.models.hland_96 import *
from hydpy.models import evap_aet_hbv96

simulationstep("1h")
parameterstep("1d")

Optionally, you can omit the submodel import lines and define alternative parameter step and simulation step sizes:

>>> print(model.get_controlfileheader(
...     import_submodels=False, parameterstep="2d", simulationstep="3d"))
# -*- coding: utf-8 -*-

from hydpy.models.hland_96 import *

simulationstep("3d")
parameterstep("2d")

save_controls(parameterstep: timetools.PeriodConstrArg | None = None, simulationstep: timetools.PeriodConstrArg | None = None, auxfiler: auxfiletools.Auxfiler | None = None, filepath: str | None = None) None[source]

Write the control parameters (and eventually some solver parameters) to a control file.

Usually, a control file consists of a header (see the documentation on the method get_controlfileheader()) and the string representations of the individual Parameter objects handled by the control SubParameters object.

The main functionality of method save_controls() is demonstrated in the documentation on method save_controls() of class HydPy, which one should apply to write the parameter information of complete HydPy projects. However, calling save_controls() on individual Model objects offers the advantage of choosing an arbitrary file path, as shown in the following example:

>>> from hydpy.models.test_stiff1d import *
>>> parameterstep("1d")
>>> simulationstep("1h")
>>> k(0.1)
>>> n(3)
>>> from hydpy import Open
>>> with Open():
...     model.save_controls(filepath="otherdir/otherfile.py")
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
otherdir/otherfile.py
---------------------------------------
# -*- coding: utf-8 -*-

from hydpy.models.test_stiff1d import *

simulationstep("1h")
parameterstep("1d")

k(0.1)
n(3)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Method save_controls() also writes the string representations of all SolverParameter objects with non-default values into the control file:

>>> solver.abserrormax(1e-6)
>>> with Open():
...     model.save_controls(filepath="otherdir/otherfile.py")
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
otherdir/otherfile.py
---------------------------------------
# -*- coding: utf-8 -*-

from hydpy.models.test_stiff1d import *

simulationstep("1h")
parameterstep("1d")

k(0.1)
n(3)

solver.abserrormax(0.000001)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Without a given file path and a proper project configuration, method save_controls() raises the following error:

>>> model.save_controls()
Traceback (most recent call last):
...
RuntimeError: To save the control parameters of a model to a file, its filename must be known.  This can be done, by passing a filename to function `save_controls` directly.  But in complete HydPy applications, it is usally assumed to be consistent with the name of the element handling the model.

Submodels like meteo_glob_fao56 allow using their instances by multiple main models. We prepare such a case by selecting such an instance as the submodel of the absolute main model lland_knauf and the the relative submodel evap_aet_morsim:

>>> from hydpy.core.importtools import reverse_model_wildcard_import
>>> reverse_model_wildcard_import()
>>> from hydpy import pub
>>> pub.timegrids = "2000-01-01", "2001-01-02", "1d"
>>> from hydpy.models.lland_knauf import *
>>> parameterstep()
>>> nhru(1)
>>> ft(1.0)
>>> fhru(1.0)
>>> lnk(ACKER)
>>> measuringheightwindspeed(10.0)
>>> lai(3.0)
>>> wmax(300.0)
>>> with model.add_radiationmodel_v1("meteo_glob_fao56") as meteo_glob_fao56:
...     latitude(50.0)
>>> with model.add_aetmodel_v1("evap_aet_morsim"):
...     measuringheightwindspeed(2.0)
...     model.add_radiationmodel_v1(meteo_glob_fao56)

To avoid name collisions, save_controls() prefixes the string submodel_ to the submodel name (which is identical to the submodel module’s name) to create the name of the variable that references the shared model’s instance:

>>> with Open():  
...     model.save_controls(filepath="otherdir/otherfile.py")
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
otherdir/otherfile.py
----------------------------------------------------------------------------...
# -*- coding: utf-8 -*-
...
from hydpy.models.lland_knauf import *
from hydpy.models import evap_aet_morsim
from hydpy.models import meteo_glob_fao56
...
simulationstep("1d")
parameterstep("1d")
...
ft(1.0)
...
measuringheightwindspeed(10.0)
...
with model.add_aetmodel_v1(evap_aet_morsim):
    measuringheightwindspeed(2.0)
    ...
    with model.add_radiationmodel_v1(meteo_glob_fao56) as submodel_meteo_glob_fao56:
        latitude(50.0)
        ...
model.add_radiationmodel_v1(submodel_meteo_glob_fao56)
...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
define_conditions(module: ModuleType | str | None = None) Generator[None, None, None][source]

Allow defining the values of condition sequences in condition files conveniently.

define_conditions() works similar to the “add_submodel” methods wrapped by instances of class SubmodelAdder but is much simpler. In combination with the with statement, it makes the all relevant state and log sequences temporarily directly available:

>>> from hydpy import pub
>>> pub.timegrids = "2000-01-01", "2001-01-01", "6h"
>>> from hydpy.models.lland_knauf import *
>>> parameterstep()
>>> nhru(2)
>>> ft(10.0)
>>> fhru(0.2, 0.8)
>>> lnk(ACKER, MISCHW)
>>> wmax(acker=100.0, mischw=200.0)
>>> measuringheightwindspeed(10.0)
>>> with model.add_aetmodel_v1("evap_aet_morsim"):
...     pass
>>> with model.aetmodel.define_conditions():
...     loggedwindspeed2m(1.0, 3.0, 2.0, 4.0)
>>> loggedwindspeed2m
Traceback (most recent call last):
...
NameError: name 'loggedwindspeed2m' is not defined
>>> model.aetmodel.sequences.logs.loggedwindspeed2m
loggedwindspeed2m(1.0, 3.0, 2.0, 4.0)

One can pass the submodel’s module or name for documentation purposes:

>>> with model.aetmodel.define_conditions("evap_aet_morsim"):
...     loggedwindspeed2m(4.0, 2.0, 3.0, 1.0)
>>> loggedwindspeed2m
Traceback (most recent call last):
...
NameError: name 'loggedwindspeed2m' is not defined
>>> model.aetmodel.sequences.logs.loggedwindspeed2m
loggedwindspeed2m(4.0, 2.0, 3.0, 1.0)

For misleading input, define_conditions() raises the following error:

>>> from hydpy.models import evap_aet_hbv96
>>> with model.aetmodel.define_conditions(evap_aet_hbv96):
...     loggedwindspeed2m(1.0, 3.0, 2.0, 4.0)
Traceback (most recent call last):
...
TypeError: While trying to define the conditions of (sub)model `evap_aet_morsim`, the following error occurred: (Sub)model `evap_aet_morsim` is not of type `evap_aet_hbv96`.
>>> loggedwindspeed2m
Traceback (most recent call last):
...
NameError: name 'loggedwindspeed2m' is not defined
>>> model.aetmodel.sequences.logs.loggedwindspeed2m
loggedwindspeed2m(4.0, 2.0, 3.0, 1.0)
load_conditions(filename: str | None = None) None[source]

Read the initial conditions from a file and assign them to the respective StateSequence and LogSequence objects.

The documentation on method load_conditions() of class HydPy explains how to read and write condition values for complete HydPy projects in the most convenient manner. However, using the underlying methods load_conditions() and save_conditions() directly offers the advantage of specifying alternative filenames. We demonstrate this by using the state sequence SM if the land_dill_assl Element object of the HydPy-H-Lahn example project:

>>> from hydpy.core.testtools import prepare_full_example_2
>>> hp, pub, TestIO = prepare_full_example_2()
>>> dill_assl = hp.elements.land_dill_assl.model
>>> dill_assl.sequences.states.sm
sm(185.13164, 181.18755, 199.80432, 196.55888, 212.04018, 209.48859,
   222.12115, 220.12671, 230.30756, 228.70779, 236.91943, 235.64427)

We work in the freshly created condition directory test:

>>> with TestIO():
...     pub.conditionmanager.currentdir = "test"

We set all soil moisture values to zero and write the updated values to file cold_start.py:

>>> dill_assl.sequences.states.sm(0.0)
>>> with TestIO():
...     dill_assl.save_conditions("cold_start.py")

Trying to reload from the written file (after changing the soil moisture values again) without passing the file name fails due to the wrong assumption that the element’s name serves as the file name base:

>>> dill_assl.sequences.states.sm(100.0)
>>> with TestIO():   
...     dill_assl.load_conditions()
Traceback (most recent call last):
...
FileNotFoundError: While trying to load the initial conditions of element `land_dill_assl`, the following error occurred: [Errno 2] No such file or directory: '...land_dill_assl.py'

One does not need to explicitly state the file extensions (.py):

>>> with TestIO():
...     dill_assl.load_conditions("cold_start")
>>> dill_assl.sequences.states.sm
sm(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)

Automatically determining the file name requires a proper reference to the related Element object:

>>> del dill_assl.element
>>> with TestIO():
...     dill_assl.save_conditions()
Traceback (most recent call last):
...
RuntimeError: While trying to save the actual conditions of element `?`, the following error occurred: To load or save the conditions of a model from or to a file, its filename must be known.  This can be done, by passing filename to method `load_conditions` or `save_conditions` directly.  But in complete HydPy applications, it is usally assumed to be consistent with the name of the element handling the model.  Actually, neither a filename is given nor does the model know its master element.

The submodels selected in the HydPy-H-Lahn example project do not require any condition sequences. Hence, we replace the combination of evap_aet_hbv96 and evap_pet_hbv96 with a plain evap_aet_morsim instance, which relies on some log sequences:

>>> with dill_assl.add_aetmodel_v1("evap_aet_morsim"):
...     pass

The following code demonstrates that reading and writing of condition sequences also works for submodels:

>>> logs = dill_assl.aetmodel.sequences.logs
>>> logs.loggedairtemperature = 20.0
>>> logs.loggedwindspeed2m = 2.0
>>> with TestIO():   
...     dill_assl.save_conditions("submodel_conditions.py")
>>> logs.loggedairtemperature = 10.0
>>> logs.loggedwindspeed2m = 1.0
>>> with TestIO():   
...     dill_assl.load_conditions("submodel_conditions.py")
>>> logs.loggedairtemperature
loggedairtemperature(20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0,
                     20.0, 20.0, 20.0, 20.0)
>>> logs.loggedwindspeed2m
loggedwindspeed2m(2.0)
save_conditions(filename: str | None = None) None[source]

Query the actual conditions of the StateSequence and LogSequence objects and write them into an initial condition file.

See the documentation on method load_conditions() for further information.

trim_conditions() None[source]

Call method trim_conditions() of the handled Sequences object.

reset_conditions() None[source]

Call method reset() of the handled Sequences object.

abstract simulate(idx: int) None[source]

Perform a simulation run over a single simulation time step.

reset_reuseflags() None[source]

Reset all REUSEMARKER attributes of the current model instance and its submodels (usually at the beginning of a simulation step).

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

load_data(idx: int) None[source]

Call method load_data() of the attribute sequences of the current model instance and its submodels.

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

save_data(idx: int) None[source]

Call method save_data() of the attribute sequences of the current model instance and its submodels.

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

update_inlets() None[source]

Call all methods defined as “INLET_METHODS” in the defined order.

>>> from hydpy.core.modeltools import AdHocModel, Method
>>> class print_1(Method):
...     @staticmethod
...     def __call__(self):
...         print(1)
>>> class print_2(Method):
...     @staticmethod
...     def __call__(self):
...         print(2)
>>> class Test(AdHocModel):
...     INLET_METHODS = print_1, print_2
>>> Test().update_inlets()
1
2

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

update_outlets() None[source]

Call all methods defined as “OUTLET_METHODS” in the defined order.

>>> from hydpy.core.modeltools import AdHocModel, Method
>>> class print_1(Method):
...     @staticmethod
...     def __call__(self):
...         print(1)
>>> class print_2(Method):
...     @staticmethod
...     def __call__(self):
...         print(2)
>>> class Test(AdHocModel):
...     OUTLET_METHODS = print_1, print_2
>>> Test().update_outlets()
1
2

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

update_receivers(idx: int) None[source]

Call all methods defined as “RECEIVER_METHODS” in the defined order.

>>> from hydpy.core.modeltools import AdHocModel, Method
>>> class print_1(Method):
...     @staticmethod
...     def __call__(self):
...        print(test.idx_sim+1)
>>> class print_2(Method):
...     @staticmethod
...     def __call__(self):
...         print(test.idx_sim+2)
>>> class Test(AdHocModel):
...     RECEIVER_METHODS = print_1, print_2
>>> test = Test()
>>> test.update_receivers(1)
2
3

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

update_senders(idx: int) None[source]

Call all methods defined as “SENDER_METHODS” in the defined order.

>>> from hydpy.core.modeltools import AdHocModel, Method
>>> class print_1(Method):
...     @staticmethod
...     def __call__(self):
...        print(test.idx_sim+1)
>>> class print_2(Method):
...     @staticmethod
...     def __call__(self):
...         print(test.idx_sim+2)
>>> class Test(AdHocModel):
...     SENDER_METHODS = print_1, print_2
>>> test = Test()
>>> test.update_senders(1)
2
3

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

new2old() None[source]

Call method new2old() of subattribute sequences.states.

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

update_outputs() None[source]

Call method update_outputs() of attribute sequences.

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

classmethod get_methods(skip: tuple[Literal['RECEIVER_METHODS', 'INLET_METHODS', 'RUN_METHODS', 'PART_ODE_METHODS', 'FULL_ODE_METHODS', 'ADD_METHODS', 'INTERFACE_METHODS', 'OUTLET_METHODS', 'SENDER_METHODS'], ...] = ()) Iterator[type[Method]][source]

Convenience method for iterating through all methods selected by a Model subclass.

>>> from hydpy.models import hland_96, ga_garto_submodel1
>>> for method in hland_96.Model.get_methods():
...     print(method.__name__)   
Calc_TC_V1
...
Pass_Q_V1
>>> for method in ga_garto_submodel1.Model.get_methods():
...     print(method.__name__)   
Set_InitialSurfaceWater_V1
...
Get_SoilWaterContent_V1
Return_RelativeMoisture_V1
...
Withdraw_AllBins_V1

One can skip all methods that belong to specific groups:

>>> for method in hland_96.Model.get_methods(skip=("OUTLET_METHODS",)):
...     print(method.__name__)   
Calc_TC_V1
...
Calc_OutRC_RConcModel_V1
>>> for method in hland_96.Model.get_methods(("OUTLET_METHODS", "ADD_METHODS")):
...     print(method.__name__)   
Calc_TC_V1
...
Calc_QT_V1

Note that function get_methods() returns the “raw” Method objects instead of the modified Python or Cython functions used for performing calculations.

find_submodels(*, include_subsubmodels: bool = True, include_mainmodel: bool = False, include_sidemodels: bool = False, include_optional: bool = False, include_feedbacks: bool = False, aggregate_vectors: bool = False, repeat_sharedmodels: bool = False, position: Literal[0, -1] | None = None) dict[str, Model] | dict[str, Model | None][source]

Find the (sub)submodel instances of the current main model instance.

Method find_submodels() returns an empty dictionary by default if no submodel is available:

>>> from hydpy import prepare_model
>>> model = prepare_model("lland_knauf")
>>> model.find_submodels()
{}

The include_mainmodel parameter allows the addition of the main model:

>>> model.find_submodels(include_mainmodel=True)
{'model': lland_knauf}

The include_optional parameter allows considering prepared and unprepared submodels:

>>> model.find_submodels(include_optional=True)
{'model.aetmodel': None, 'model.radiationmodel': None, 'model.soilmodel': None}
>>> model.aetmodel = prepare_model("evap_aet_minhas")
>>> model.aetmodel.petmodel = prepare_model("evap_pet_mlc")
>>> model.aetmodel.petmodel.retmodel = prepare_model("evap_ret_tw2002")
>>> from pprint import pprint
>>> pprint(model.find_submodels(include_optional=True))  
{'model.aetmodel': evap_aet_minhas...,
 'model.aetmodel.intercmodel': None,
 'model.aetmodel.petmodel': evap_pet_mlc...,
 'model.aetmodel.petmodel.retmodel': evap_ret_tw2002,
 'model.aetmodel.petmodel.retmodel.radiationmodel': None,
 'model.aetmodel.petmodel.retmodel.tempmodel': None,
 'model.aetmodel.soilwatermodel': None,
 'model.radiationmodel': None,
 'model.soilmodel': None}

By default, find_submodels() does not return an additional entry when a main model serves as a sub-submodel:

>>> model.aetmodel.soilwatermodel = model
>>> model.aetmodel.soilwatermodel_is_mainmodel = True
>>> pprint(model.find_submodels(include_optional=True))  
{'model.aetmodel': evap_aet_minhas...,
 'model.aetmodel.intercmodel': None,
 'model.aetmodel.petmodel': evap_pet_mlc...,
 'model.aetmodel.petmodel.retmodel': evap_ret_tw2002,
 'model.aetmodel.petmodel.retmodel.radiationmodel': None,
 'model.aetmodel.petmodel.retmodel.tempmodel': None,
 'model.radiationmodel': None,
 'model.soilmodel': None}

Use the include_feedbacks parameter to make such feedback connections transparent:

>>> pprint(model.find_submodels(include_mainmodel=True,
...     include_optional=True, include_feedbacks=True))  
{'model': lland_knauf...,
 'model.aetmodel': evap_aet_minhas...,
 'model.aetmodel.intercmodel': None,
 'model.aetmodel.petmodel': evap_pet_mlc...,
 'model.aetmodel.petmodel.retmodel': evap_ret_tw2002,
 'model.aetmodel.petmodel.retmodel.radiationmodel': None,
 'model.aetmodel.petmodel.retmodel.tempmodel': None,
 'model.aetmodel.soilwatermodel': lland_knauf...,
 'model.radiationmodel': None,
 'model.soilmodel': None}

find_submodels() includes only one reference to shared model instances by default:

>>> model.radiationmodel = prepare_model("meteo_glob_fao56")
>>> model.aetmodel = prepare_model("evap_aet_morsim")
>>> model.aetmodel.radiationmodel = model.radiationmodel
>>> pprint(model.find_submodels(include_optional=True))  
{'model.aetmodel': evap_aet_morsim...,
 'model.aetmodel.intercmodel': None,
 'model.aetmodel.snowalbedomodel': None,
 'model.aetmodel.snowcovermodel': None,
 'model.aetmodel.snowycanopymodel': None,
 'model.aetmodel.soilwatermodel': None,
 'model.aetmodel.tempmodel': None,
 'model.radiationmodel': meteo_glob_fao56,
 'model.soilmodel': None}

Use the repeat_sharedmodels parameter to change this behaviour:

>>> pprint(model.find_submodels(
...     repeat_sharedmodels=True, include_optional=True))  
{'model.aetmodel': evap_aet_morsim...,
 'model.aetmodel.intercmodel': None,
 'model.aetmodel.radiationmodel': meteo_glob_fao56,
 'model.aetmodel.snowalbedomodel': None,
 'model.aetmodel.snowcovermodel': None,
 'model.aetmodel.snowycanopymodel': None,
 'model.aetmodel.soilwatermodel': None,
 'model.aetmodel.tempmodel': None,
 'model.radiationmodel': meteo_glob_fao56,
 'model.soilmodel': None}

All previous examples dealt with scalar submodel references handled by SubmodelProperty. Now we will focus on vectors of submodel references handled by SubmodelsProperty and take sw1d_channel as an example:

>>> channel = prepare_model("sw1d_channel")
>>> channel.parameters.control.nmbsegments(2)

Again, method find_submodels() returns by default an empty dictionary if no submodel is available:

>>> channel.find_submodels()
{}

The include_optional parameter works as shown for the scalar case. But for scalar cases, the names contain an additional suffix to indicate the position of the respective submodel:

>>> pprint(channel.find_submodels(include_optional=True))
{'model.routingmodels_0': None,
 'model.routingmodels_1': None,
 'model.routingmodels_2': None,
 'model.storagemodels_0': None,
 'model.storagemodels_1': None}

We now add some possible submodels to the sw1d_channel main model:

>>> with channel.add_routingmodel_v1("sw1d_q_in", position=0, update=False):
...     pass
>>> with channel.add_storagemodel_v1("sw1d_storage", position=0, update=False):
...     pass
>>> with channel.add_routingmodel_v2("sw1d_lias", position=1, update=False):
...     pass
>>> with channel.add_storagemodel_v1("sw1d_storage", position=1, update=False):
...     pass
>>> with channel.add_routingmodel_v3("sw1d_weir_out", position=2, update=False):
...     pass

Method find_submodels() associates them with the correct positions:

>>> pprint(channel.find_submodels())
{'model.routingmodels_0': sw1d_q_in,
 'model.routingmodels_1': sw1d_lias,
 'model.routingmodels_2': sw1d_weir_out,
 'model.storagemodels_0': sw1d_storage,
 'model.storagemodels_1': sw1d_storage}

One can use the aggregate_vectors parameter to gain a better overview. Then, find_submodels() reports only the names of the respective SubmodelsProperty instances with a suffixed wildcard to distinguish them from SubmodelProperty instances:

>>> channel.find_submodels(aggregate_vectors=True)
{'model.routingmodels_*': None, 'model.storagemodels_*': None}

Another option is to include side models. However, this does not work in combination with including sub-submodels and thus cannot give further insight into the configuration of a sw1d_channel model:

>>> pprint(channel.find_submodels(include_sidemodels=True))
Traceback (most recent call last):
...
ValueError: Including sub-submodels and side-models leads to ambiguous results.

So, one needs to apply it to the respective submodels directly:

>>> pprint(channel.storagemodels[0].find_submodels(
...     include_subsubmodels=False, include_sidemodels=True))
{'model.routingmodelsdownstream_0': sw1d_lias,
 'model.routingmodelsupstream_0': sw1d_q_in}
>>> pprint(channel.routingmodels[1].find_submodels(
...     include_subsubmodels=False, include_sidemodels=True))
{'model.routingmodelsdownstream_0': sw1d_weir_out,
 'model.routingmodelsupstream_0': sw1d_q_in,
 'model.storagemodeldownstream': sw1d_storage,
 'model.storagemodelupstream': sw1d_storage}

When dealing with submodel arrays handled by SubmodelsProperty instances, one might be interested in only querying the first or the last model, which is supported by the position parameter:

>>> pprint(channel.find_submodels(position=0))
{'model.routingmodels_0': sw1d_q_in, 'model.storagemodels_0': sw1d_storage}
>>> pprint(channel.find_submodels(position=-1))
{'model.routingmodels_2': sw1d_weir_out, 'model.storagemodels_1': sw1d_storage}
>>> pprint(channel.find_submodels(position=1))
Traceback (most recent call last):
...
ValueError: The `position` argument requires the integer value `0´ or `-1`, but the value `1` of type `int` is given.
query_submodels(name: ModuleType | str, /) list[Model][source]

Use find_submodels() to query all (sub)models of the given type.

>>> from hydpy import prepare_model
>>> model = prepare_model("lland_knauf")
>>> model.query_submodels("meteo_glob_fao56")
[]
>>> model.radiationmodel = prepare_model("meteo_glob_fao56")
>>> model.query_submodels("meteo_glob_fao56")
[meteo_glob_fao56]
>>> model.aetmodel = prepare_model("evap_aet_morsim")
>>> model.aetmodel.radiationmodel = model.radiationmodel
>>> model.query_submodels("meteo_glob_fao56")
[meteo_glob_fao56]
>>> from hydpy.models import meteo_glob_fao56
>>> model.aetmodel.radiationmodel = prepare_model(meteo_glob_fao56)
>>> model.query_submodels(meteo_glob_fao56)
[meteo_glob_fao56, meteo_glob_fao56]
update_parameters(ignore_errors: bool = False) None[source]

Use the control parameter values of the current model for updating its derived parameters and the control and derived parameters of all its submodels.

We use the combination of hland_96, evap_aet_hbv96, and evap_pet_hbv96 used by the HydPy-H-Lahn project for modelling the Dill catchment as an example:

>>> from hydpy.core.testtools import prepare_full_example_2
>>> hp = prepare_full_example_2()[0]
>>> model = hp.elements.land_dill_assl.model

First, all zones of the Dill catchment are either of type FIELD or FOREST:

>>> model.parameters.control.zonetype
zonetype(FIELD, FOREST, FIELD, FOREST, FIELD, FOREST, FIELD, FOREST,
         FIELD, FOREST, FIELD, FOREST)

Hence, the Soil parameter of evap_aet_hbv96 must be True for the entire basin, as both zone types possess a soil module which requires soil evapotranspiration estimates:

>>> model.aetmodel.parameters.control.soil
soil(True)

Second, hland_96 requires definitions for the zones’ altitude (ZoneZ) and determines the average basin altitude (Z) automatically:

>>> model.parameters.control.zonez
zonez(2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 6.0, 7.0, 7.0)
>>> model.parameters.derived.z
z(4.205345)

evap_aet_hbv96 handles its altitude data similarly but relies on the unit 1 m instead of 100 m:

>>> model.aetmodel.petmodel.parameters.control.hrualtitude
hrualtitude(200.0, 200.0, 300.0, 300.0, 400.0, 400.0, 500.0, 500.0,
            600.0, 600.0, 700.0, 700.0)
>>> model.aetmodel.petmodel.parameters.derived.altitude
altitude(420.53445)

We now set the first zone to type ILAKE and the altitude of all zones to 400 m:

>>> from hydpy.models.hland_96 import ILAKE
>>> model.parameters.control.zonetype[0] = ILAKE
>>> model.parameters.control.zonez(4.0)

update_parameters() uses the appropriate interface methods to transfer the updated control parameter values from the main model to all its submodels. So, parameter Soil parameter of evap_aet_hbv96 becomes aware of the introduced internal lake zone, which does not include a soil module and hence needs no soil evapotranspiration estimates:

>>> model.update_parameters()
>>> model.aetmodel.parameters.control.soil
soil(field=True, forest=True, ilake=False)

Additionally, update_parameters() uses method update() of class Parameters for updating the derived parameters Z of the hland_96 main model and Altitude of the evap_pet_hbv96 submodel:

>>> model.parameters.derived.z
z(4.0)
>>> model.aetmodel.petmodel.parameters.control.hrualtitude
hrualtitude(400.0)
>>> model.aetmodel.petmodel.parameters.derived.altitude
altitude(400.0)
property conditions: dict[str, dict[str, dict[str, float | ndarray[Any, dtype[float64]]]]]

A nested dictionary that contains the values of all condition sequences of a model and its submodels.

See the documentation on property conditions for further information.

property couple_models: ModelCoupler | None

If available, return a function object for coupling models to a composite model suitable at least for the actual model subclass (see method unite_collectives()).

class hydpy.core.modeltools.RunModel[source]

Bases: Model

Base class for AdHocModel and SegmentModel that introduces so-called “run methods”, which need to be executed in the order of their positions in the RUN_METHODS tuple.

METHOD_GROUPS: ClassVar[tuple[str, ...]] = ('RECEIVER_METHODS', 'INLET_METHODS', 'RUN_METHODS', 'ADD_METHODS', 'OUTLET_METHODS', 'SENDER_METHODS')
abstract run() None[source]

Call all methods defined as “run methods” in the defined order.

simulate(idx: int) None[source]

Perform a simulation run over a single simulation time step.

The required argument idx corresponds to property idx_sim (see the main documentation on class Model).

You can integrate method simulate() into your workflows for tailor-made simulation runs. Method simulate() is complete enough to allow for consecutive calls. However, note that it does neither call save_data(), update_receivers(), nor update_senders(). Also, one would have to reset the related node sequences, as done in the following example:

>>> from hydpy.core.testtools import prepare_full_example_2
>>> hp, pub, TestIO = prepare_full_example_2()
>>> model = hp.elements.land_dill_assl.model
>>> for idx in range(4):
...     model.simulate(idx)
...     print(hp.nodes.dill_assl.sequences.sim)
...     hp.nodes.dill_assl.sequences.sim = 0.0
sim(11.757526)
sim(8.865079)
sim(7.101815)
sim(5.994195)
>>> hp.nodes.dill_assl.sequences.sim.series
InfoArray([nan, nan, nan, nan])

The results above are identical to those of method simulate() of class HydPy, which is the standard method to perform simulation runs (except that method simulate() of class HydPy also performs the steps neglected by method simulate() of class Model mentioned above):

>>> from hydpy import round_
>>> hp.reset_conditions()
>>> hp.simulate()
>>> round_(hp.nodes.dill_assl.sequences.sim.series)
11.757526, 8.865079, 7.101815, 5.994195

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

class hydpy.core.modeltools.AdHocModel[source]

Bases: RunModel

Base class for models solving the underlying differential equations in an “ad hoc manner”.

“Ad hoc” stands for the classical approaches in hydrology to calculate individual fluxes separately (often sequentially) and without error control (Clark and Kavetski, 2010).

run() None[source]

Call all methods defined as “run methods” in the defined order.

>>> from hydpy.core.modeltools import AdHocModel, Method
>>> class print_1(Method):
...     @staticmethod
...     def __call__(self):
...         print(1)
>>> class print_2(Method):
...     @staticmethod
...     def __call__(self):
...         print(2)
>>> class Test(AdHocModel):
...     RUN_METHODS = print_1, print_2
>>> Test().run()
1
2

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

class hydpy.core.modeltools.SegmentModel[source]

Bases: RunModel

Base class for (routing) models that solve the underlying differential equations “segment-wise”.

“segment-wise” means that SegmentModel first runs the “run methods” for the first segment (by setting idx_segment to zero), then for the second segment (by setting idx_segment to one), and so on. Therefore, it requires the concrete model subclass to provide a control parameter named “NmbSegments”. Additionally, it requires the concrete model to implement a solver parameter named “NmbRuns” that defines how many times the “run methods” need to be (repeatedly) executed for each segment. See musk_classic and musk_mct as examples.

idx_segment

The segment index.

idx_run

The run index.

nmb_segments: int = 0
run() None[source]

Call all methods defined as “run methods” “segment-wise”.

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

run_segments(method: Method) None[source]

Run the given methods for all segments.

Method run_segments() is mainly thought for testing purposes. See the documentation on method Calc_Discharge_V1 on how to apply it.

class hydpy.core.modeltools.SubstepModel[source]

Bases: RunModel

Base class for (routing) models that solve the underlying differential equations “substep-wise”.

“substep-wise” means method run() repeatedly calls all “run methods” in the usual order within each simulation step until the timeleft attribute is not larger than zero anymore. The concrete model subclass is up to reduce timeleft. This mechanism allows the concrete model to adjust the internal calculation time step depending on its current accuracy and stability requirements.

cymodel: CySubstepModelProtocol | None
property timeleft: float

The time left within the current simulation step [s].

run() None[source]

Call all methods defined as “run methods” repeatedly.

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

class hydpy.core.modeltools.SolverModel[source]

Bases: Model

Base class for hydrological models, which solve ordinary differential equations with numerical integration algorithms.

abstract solve() None[source]

Solve all FULL_ODE_METHODS in parallel.

class hydpy.core.modeltools.NumConstsELS[source]

Bases: object

Configuration options for using the “Explicit Lobatto Sequence” implemented by class ELSModel.

You can change the following solver options at your own risk.

>>> from hydpy.core.modeltools import NumConstsELS
>>> consts = NumConstsELS()

The maximum number of Runge Kutta submethods to be applied (the higher, the better the theoretical accuracy, but also the worse the time spent unsuccessful when the theory does not apply):

>>> consts.nmb_methods
10

The number of entries to handle the stages of the highest order method (must agree with the maximum number of methods):

>>> consts.nmb_stages
11

The maximum increase of the integration step size in case of success:

>>> consts.dt_increase
2.0

The maximum decrease of the integration step size in case of failure:

>>> consts.dt_decrease
10.0

The Runge Kutta coefficients, one matrix for each submethod:

>>> consts.a_coefs.shape
(11, 12, 11)
a_coeffs: ndarray
nmb_methods: int
nmb_stages: int
dt_increase: float
dt_decrease: float
class hydpy.core.modeltools.NumVarsELS[source]

Bases: object

Intermediate results of the “Explicit Lobatto Sequence” implemented by class ELSModel.

Class NumVarsELS should be of relevance for model developers, as it helps to evaluate how efficient newly implemented models are solved (see the documentation on method solve() of class ELSModel as an example).

use_relerror: bool
nmb_calls: int
t0: float
t1: float
dt_est: float
dt: float
idx_method: int
idx_stage: int
abserror: float
relerror: float
last_abserror: float
last_relerror: float
extrapolated_abserror: float
extrapolated_relerror: float
f0_ready: bool
class hydpy.core.modeltools.ELSModel[source]

Bases: SolverModel

Base class for hydrological models using the “Explicit Lobatto Sequence” for solving ordinary differential equations.

The “Explicit Lobatto Sequence” is a variable order Runge Kutta method combining different Lobatto methods. Its main idea is to first calculate a solution with a lower order method, then use these results to apply the next higher-order method, and to compare both results. If they are close enough, the latter results are accepted. If not, the next higher-order method is applied (or, if no higher-order method is available, the step size is decreased, and the algorithm restarts with the method of the lowest order). So far, a thorough description of the algorithm is available in German only (Tyralla, 2016).

Note the strengths and weaknesses of class ELSModel discussed in the documentation on method solve(). Model developers should not derive from class ELSModel when trying to implement models with a high potential for stiff parameterisations. Discontinuities should be regularised, for example, by the “smoothing functions” provided by module smoothtools. Model users should be careful not to define two small smoothing factors, to avoid needlessly long simulation times.

METHOD_GROUPS: ClassVar[tuple[str, ...]] = ('RECEIVER_METHODS', 'INLET_METHODS', 'PART_ODE_METHODS', 'FULL_ODE_METHODS', 'ADD_METHODS', 'OUTLET_METHODS', 'SENDER_METHODS')
numconsts: NumConstsELS
numvars: NumVarsELS
simulate(idx: int) None[source]

Similar to method simulate() of class AdHocModel but calls method solve() instead of run().

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

solve() None[source]

Solve all FULL_ODE_METHODS in parallel.

Implementing numerical integration algorithms that (hopefully) always work well in practice is a tricky task. The following exhaustive examples show how well our “Explicit Lobatto Sequence” algorithm performs for the numerical test models test_stiff0d and test_discontinous. We hope to cover all possible corner cases. Please tell us if you find one we missed.

First, we set the value of parameter K to zero, resulting in no changes at all and thus defining the simplest test case possible:

>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> k(0.0)

Second, we assign values to the solver parameters AbsErrorMax, RelDTMin, and RelDTMax to specify the required numerical accuracy and the smallest and largest internal integration step size allowed:

>>> solver.abserrormax(0.1)
>>> solver.reldtmin(0.001)
>>> solver.reldtmax(1.0)

Additionally, we set RelErrorMax to nan, which disables taking relative errors into account:

>>> solver.relerrormax(nan)

Calling method solve() correctly calculates zero discharge (Q) and thus does not change the water storage (S):

>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(1.0)
>>> fluxes.q
q(0.0)

The achieve the above result, ELSModel requires two function calls, one for the initial guess (using the Explicit Euler Method) and the other one (extending the Explicit Euler method to the Explicit Heun method) to confirm the first guess meets the required accuracy:

>>> model.numvars.idx_method
2
>>> model.numvars.dt
1.0
>>> model.numvars.nmb_calls
2

With moderate changes due to setting the value of parameter K to 0.1, two method calls are still sufficient:

>>> k(0.1)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.905)
>>> fluxes.q
q(0.095)
>>> model.numvars.idx_method
2
>>> model.numvars.nmb_calls
2

Calculating the analytical solution shows ELSModel did not exceed the given tolerance value:

>>> import numpy
>>> from hydpy import round_
>>> round_(numpy.exp(-k))
0.904837

After decreasing the allowed error by one order of magnitude, ELSModel requires four method calls (again, one for the first order and one for the second-order method, and two additional calls for the third-order method):

>>> solver.abserrormax(0.001)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.904833)
>>> fluxes.q
q(0.095167)
>>> model.numvars.idx_method
3
>>> model.numvars.nmb_calls
4

After decreasing AbsErrorMax by ten again, ELSModel needs one further higher-order method, which requires three additional calls, making a sum of seven:

>>> solver.abserrormax(0.0001)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.904837)
>>> fluxes.q
q(0.095163)
>>> model.numvars.idx_method
4
>>> model.numvars.nmb_calls
7

ELSModel achieves even a very extreme numerical precision (just for testing, way beyond hydrological requirements) in one single step but now requires 29 method calls:

>>> solver.abserrormax(1e-12)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.904837)
>>> fluxes.q
q(0.095163)
>>> model.numvars.dt
1.0
>>> model.numvars.idx_method
8
>>> model.numvars.nmb_calls
29

With a more dynamical parameterisation, where the storage decreases by about 40 % per time step, ELSModel needs seven method calls to meet a “normal” error tolerance:

>>> solver.abserrormax(0.01)
>>> k(0.5)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.606771)
>>> fluxes.q
q(0.393229)
>>> model.numvars.idx_method
4
>>> model.numvars.nmb_calls
7
>>> round_(numpy.exp(-k))
0.606531

Being an explicit integration method, the “Explicit Lobatto Sequence” can be inefficient for solving stiff initial value problems. Setting K to 2.0 forces ELSModel to solve the problem in two substeps, requiring a total of 22 method calls:

>>> k(2.0)
>>> round_(numpy.exp(-k))
0.135335
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.134658)
>>> fluxes.q
q(0.865342)
>>> round_(model.numvars.dt)
0.3
>>> model.numvars.nmb_calls
22

Increasing the stiffness of the initial value problem further can increase computation times rapidly:

>>> k(4.0)
>>> round_(numpy.exp(-k))
0.018316
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.019774)
>>> fluxes.q
q(0.980226)
>>> round_(model.numvars.dt)
0.3
>>> model.numvars.nmb_calls
44

If we prevent ELSModel from compensatingf or its problems by disallowing it to reduce its integration step size, it does not achieve satisfying results:

>>> solver.reldtmin(1.0)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.09672)
>>> fluxes.q
q(0.90328)
>>> round_(model.numvars.dt)
1.0
>>> model.numvars.nmb_calls
46

You can restrict the allowed maximum integration step size, which can help to prevent from loosing to much performance due to trying to solve too stiff problems, repeatedly:

>>> solver.reldtmin(0.001)
>>> solver.reldtmax(0.25)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.016806)
>>> fluxes.q
q(0.983194)
>>> round_(model.numvars.dt)
0.25
>>> model.numvars.nmb_calls
33

Alternatively, you can restrict the available number of Lobatto methods. Using two methods only is an inefficient choice for the given initial value problem but at least solves it with the required accuracy:

>>> solver.reldtmax(1.0)
>>> model.numconsts.nmb_methods = 2
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.020284)
>>> fluxes.q
q(0.979716)
>>> round_(model.numvars.dt)
0.156698
>>> model.numvars.nmb_calls
74

In the above examples, we control numerical accuracies based on absolute error estimates only via parameter AbsErrorMax. After assigning an actual value to parameter RelErrorMax, ELSModel also takes relative errors into account. We modify some of the above examples to show how this works.

Generally, it is sufficient to meet one of both criteria. If we repeat the second example with a relaxed absolute but a strict relative tolerance, we reproduce the original result due to our absolute criteria being the relevant one:

>>> solver.abserrormax(0.1)
>>> solver.relerrormax(0.000001)
>>> k(0.1)
>>> states.s(1.0)
>>> model.solve()
>>> states.s
s(0.905)
>>> fluxes.q
q(0.095)

The same holds for the opposite case of a strict absolute but a relaxed relative tolerance:

>>> solver.abserrormax(0.000001)
>>> solver.relerrormax(0.1)
>>> k(0.1)
>>> states.s(1.0)
>>> model.solve()
>>> states.s
s(0.905)
>>> fluxes.q
q(0.095)

Reiterating the “more dynamical parameterisation” example results in slightly different but also correct results:

>>> k(0.5)
>>> states.s(1.0)
>>> model.solve()
>>> states.s
s(0.607196)
>>> fluxes.q
q(0.392804)

Reiterating the stiffest example with a relative instead of an absolute error tolerance of 0.1 achieves higher accuracy, as to be expected due to the value of S being far below 1.0 for some time:

>>> k(4.0)
>>> states.s(1.0)
>>> model.solve()
>>> states.s
s(0.0185)
>>> fluxes.q
q(0.9815)

Besides its weaknesses with stiff problems, ELSModel cannot solve discontinuous problems well. We use the test_stiff0d example model to demonstrate how ELSModel behaves when confronted with such a problem.

>>> from hydpy import reverse_model_wildcard_import
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_discontinous import *
>>> parameterstep()

Everything works fine as long as the discontinuity does not affect the considered simulation step:

>>> k(0.5)
>>> solver.abserrormax(0.01)
>>> solver.reldtmin(0.001)
>>> solver.reldtmax(1.0)
>>> solver.relerrormax(nan)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(0.5)
>>> fluxes.q
q(0.5)
>>> model.numvars.idx_method
2
>>> model.numvars.dt
1.0
>>> model.numvars.nmb_calls
2

The occurrence of a discontinuity within the simulation step often increases computation times more than a stiff parameterisation:

>>> k(2.0)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(-0.006827)
>>> fluxes.q
q(1.006827)
>>> model.numvars.nmb_calls
58
>>> k(2.1)
>>> states.s(1.0)
>>> model.numvars.nmb_calls = 0
>>> model.solve()
>>> states.s
s(-0.00072)
>>> fluxes.q
q(1.00072)
>>> model.numvars.nmb_calls
50

When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version.

calculate_single_terms() None[source]

Apply all methods stored in the PART_ODE_METHODS tuple.

>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> k(0.25)
>>> states.s = 1.0
>>> model.calculate_single_terms()
>>> fluxes.q
q(0.25)
calculate_full_terms() None[source]

Apply all methods stored in the FULL_ODE_METHODS tuple.

>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> k(0.25)
>>> states.s.old = 1.0
>>> fluxes.q = 0.25
>>> model.calculate_full_terms()
>>> states.s.old
1.0
>>> states.s.new
0.75
get_point_states() None[source]

Load the states corresponding to the actual stage.

>>> from hydpy import round_
>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> states.s.old = 2.0
>>> states.s.new = 2.0
>>> model.numvars.idx_stage = 2
>>> points = numpy.asarray(states.fastaccess._s_points)
>>> points[:4] = 0.0, 0.0, 1.0, 0.0
>>> model.get_point_states()
>>> round_(states.s.old)
2.0
>>> round_(states.s.new)
1.0
>>> from hydpy import reverse_model_wildcard_import, print_vector
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> states.sv.old = 3.0, 3.0
>>> states.sv.new = 3.0, 3.0
>>> model.numvars.idx_stage = 2
>>> points = numpy.asarray(states.fastaccess._sv_points)
>>> points[:4, 0] = 0.0, 0.0, 1.0, 0.0
>>> points[:4, 1] = 0.0, 0.0, 2.0, 0.0
>>> model.get_point_states()
>>> print_vector(states.sv.old)
3.0, 3.0
>>> print_vector(states.sv.new)
1.0, 2.0
set_point_states() None[source]

Save the states corresponding to the actual stage.

>>> from hydpy import print_vector
>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> states.s.old = 2.0
>>> states.s.new = 1.0
>>> model.numvars.idx_stage = 2
>>> points = numpy.asarray(states.fastaccess._s_points)
>>> points[:] = 0.
>>> model.set_point_states()
>>> print_vector(points[:4])
0.0, 0.0, 1.0, 0.0
>>> from hydpy import reverse_model_wildcard_import
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> states.sv.old = 3.0, 3.0
>>> states.sv.new = 1.0, 2.0
>>> model.numvars.idx_stage = 2
>>> points = numpy.asarray(states.fastaccess._sv_points)
>>> points[:] = 0.
>>> model.set_point_states()
>>> print_vector(points[:4, 0])
0.0, 0.0, 1.0, 0.0
>>> print_vector(points[:4, 1])
0.0, 0.0, 2.0, 0.0
set_result_states() None[source]

Save the final states of the actual method.

>>> from hydpy import print_vector
>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> states.s.old = 2.0
>>> states.s.new = 1.0
>>> model.numvars.idx_method = 2
>>> results = numpy.asarray(states.fastaccess._s_results)
>>> results[:] = 0.0
>>> model.set_result_states()
>>> print_vector(results[:4])
0.0, 0.0, 1.0, 0.0
>>> from hydpy import reverse_model_wildcard_import
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> states.sv.old = 3.0, 3.0
>>> states.sv.new = 1.0, 2.0
>>> model.numvars.idx_method = 2
>>> results = numpy.asarray(states.fastaccess._sv_results)
>>> results[:] = 0.0
>>> model.set_result_states()
>>> print_vector(results[:4, 0])
0.0, 0.0, 1.0, 0.0
>>> print_vector(results[:4, 1])
0.0, 0.0, 2.0, 0.0
get_sum_fluxes() None[source]

Get the sum of the fluxes calculated so far.

>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> fluxes.q = 0.0
>>> fluxes.fastaccess._q_sum = 1.0
>>> model.get_sum_fluxes()
>>> fluxes.q
q(1.0)
>>> from hydpy import reverse_model_wildcard_import, print_vector
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> fluxes.qv = 0.0, 0.0
>>> numpy.asarray(fluxes.fastaccess._qv_sum)[:] = 1.0, 2.0
>>> model.get_sum_fluxes()
>>> fluxes.qv
qv(1.0, 2.0)
set_point_fluxes() None[source]

Save the fluxes corresponding to the actual stage.

>>> from hydpy import print_vector
>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> fluxes.q = 1.0
>>> model.numvars.idx_stage = 2
>>> points = numpy.asarray(fluxes.fastaccess._q_points)
>>> points[:] = 0.0
>>> model.set_point_fluxes()
>>> print_vector(points[:4])
0.0, 0.0, 1.0, 0.0
>>> from hydpy import reverse_model_wildcard_import
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> fluxes.qv = 1.0, 2.0
>>> model.numvars.idx_stage = 2
>>> points = numpy.asarray(fluxes.fastaccess._qv_points)
>>> points[:] = 0.0
>>> model.set_point_fluxes()
>>> print_vector(points[:4, 0])
0.0, 0.0, 1.0, 0.0
>>> print_vector(points[:4, 1])
0.0, 0.0, 2.0, 0.0
set_result_fluxes() None[source]

Save the final fluxes of the actual method.

>>> from hydpy import print_vector
>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> fluxes.q = 1.0
>>> model.numvars.idx_method = 2
>>> results = numpy.asarray(fluxes.fastaccess._q_results)
>>> results[:] = 0.0
>>> model.set_result_fluxes()
>>> from hydpy import round_
>>> print_vector(results[:4])
0.0, 0.0, 1.0, 0.0
>>> from hydpy import reverse_model_wildcard_import
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> fluxes.qv = 1.0, 2.0
>>> model.numvars.idx_method = 2
>>> results = numpy.asarray(fluxes.fastaccess._qv_results)
>>> results[:] = 0.0
>>> model.set_result_fluxes()
>>> print_vector(results[:4, 0])
0.0, 0.0, 1.0, 0.0
>>> print_vector(results[:4, 1])
0.0, 0.0, 2.0, 0.0
integrate_fluxes() None[source]

Perform a dot multiplication between the fluxes and the A coefficients associated with the different stages of the actual method.

>>> from hydpy import print_vector
>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> model.numvars.idx_method = 2
>>> model.numvars.idx_stage = 1
>>> model.numvars.dt = 0.5
>>> points = numpy.asarray(fluxes.fastaccess._q_points)
>>> points[:4] = 15.0, 2.0, -999.0, 0.0
>>> model.integrate_fluxes()
>>> from hydpy import round_
>>> from hydpy import pub
>>> print_vector(numpy.asarray(model.numconsts.a_coefs)[1, 1, :2])
0.375, 0.125
>>> fluxes.q
q(2.9375)
>>> from hydpy import reverse_model_wildcard_import
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> model.numvars.idx_method = 2
>>> model.numvars.idx_stage = 1
>>> model.numvars.dt = 0.5
>>> points = numpy.asarray(fluxes.fastaccess._qv_points)
>>> points[:4, 0] = 1.0, 1.0, -999.0, 0.0
>>> points[:4, 1] = 15.0, 2.0, -999.0, 0.0
>>> model.integrate_fluxes()
>>> print_vector(numpy.asarray(model.numconsts.a_coefs)[1, 1, :2])
0.375, 0.125
>>> fluxes.qv
qv(0.25, 2.9375)
reset_sum_fluxes() None[source]

Set the sum of the fluxes calculated so far to zero.

>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> fluxes.fastaccess._q_sum = 5.0
>>> model.reset_sum_fluxes()
>>> fluxes.fastaccess._q_sum
0.0
>>> from hydpy import reverse_model_wildcard_import, print_vector
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> import numpy
>>> sums = numpy.asarray(fluxes.fastaccess._qv_sum)
>>> sums[:] = 5.0, 5.0
>>> model.reset_sum_fluxes()
>>> print_vector(fluxes.fastaccess._qv_sum)
0.0, 0.0
addup_fluxes() None[source]

Add up the sum of the fluxes calculated so far.

>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> fluxes.fastaccess._q_sum = 1.0
>>> fluxes.q(2.0)
>>> model.addup_fluxes()
>>> fluxes.fastaccess._q_sum
3.0
>>> from hydpy import reverse_model_wildcard_import, print_vector
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> sums = numpy.asarray(fluxes.fastaccess._qv_sum)
>>> sums[:] = 1.0, 2.0
>>> fluxes.qv(3.0, 4.0)
>>> model.addup_fluxes()
>>> print_vector(sums)
4.0, 6.0
calculate_error() None[source]

Estimate the numerical error based on the relevant fluxes calculated by the current and the last method.

“Relevant fluxes” are those contained within the SOLVERSEQUENCES tuple. If this tuple is empty, method calculate_error() selects all flux sequences of the respective model with a True NUMERIC attribute.

>>> from hydpy import round_
>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> results = numpy.asarray(fluxes.fastaccess._q_results)
>>> results[:5] = 0.0, 0.0, 3.0, 4.0, 4.0
>>> model.numvars.use_relerror = False
>>> model.numvars.idx_method = 3
>>> model.calculate_error()
>>> round_(model.numvars.abserror)
1.0
>>> round_(model.numvars.relerror)
inf
>>> model.numvars.use_relerror = True
>>> model.calculate_error()
>>> round_(model.numvars.abserror)
1.0
>>> round_(model.numvars.relerror)
0.25
>>> model.numvars.idx_method = 4
>>> model.calculate_error()
>>> round_(model.numvars.abserror)
0.0
>>> round_(model.numvars.relerror)
0.0
>>> model.numvars.idx_method = 1
>>> model.calculate_error()
>>> round_(model.numvars.abserror)
0.0
>>> round_(model.numvars.relerror)
inf
>>> from hydpy import reverse_model_wildcard_import
>>> reverse_model_wildcard_import()
>>> from hydpy.models.test_stiff1d import *
>>> parameterstep()
>>> n(2)
>>> model.numvars.use_relerror = True
>>> model.numvars.idx_method = 3
>>> results = numpy.asarray(fluxes.fastaccess._qv_results)
>>> results[:5, 0] = 0.0, 0.0, -4.0, -2.0, -2.0
>>> results[:5, 1] = 0.0, 0.0, -8.0, -4.0, -4.0
>>> model.calculate_error()
>>> round_(model.numvars.abserror)
4.0
>>> round_(model.numvars.relerror)
1.0
>>> model.numvars.idx_method = 4
>>> model.calculate_error()
>>> round_(model.numvars.abserror)
0.0
>>> round_(model.numvars.relerror)
0.0
>>> model.numvars.idx_method = 1
>>> model.calculate_error()
>>> round_(model.numvars.abserror)
0.0
>>> round_(model.numvars.relerror)
inf
extrapolate_error() None[source]

Estimate the numerical error expected when applying all methods available based on the results of the current and the last method.

Note that you cannot apply this extrapolation strategy to the first method. If the current method is the first one, method extrapolate_error() returns -999.9:

>>> from hydpy.models.test_stiff0d import *
>>> parameterstep()
>>> model.numvars.use_relerror = False
>>> model.numvars.abserror = 0.01
>>> model.numvars.last_abserror = 0.1
>>> model.numvars.idx_method = 10
>>> model.extrapolate_error()
>>> from hydpy import round_
>>> round_(model.numvars.extrapolated_abserror)
0.01
>>> model.numvars.extrapolated_relerror
inf
>>> model.numvars.use_relerror = True
>>> model.numvars.relerror = 0.001
>>> model.numvars.last_relerror = 0.01
>>> model.extrapolate_error()
>>> round_(model.numvars.extrapolated_abserror)
0.01
>>> round_(model.numvars.extrapolated_relerror)
0.001
>>> model.numvars.idx_method = 9
>>> model.extrapolate_error()
>>> round_(model.numvars.extrapolated_abserror)
0.001
>>> round_(model.numvars.extrapolated_relerror)
0.0001
>>> model.numvars.relerror = inf
>>> model.extrapolate_error()
>>> round_(model.numvars.extrapolated_relerror)
inf
>>> model.numvars.abserror = 0.0
>>> model.extrapolate_error()
>>> round_(model.numvars.extrapolated_abserror)
0.0
>>> round_(model.numvars.extrapolated_relerror)
0.0
class hydpy.core.modeltools.SubmodelInterface[source]

Bases: Model, ABC

Base class for defining interfaces for submodels.

typeid: ClassVar[int]

Type identifier that we use for differentiating submodels that target the same process group (e.g. infiltration) but follow different interfaces.

For Submodel_V1, typeid is 1, for Submodel_V2 2, and so on.

We prefer using typeid over the standard isinstance() checks in model equations as it allows releasing Python’s Globel Interpreter Lock in Cython.

preparemethod2arguments: dict[str, tuple[tuple[Any, ...], dict[str, Any]]]
static share_configuration(sharable_configuration: SharableConfiguration) Generator[None, None, None][source]

Share class-level configurations between a main model and a submodel temporarily.

The default implementation of method share_configuration() does nothing. Submodels can overwrite it to adjust their classes to the current main model during initialisation.

add_mainmodel_as_subsubmodel(mainmodel: Model) bool[source]

If appropriate, add the given main model as a sub-submodel of the current submodel.

The default implementation of method add_mainmodel_as_subsubmodel() just returns False. Submodels can overwrite it to enable them to query data from their main models actively. If a submodel accepts a main model as a sub-submodel, it must return True; otherwise, False.

class hydpy.core.modeltools.SharableSubmodelInterface[source]

Bases: SubmodelInterface, ABC

Base class for defining interfaces for submodels designed as “sharable”.

Currently, SharableSubmodelInterface implements no functionality. Its sole purpose is to allow model developers to mark a submodel as sharable, meaning multiple main model instances can share the same submodel instance. It is more of a safety mechanism to prevent reusing submodels that are not designed for this purpose.

preparemethod2arguments: dict[str, tuple[tuple[Any, ...], dict[str, Any]]]
typeid: ClassVar[int]

Type identifier that we use for differentiating submodels that target the same process group (e.g. infiltration) but follow different interfaces.

For Submodel_V1, typeid is 1, for Submodel_V2 2, and so on.

We prefer using typeid over the standard isinstance() checks in model equations as it allows releasing Python’s Globel Interpreter Lock in Cython.

cymodel: CyModelProtocol | None
parameters: parametertools.Parameters
sequences: sequencetools.Sequences
masks: masktools.Masks
METHOD_GROUPS: ClassVar[tuple[str, ...]]
REUSABLE_METHODS: ClassVar[tuple[type[ReusableMethod], ...]]
DOCNAME: DocName
class hydpy.core.modeltools.Submodel(model: Model)[source]

Bases: object

Base class for implementing “submodels” that serve to deal with (possibly complicated) general mathematical algorithms (e.g. root-finding algorithms) within hydrological model methods.

You might find class Submodel useful when trying to implement algorithms requiring some interaction with the respective model without any Python overhead. See the modules roottools and rootutils as an example, implementing Python interfaces and Cython implementations of a root-finding algorithms, respectively.

METHODS: ClassVar[tuple[type[Method], ...]]
CYTHONBASECLASS: ClassVar[type[object]]
PYTHONCLASS: ClassVar[type[object]]
name: ClassVar[str]
class hydpy.core.modeltools.CoupleModels(*args, **kwargs)[source]

Bases: Protocol[TypeModel_co]

Specification for defining custom “couple_models” functions to be wrapped by function define_modelcoupler().

hydpy.core.modeltools.define_modelcoupler(inputtypes: tuple[type[TypeModel_contra], ...], outputtype: type[TypeModel_co]) Callable[[CoupleModels[TypeModel_co]], ModelCoupler[TypeModel_co, TypeModel_contra]][source]

Wrap a model-specific function for creating a composite model based given on Node and Element objects and their handled “normal” Model instances.

class hydpy.core.modeltools.ModelCoupler(inputtypes: tuple[type[TypeModel_contra], ...], outputtype: type[TypeModel_co], wrapped: CoupleModels[TypeModel_co])[source]

Bases: Generic[TypeModel_co, TypeModel_contra]

Wrapper that extends the functionality of model-specific functions for coupling “normal” models to composite models.

One benefit of using ModelCoupler over raw “couple_models” is that it alternatively accepts Selection objects instead of Nodes and Elements objects:

>>> from hydpy import Element, Elements, Node, Nodes, prepare_model, Selection
>>> n12 = Node("n12", variable="LongQ")
>>> e1 = Element("e1", outlets=n12)
>>> channel1 = prepare_model("sw1d_channel")
>>> channel1.parameters.control.nmbsegments(1)
>>> with channel1.add_storagemodel_v1("sw1d_storage", position=0, update=False):
...     pass
>>> with channel1.add_routingmodel_v2("sw1d_lias", position=1, update=False):
...     pass
>>> e1.model = channel1
>>> e2 = Element("e2", inlets=n12)
>>> channel2 = prepare_model("sw1d_channel")
>>> channel2.parameters.control.nmbsegments(1)
>>> with channel2.add_storagemodel_v1("sw1d_storage", position=0, update=False):
...     pass
>>> e2.model = channel2
>>> network1 = e1.model.couple_models(nodes=Nodes(n12), elements=Elements(e1, e2))
>>> assert network1.storagemodels[0] is channel1.storagemodels[0]
>>> assert network1.storagemodels[1] is channel2.storagemodels[0]
>>> assert network1.routingmodels[0] is channel1.routingmodels[1]
>>> assert network1.storagemodels[0].routingmodelsdownstream.number == 1
>>> assert network1.storagemodels[1].routingmodelsupstream.number == 1
>>> selection = Selection("test", nodes=n12, elements=[e1, e2])
>>> network2 = e1.model.couple_models(selection=selection)
>>> assert network2.storagemodels[0] is channel1.storagemodels[0]
>>> assert network2.storagemodels[1] is channel2.storagemodels[0]
>>> assert network2.routingmodels[0] is channel1.routingmodels[1]
>>> assert network2.storagemodels[0].routingmodelsdownstream.number == 1
>>> assert network2.storagemodels[1].routingmodelsupstream.number == 1

It additionally checks if the wrapped “couple_models” function supports the types of all passed model instances:

>>> e3 = Element("e3", inlets="n3_in", outlets="n3_out")
>>> e3.model = prepare_model("musk_classic")
>>> e1.model.couple_models(nodes=Nodes(n12), elements=Elements(e1, e2, e3))
Traceback (most recent call last):
...
TypeError: While trying to couple the given model instances to a composite model of type `sw1d_network` based on function `combine_channels`, the following error occurred: `musk_classic` of element `e3` is not among the supported model types: sw1d_channel.