hydpy.core.sequencetools — HydPy 6.2.0 documentation
"""This module implements tools for defining and handling different kinds of hydrological model sequences (time series).""" # import... # ...from standard library from __future__ import annotations import abc import copy import dataclasses import enum import os import sys import types import warnings # ...from site-packages import numpy # ...from HydPy import hydpy from hydpy import config from hydpy.core import devicetools from hydpy.core import exceptiontools from hydpy.core import objecttools from hydpy.core import propertytools from hydpy.core import variabletools from hydpy.core.typingtools import * if TYPE_CHECKING: from hydpy.core import modeltools from hydpy.core import timetools from hydpy.cythons import pointerutils from hydpy.cythons import sequenceutils else: # from hydpy.core import modeltools actual import below from hydpy.cythons.autogen import pointerutils from hydpy.cythons.autogen import sequenceutils TypeSequences = TypeVar("TypeSequences", "Sequences", "devicetools.Node") TypeModelSequences = TypeVar( "TypeModelSequences", bound="ModelSequences[ModelSequence, variabletools.FastAccess]", ) TypeSequence_co = TypeVar("TypeSequence_co", bound="Sequence_", covariant=True) TypeIOSequence_co = TypeVar("TypeIOSequence_co", bound="IOSequence", covariant=True) TypeModelSequence_co = TypeVar( "TypeModelSequence_co", bound="ModelSequence", covariant=True ) TypeModelIOSequence_co = TypeVar( "TypeModelIOSequence_co", bound="ModelIOSequence", covariant=True ) TypeOutputSequence_co = TypeVar( "TypeOutputSequence_co", bound="OutputSequence", covariant=True ) TypeLinkSequence_co = TypeVar( "TypeLinkSequence_co", bound="LinkSequence", covariant=True ) TypeFastAccessIOSequence_co = TypeVar( "TypeFastAccessIOSequence_co", bound="FastAccessIOSequence", covariant=True ) ModelSequencesSubypes: TypeAlias = Union[ "InputSequences", "FactorSequences", "FluxSequences", "StateSequences", "LogSequences", "AideSequences", "InletSequences", "OutletSequences", "ReceiverSequences", "ObserverSequences", "SenderSequences", ] ModelIOSequencesSubtypes: TypeAlias = Union[ "InputSequences", "FactorSequences", "FluxSequences", "StateSequences", "InletSequences", "OutletSequences", "ReceiverSequences", "SenderSequences", "ObserverSequences", ] LinkSequencesSubtypes: TypeAlias = Union[ "InletSequences", "OutletSequences", "ReceiverSequences", "ObserverSequences", "SenderSequences", ] InOutSequence: TypeAlias = Union[ "InputSequence", "InletSequence", "ReceiverSequence", "ObserverSequence", "OutputSequence", "OutletSequence", "SenderSequence", ] InOutSequenceTypes: TypeAlias = Union[ type["InputSequence"], type["InletSequence"], type["ReceiverSequence"], type["ObserverSequence"], type["OutputSequence"], type["OutletSequence"], type["SenderSequence"], ] Aggregation: TypeAlias = Optional[Literal["unmodified", "mean"]][docs] class FastAccessIOSequence(variabletools.FastAccess): """Provides fast access to the values of the |IOSequence| objects of a specific subgroup and supports handling time series data during simulations. The following details are of relevance for *HydPy* developers only. |sequencetools.FastAccessIOSequence| is applied in Python mode only. When working in Cython mode, it is replaced by model-specific Cython extension classes, which are computationally more efficient. For compatibility with these extension classes, |sequencetools.FastAccessIOSequence| objects work with dynamically set instance members. For example, suppose there is a sequence named `seq1`, which is 2-dimensional, then its associated attributes are: * seq1 (|NDArrayFloat|): The actual sequence value(s). * _seq1_ndim (|int|): The number of dimensions. * _seq1_length_0 (|int|): Length in the first dimension. * _seq1_length_1 (|int|): Length in the second dimension. * _seq1_ramflag (|bool|): Handle time series data in RAM? * _seq1_array (|NDArrayFloat|): Time-series data (when handled in RAM). * _seq1_diskflag_reading (|bool|): Read data from a NetCDF file during simulation? * _seq1_diskflag_writing (|bool|): Write data to a NetCDF file during simulation? * _seq1_ncarray (|NDArrayFloat|): An array connected with the data slice of the NetCDF file relevant for `seq1`. Note that the respective |IOSequences| and |IOSequence| objects initialise, change, and apply these dynamical attributes. To handle them directly is error-prone and thus not recommended. """
[docs] def load_data(self, idx: int) -> None: """Load the data of certain sequences from the defined sources. The following flags specify the data source (listed in the order of their priority): * inputflag (|bool|): Take the data from an "input node". * diskflag_reading (|bool|): Read the data "on the fly" from a NetCDF file during a simulation run. * ramflag (|bool|): Take the data from the time series handled by the |IOSequence.series| attribute of the respective |IOSequence| object. If, for example, `diskflag_reading` and `ramflag` are both activated, |FastAccessIOSequence.load_data| prefers the data available within the NetCDF file. """ for name in self: ndim = self._get_attribute(name, "ndim") inputflag = self._get_attribute(name, "inputflag", False) diskflag = self._get_attribute(name, "diskflag_reading") ramflag = self._get_attribute(name, "ramflag") if inputflag or diskflag or ramflag: if inputflag: actual = self._get_attribute(name, "inputpointer")[0] elif diskflag: actual = self._get_attribute(name, "ncarray")[0] else: actual = self._get_attribute(name, "array")[idx] if ndim == 0: setattr(self, name, actual) else: getattr(self, name)[:] = actual
[docs] def save_data(self, idx: int) -> None: """Save the data of certain sequences to the defined sources. The following flags the data targets: * diskflag_writing (|bool|): Write the data "on the fly" to a NetCDF file during a simulation run. * ramflag (|bool|): Give the data to the time series handled by the |IOSequence.series| attribute of the respective |IOSequence| object. It is possible to write data to a NetCDF file and pass it to |IOSequence.series| simultaneously. """ for name in self: actual = getattr(self, name) if self._get_attribute(name, "diskflag_writing"): try: self._get_attribute(name, "ncarray")[:] = actual.flatten() except AttributeError: self._get_attribute(name, "ncarray")[:] = actual if self._get_attribute(name, "ramflag"): self._get_attribute(name, "array")[idx] = actual
[docs] class FastAccessInputSequence(FastAccessIOSequence): """|FastAccessIOSequence| subclass specialised for input sequences."""
[docs] def set_pointerinput(self, name: str, pdouble: pointerutils.PDouble) -> None: """Use the given |PDouble| object as the pointer for the 0-dimensional |InputSequence| object with the given name.""" setattr(self, f"_{name}_inputpointer", pdouble)
[docs] class FastAccessOutputSequence(FastAccessIOSequence): """|FastAccessIOSequence| subclass specialised for output sequences."""
[docs] def set_pointeroutput(self, name: str, pdouble: pointerutils.PDouble) -> None: """Use the given |PDouble| object as the pointer for the 0-dimensional |OutputSequence| object with the given name.""" self._set_attribute(name, "outputpointer", pdouble)
[docs] def update_outputs(self) -> None: """Pass the data of all sequences with an activated output flag.""" for name in self: if self._get_attribute(name, "outputflag", False): self._get_attribute(name, "outputpointer")[0] = getattr(self, name)
[docs] class FastAccessLinkSequence(FastAccessIOSequence): """|FastAccessIOSequence| subclass specialised for link sequences."""
[docs] def alloc_pointer(self, name: str, shape: int | tuple[int, ...]) -> None: """Allocate enough memory for the given vector length of the |LinkSequence| object's pointer with the given name. Cython extension classes must define |FastAccessLinkSequence.alloc_pointer| if the model handles at least one 1-dimensional |LinkSequence| subclass. """ getattr(self, f"_{name}_pointer").shape = shape
[docs] def dealloc_pointer(self, name: str) -> None: """Free the previously allocated memory of the |LinkSequence| object's pointer with the given name. Cython extension classes must define |FastAccessLinkSequence.dealloc_pointer| if the model handles at least one 1-dimensional |LinkSequence| subclass. """
[docs] def set_pointer0d(self, name: str, value: pointerutils.Double) -> None: """Define a pointer referencing the given |Double| object for the 0-dimensional |LinkSequence| object with the given name. Cython extension classes must define |FastAccessLinkSequence.set_pointer0d| if the model handles at least one 0-dimensional |LinkSequence| subclasses. """ setattr(self, f"_{name}_pointer", pointerutils.PDouble(value))
[docs] def set_pointer1d(self, name: str, value: pointerutils.Double, idx: int) -> None: """Define a pointer referencing the given |Double| object for the 1-dimensional |LinkSequence| object with the given name. The given index defines the vector position of the defined pointer. Cython extension classes must define |FastAccessLinkSequence.set_pointer1d| if the model handles at least one 1-dimensional |LinkSequence| subclasses. """ ppdouble: pointerutils.PPDouble = getattr(self, f"_{name}_pointer") ppdouble.set_pointer(value, idx)
[docs] def get_pointervalue(self, name: str) -> float | NDArrayFloat: """Return the actual value(s) referenced by the pointer(s) of the |LinkSequence| object with the given name.""" value = getattr(self, f"_{name}_pointer")[:] if self._get_attribute(name, "ndim"): return numpy.asarray(value, dtype=config.NP_FLOAT) return float(value)
[docs] class FastAccessNodeSequence(FastAccessIOSequence): """|sequencetools.FastAccessIOSequence| subclass specialised for |Node| objects. In contrast to other |FastAccessIOSequence| subclasses, |sequencetools.FastAccessNodeSequence| only needs to handle a fixed number of sequences, |Sim| and |Obs|. It thus can define the related attributes explicitly. """ sim: pointerutils.Double obs: pointerutils.Double _sim_ramflag: bool _obs_ramflag: bool _sim_array: NDArrayFloat _obs_array: NDArrayFloat _sim_diskflag_reading: bool _sim_diskflag_writing: bool _obs_diskflag_reading: bool _obs_diskflag_writing: bool _sim_ncarray: NDArrayFloat _obs_ncarray: NDArrayFloat _reset_obsdata: bool
[docs] def load_simdata(self, idx: int) -> None: """Load the next sim sequence value from a NetCDF file or, with second priority, from the |IOSequence.series| attribute of the current |Sim| object.""" if self._sim_diskflag_reading: self.sim[0] = self._sim_ncarray[0] elif self._sim_ramflag: self.sim[0] = self._sim_array[idx]
[docs] def save_simdata(self, idx: int) -> None: """Save the next sim sequence value to a NetCDF file and/or to the |IOSequence.series| attribute of the |Sim| object.""" if self._sim_diskflag_writing: self._sim_ncarray[0] = self.sim[0] if self._sim_ramflag: self._sim_array[idx] = self.sim[0]
[docs] def load_obsdata(self, idx: int) -> None: """Load the next sim sequence value from a NetCDF file or, with second priority, from the |IOSequence.series| attribute of the |Obs| object.""" if self._obs_diskflag_reading: self.obs[0] = self._obs_ncarray[0] elif self._obs_ramflag: self.obs[0] = self._obs_array[idx]
[docs] def save_obsdata(self, idx: int) -> None: """Save the next sim sequence value to a NetCDF file and/or to the |IOSequence.series| attribute of the |Obs| object.""" if self._obs_diskflag_writing: self._obs_ncarray[0] = self.obs[0] if self._obs_ramflag: self._obs_array[idx] = self.obs[0]
[docs] def load_data(self, idx: int) -> None: """Call both method |sequencetools.FastAccessNodeSequence.load_simdata| and method |sequencetools.FastAccessNodeSequence.load_obsdata|.""" self.load_simdata(idx) self.load_obsdata(idx)
[docs] def save_data(self, idx: int) -> None: """Call both method |sequencetools.FastAccessNodeSequence.save_simdata| and method |sequencetools.FastAccessNodeSequence.save_obsdata|.""" self.save_simdata(idx) self.save_obsdata(idx)
[docs] def reset(self, idx: int = 0) -> None: # pylint: disable=unused-argument # required for consistincy with the other reset methods. """Set the actual value of the simulation sequence to zero.""" self.sim[0] = 0.0
[docs] def fill_obsdata(self, idx: int = 0) -> None: """Use the current sim value for the current `obs` value if obs is |numpy.nan|.""" # pylint: disable=unused-argument # required for consistincy with the other reset methods. if numpy.isnan(self.obs[0]): self._reset_obsdata = True self.obs[0] = self.sim[0]
[docs] def reset_obsdata(self, idx: int = 0) -> None: """Reset the current `obs` value to |numpy.nan| if modified beforehand by method |FastAccessNodeSequence.fill_obsdata|.""" # pylint: disable=unused-argument # required for consistincy with the other reset methods. if self._reset_obsdata: self.obs[0] = numpy.nan self._reset_obsdata = False
[docs] class InfoArray(NDArrayFloat): """|numpy| |numpy.ndarray| subclass with an additional attribute describing the (potential) aggregation of the handled data. >>> from hydpy.core.sequencetools import InfoArray >>> array = InfoArray([1.0, 2.0], aggregation="mean") >>> array InfoArray([1., 2.]) >>> array.aggregation 'mean' >>> subarray = array[:1] >>> subarray InfoArray([1.]) >>> subarray.aggregation 'mean' """ aggregation: Aggregation def __new__(cls, array: NDArrayFloat, aggregation: Aggregation = None) -> InfoArray: obj = numpy.asarray(array).view(cls) obj.aggregation = aggregation return obj def __array_finalize__(self, obj: NDArray | None) -> None: if isinstance(obj, InfoArray): self.aggregation = obj.aggregation else: self.aggregation = None
[docs] class StandardInputNames(enum.StrEnum): """Standard names for the |InputSequence| subclasses of the various models. One can use these names instead of the model-specific sequence names for reading input time series from or to files. For further information, see the introductory documentation on class |HydPy|. The suffix "_HRU" refers to 1-dimensional sequences for which the different entries correspond to different spatial units (typically hydrological response units). """ AIR_TEMPERATURE = "air_temperature" """Air temperature 2 m above the ground [°C].""" ALBEDO_HRU = "albedo_hru" """Surface albedo [-].""" ARTIFICIAL_GROUNDWATER_RECHARGE = "artificial_groundwater_recharge" """Artificial/additional groundwater recharge [mm/T].""" ARTIFICIAL_SURFACE_WATER_SUPPLY = "artificial_surface_water_supply" """Artificial/additional surface water supply [mm/T].""" ATMOSPHERIC_PRESSURE = "atmospheric_pressure" """Atmospheric pressure [hPa].""" CAPILLARY_RISE = "capillary_rise" """Capillary rise [mm/T].""" CLEAR_SKY_SOLAR_RADIATION = "clear_sky_solar_radiation" """Clear sky solar radiation [W/m²].""" EVAPOTRANSPIRATION = "evapotranspiration" """Actual evapotranspiration [mm/T].""" GLOBAL_RADIATION = "global_radiation" """Global radiation [W/m²].""" INTERCEPTED_WATER_HRU = "intercepted_water_hru" """Amount of intercepted water [mm].""" MAXIMUM_AIR_TEMPERATURE = "maximum_air_temperature" """Highest air temperature 2 m above the ground within time interval [°C].""" MINIMUM_AIR_TEMPERATURE = "minimum_air_temperature" """Lowest air temperature 2 m above the ground within time interval [°C].""" NORMAL_AIR_TEMPERATURE = "normal_air_temperature" """Normal air temperature 2 m above the ground [°C].""" NORMAL_EVAPOTRANSPIRATION = "normal_evapotranspiration" """Normal evapotranspiration [mm/T].""" POSSIBLE_SUNSHINE_DURATION = "possible_sunshine_duration" """Possible sunshine duration [h].""" POTENTIAL_EVAPOTRANSPIRATION = "potential_evapotranspiration" """Potential evapotranspiration [mm/T].""" PRECIPITATION = "precipitation" """Precipitation [mm/T].""" RELATIVE_HUMIDITY = "relative_humidity" """Relative humidity [%].""" SNOW_COVER_DEGREE_CANOPY_HRU = "snow_cover_degree_canopy_hru" """Snow cover degree in the canopies of tree-like vegetation [-].""" SNOW_COVER_DEGREE_HRU = "snow_cover_degree_hru" """Snow cover degree [-].""" SOIL_WATER_HRU = "soil_water_hru" """Amount of soil water [mm].""" SUNSHINE_DURATION = "sunshine_duration" """Sunshine duration [h].""" WIND_SPEED = "wind_speed" """Wind speed [m/s]."""
[docs] class Sequences: """Base class for handling all sequences of a specific model. |Sequences| objects handle nine sequence subgroups as attributes such as the `inlets` and the `receivers` subsequences: >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> sequences = hp.elements.land_dill_assl.model.sequences >>> bool(sequences.inlets) False >>> bool(sequences.fluxes) True Iteration makes only the non-empty subgroups available that handle |Sequence_| objects: >>> for subseqs in sequences: ... print(subseqs.name) inputs factors fluxes states aides outlets >>> len(sequences) 6 Keyword access provides a type-safe way to query a subgroup via a string: >>> type(sequences["inputs"]).__name__ 'InputSequences' >>> type(sequences["wrong"]) Traceback (most recent call last): ... TypeError: There is no sequence subgroup named `wrong`. >>> sequences["model"] Traceback (most recent call last): ... TypeError: Attribute `model` is of type `Model`, which is not a subtype of class \ `SubSequences`. Class |Sequences| provides some methods related to reading and writing time series data, which (directly or indirectly) call the corresponding methods of the handled |IOSequence| objects. In most cases, users should prefer to use the related methods of class |HydPy|, but using the ones of class |Sequences| can be more convenient when analysing a specific model in-depth. To introduce these methods, we first change two IO-related settings: >>> from hydpy import round_ >>> pub.options.checkseries = False >>> pub.sequencemanager.overwrite = True Method |Sequences.prepare_series| can both enable and disable the handling of time series in rapid access memory (RAM), and both enable and disable the reading of input data from NetCDF files and the writing of NetCDF files "on the fly" during simulation runs: >>> from hydpy import attrready >>> sequences.prepare_series(allocate_ram=False, jit=False) >>> sequences.inputs.t.ramflag False >>> attrready(sequences.inputs.t, "series") False >>> sequences.inputs.t.diskflag False >>> sequences.inputs.t.diskflag_reading False >>> sequences.states.sm.diskflag_writing False >>> sequences.prepare_series() >>> sequences.inputs.t.ramflag True >>> attrready(sequences.inputs.t, "series") True >>> sequences.inputs.t.diskflag False >>> sequences.inputs.t.diskflag_reading False >>> sequences.states.sm.diskflag_writing False >>> sequences.prepare_series(allocate_ram=False, jit=True) >>> sequences.inputs.t.ramflag False >>> attrready(sequences.inputs.t, "series") False >>> sequences.inputs.t.diskflag True >>> sequences.inputs.t.diskflag_reading True >>> sequences.states.sm.diskflag_writing True After applying |Sequences.prepare_series|, you can use the methods |Sequences.load_series| and |Sequences.save_series| to read or write the time series of the relevant |InputSequence|, |FactorSequence|, |FluxSequence|, |StateSequence|, and |LinkSequence| objects, as the following technical test suggests. The documentation on class |IOSequence| explains the underlying functionalities of in more detail. >>> from unittest.mock import patch >>> template = "hydpy.core.sequencetools.%s.load_series" >>> with ( ... patch(template % "InputSequences") as inputs, ... patch(template % "FactorSequences") as factors, ... patch(template % "FluxSequences") as fluxes, ... patch(template % "StateSequences") as states, ... patch(template % "OutletSequences") as outlets, ... ): ... sequences.load_series() ... inputs.assert_called_with() ... factors.assert_called_with() ... fluxes.assert_called_with() ... states.assert_called_with() ... outlets.assert_called_with() >>> template = "hydpy.core.sequencetools.%s.save_series" >>> with ( ... patch(template % "InputSequences") as inputs, ... patch(template % "FactorSequences") as factors, ... patch(template % "FluxSequences") as fluxes, ... patch(template % "StateSequences") as states, ... patch(template % "OutletSequences") as outlets, ... ): ... sequences.save_series() ... inputs.assert_called_with() ... factors.assert_called_with() ... fluxes.assert_called_with() ... states.assert_called_with() ... outlets.assert_called_with() .. testsetup:: >>> from hydpy import Node, Element >>> Node.clear_all() >>> Element.clear_all() >>> pub.options.checkseries = True >>> pub.sequencemanager.overwrite = False """ model: modeltools.Model inlets: InletSequences observers: ObserverSequences receivers: ReceiverSequences inputs: InputSequences factors: FactorSequences fluxes: FluxSequences states: StateSequences logs: LogSequences aides: AideSequences outlets: OutletSequences senders: SenderSequences def __init__( self, model: modeltools.Model, *, cls_inlets: type[InletSequences] | None = None, cls_observers: type[ObserverSequences] | None = None, cls_receivers: type[ReceiverSequences] | None = None, cls_inputs: type[InputSequences] | None = None, cls_factors: type[FactorSequences] | None = None, cls_fluxes: type[FluxSequences] | None = None, cls_states: type[StateSequences] | None = None, cls_logs: type[LogSequences] | None = None, cls_aides: type[AideSequences] | None = None, cls_outlets: type[OutletSequences] | None = None, cls_senders: type[SenderSequences] | None = None, cymodel: CyModelProtocol | None = None, cythonmodule: types.ModuleType | None = None, ) -> None: self.model = model self.inlets = self.__prepare_subseqs( InletSequences, cls_inlets, cymodel, cythonmodule ) self.observers = self.__prepare_subseqs( ObserverSequences, cls_observers, cymodel, cythonmodule ) self.receivers = self.__prepare_subseqs( ReceiverSequences, cls_receivers, cymodel, cythonmodule ) self.inputs = self.__prepare_subseqs( InputSequences, cls_inputs, cymodel, cythonmodule ) self.factors = self.__prepare_subseqs( FactorSequences, cls_factors, cymodel, cythonmodule ) self.fluxes = self.__prepare_subseqs( FluxSequences, cls_fluxes, cymodel, cythonmodule ) self.states = self.__prepare_subseqs( StateSequences, cls_states, cymodel, cythonmodule ) self.logs = self.__prepare_subseqs( LogSequences, cls_logs, cymodel, cythonmodule ) self.aides = self.__prepare_subseqs( AideSequences, cls_aides, cymodel, cythonmodule ) self.outlets = self.__prepare_subseqs( OutletSequences, cls_outlets, cymodel, cythonmodule ) self.senders = self.__prepare_subseqs( SenderSequences, cls_senders, cymodel, cythonmodule ) def __prepare_subseqs( self, default: type[TypeModelSequences], class_: type[TypeModelSequences] | None, cymodel, cythonmodule, ) -> TypeModelSequences: name = default.__name__ if class_ is None: class_ = copy.copy(default) setattr(class_, "CLASSES", ()) return class_(self, getattr(cythonmodule, name, None), cymodel) @property def iosubsequences(self) -> Iterator[ModelIOSequencesSubtypes]: """Yield all relevant |IOSequences| objects handled by the current |Sequences| object. For |hland_96|, the available IO-subgroups are `inputs`, `factors`, `fluxes`, `states`, and `outlets`: >>> from hydpy import prepare_model >>> model = prepare_model("hland_96") >>> for subseqs in model.sequences.iosubsequences: ... print(subseqs.name) inputs factors fluxes states outlets Not all models implement sequences for all possible subgroups. The |Sequences.iosubsequences| property only yields those subgroups which are non-empty. """ if self.inputs: yield self.inputs if self.factors: yield self.factors if self.fluxes: yield self.fluxes if self.states: yield self.states yield from self.linksubsequences @property def linksubsequences(self) -> Iterator[LinkSequencesSubtypes]: """Yield all relevant |LinkSequences| objects handled by the current |Sequences| object. For |musk_classic|, the available link sequence subgroups are `inlets` and `outlets`: >>> from hydpy import prepare_model >>> model = prepare_model("musk_classic") >>> for subseqs in model.sequences.linksubsequences: ... print(subseqs.name) inlets outlets Not all models implement sequences for all possible subgroups. Therefore, the |Sequences.linksubsequences| property only yields those subgroups which are non-empty. """ if self.inlets: yield self.inlets if self.outlets: yield self.outlets if self.observers: yield self.observers if self.receivers: yield self.receivers if self.senders: yield self.senders
[docs] def prepare_series(self, allocate_ram: bool = True, jit: bool = False) -> None: """Call method |IOSequences.prepare_series| of attribute |Sequences.inputs| with `read_jit=jit` and of attributes |Sequences.factors|, |Sequences.fluxes|, and |Sequences.states| with `write_jit=jit`.""" self.inputs.prepare_series(allocate_ram=allocate_ram, read_jit=jit) self.factors.prepare_series(allocate_ram=allocate_ram, write_jit=jit) self.fluxes.prepare_series(allocate_ram=allocate_ram, write_jit=jit) self.states.prepare_series(allocate_ram=allocate_ram, write_jit=jit)
[docs] def load_series(self): """Call method |IOSequences.load_series| of all handled |IOSequences| objects.""" for subseqs in self.iosubsequences: subseqs.load_series()
[docs] def save_series(self): """Call method |IOSequence.save_series| of all handled |IOSequences| objects.""" for subseqs in self.iosubsequences: subseqs.save_series()
[docs] def load_data(self, idx: int) -> None: """Call method |ModelIOSequences.load_data| of the handled |sequencetools.InputSequences|, |sequencetools.InletSequences|, |sequencetools.ObserverSequences|, and |sequencetools.ReceiverSequences|. objects.""" self.inputs.load_data(idx) self.inlets.load_data(idx) self.observers.load_data(idx) self.receivers.load_data(idx)
[docs] def save_data(self, idx: int) -> None: """Call method |ModelIOSequences.save_data| of all handled |sequencetools.ModelIOSequences| objects.""" for subseqs in self.iosubsequences: subseqs.save_data(idx)
[docs] def update_outputs(self) -> None: """Call the method |OutputSequences.update_outputs| of the subattributes |Sequences.factors|, |Sequences.fluxes|, and |Sequences.states|. When working in Cython mode, the standard model import overrides this generic Python version with a model-specific Cython version. """ self.factors.update_outputs() self.fluxes.update_outputs() self.states.update_outputs()
[docs] def reset(self) -> None: """Call method |ConditionSequence.reset| of all handled |ConditionSequence| objects.""" self.states.reset() self.logs.reset()
@property def conditionsequences(self) -> Iterator[ConditionSequence]: """Generator object yielding all conditions (|StateSequence| and |LogSequence| objects). """ yield from self.states yield from self.logs @property def conditions(self) -> ConditionsSubmodel: """A nested dictionary that contains the values of all condition sequences of a single model instance. See the documentation on property |HydPy.conditions| for further information. """ conditions: dict[str, dict[str, float | NDArrayFloat]] = {} for seq in self.conditionsequences: subconditions = conditions.get(seq.subseqs.name, {}) subconditions[seq.name] = copy.deepcopy(seq.values) conditions[seq.subseqs.name] = subconditions return conditions @conditions.setter def conditions(self, conditions: ConditionsSubmodel) -> None: with hydpy.pub.options.trimvariables(False): for subname, subconditions in conditions.items(): subseqs = getattr(self, subname) for seqname, values in subconditions.items(): getattr(subseqs, seqname)(values) for seq in reversed(tuple(self.conditionsequences)): seq.trim()
[docs] def trim_conditions(self) -> None: """Call method |trim| of each handled |ConditionSequence|. |Sequences.trim_conditions| is just a convenience function for calling method |trim| of all |StateSequence| and |LogSequence| objects returned by property |Sequences.conditionsequences|. We demonstrate its functionality by preparing an instance of application model |lland_dd|, using its available default values, and defining out-of-bound values of the soil moisture state sequence |lland_states.BoWa|: >>> from hydpy import prepare_model, pub >>> pub.timegrids = "2000-01-01", "2000-01-10", "1d" >>> with pub.options.usedefaultvalues(True): ... model = prepare_model("lland_dd") ... model.parameters.control.nhru(2) >>> model.sequences.states.bowa = -100.0 >>> model.sequences.trim_conditions() >>> model.sequences.states.bowa bowa(0.0, 0.0) """ for seq in self.conditionsequences: seq.trim()
def __getitem__( self, item: str ) -> SubSequences[TypeSequences, Sequence_, variabletools.FastAccess]: try: subseqs = getattr(self, item) except AttributeError: raise TypeError(f"There is no sequence subgroup named `{item}`.") from None if isinstance(subseqs, SubSequences): return subseqs raise TypeError( f"Attribute `{item}` is of type `{type(subseqs).__name__}`, which is not " f"a subtype of class `SubSequences`." ) def __iter__(self) -> Iterator[ModelSequencesSubypes]: if self.inlets: yield self.inlets if self.observers: yield self.observers if self.receivers: yield self.receivers if self.inputs: yield self.inputs if self.factors: yield self.factors if self.fluxes: yield self.fluxes if self.states: yield self.states if self.logs: yield self.logs if self.aides: yield self.aides if self.outlets: yield self.outlets if self.senders: yield self.senders def __len__(self) -> int: return sum(1 for _ in self) def __bool__(self) -> bool: return any(seqs for seqs in self)
[docs] class SubSequences( variabletools.SubVariables[ TypeSequences, TypeSequence_co, variabletools.TypeFastAccess_co ] ): """Base class for handling subgroups of sequences. Each |SubSequences| object has a `fastaccess` attribute, which is an instance of (a subclass of) class |FastAccess| when working in pure Python mode: >>> from hydpy import classname, Node, prepare_model, pub >>> with pub.options.usecython(False): ... model = prepare_model("lland_dd") >>> classname(model.sequences.logs.fastaccess) 'FastAccess' >>> classname(model.sequences.inputs.fastaccess) 'FastAccessInputSequence' >>> from hydpy.core.sequencetools import FastAccessNodeSequence >>> with pub.options.usecython(False): ... node = Node("test1") >>> isinstance(node.sequences.fastaccess, FastAccessNodeSequence) True When working in Cython mode (the default and much faster than the pure Python mode), `fastaccess` is an object of the Cython extension class `FastAccessNodeSequence` of module `sequenceutils` or a Cython extension class specialised for the respective model and sequence group: >>> with pub.options.usecython(True): ... model = prepare_model("lland_dd") >>> classname(model.sequences.inputs.fastaccess) 'InputSequences' >>> from hydpy.cythons.sequenceutils import FastAccessNodeSequence >>> with pub.options.usecython(True): ... node = Node("test2") >>> isinstance(Node("test2").sequences.fastaccess, FastAccessNodeSequence) True See the documentation of similar class |SubParameters| for further information. However, note the difference that model developers should not subclass |SubSequences| directly but specialised subclasses like |sequencetools.FluxSequences| or |sequencetools.StateSequences| instead. .. testsetup:: >>> Node.clear_all() """ @property def name(self) -> str: """The class name in lowercase letters omitting the last eight characters ("equences"). >>> from hydpy.core.sequencetools import StateSequences >>> class StateSequences(StateSequences): ... CLASSES = () >>> StateSequences(None).name 'states' """ return type(self).__name__[:-8].lower()
[docs] class ModelSequences( SubSequences[Sequences, TypeModelSequence_co, variabletools.TypeFastAccess_co] ): """Base class for handling model-related subgroups of sequences.""" seqs: Sequences _cymodel: CyModelProtocol | None def __init__( self, master: Sequences, cls_fastaccess: type[variabletools.TypeFastAccess_co] | None = None, cymodel: CyModelProtocol | None = None, ) -> None: self.seqs = master self._cymodel = cymodel super().__init__(master=master, cls_fastaccess=cls_fastaccess) def _init_fastaccess(self) -> None: super()._init_fastaccess() if self._cls_fastaccess and self._cymodel: setattr(self._cymodel.sequences, self.name, self.fastaccess)
[docs] @dataclasses.dataclass class SeriesMode: """The type of property |IOSequence.seriesmode| of class |IOSequence|.""" ramflag: bool """Corresponds to property |IOSequence.ramflag| of class |IOSequence|.""" diskflag_reading: bool """Corresponds to property |IOSequence.diskflag_reading| of class |IOSequence|.""" diskflag_writing: bool """Corresponds to property |IOSequence.diskflag_writing| of class |IOSequence|."""
[docs] class IOSequences( SubSequences[TypeSequences, TypeIOSequence_co, TypeFastAccessIOSequence_co] ): """Subclass of |SubSequences|, specialised for handling |IOSequence| objects.""" seqs: Sequences
[docs] def prepare_series( self, allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False ) -> None: """Call method |IOSequence.prepare_series| of all handled |IOSequence| objects.""" for seq in self: seq.prepare_series( allocate_ram=allocate_ram, read_jit=read_jit, write_jit=write_jit )
[docs] def load_series(self) -> None: """Call method |IOSequence.load_series| of all handled |IOSequence| objects with an activated |IOSequence.ramflag|.""" for seq in self: if seq.ramflag: seq.load_series()
[docs] def save_series(self) -> None: """Call method |IOSequence.save_series| of all handled |IOSequence| objects with an activated |IOSequence.ramflag|.""" for seq in self: if seq.ramflag: seq.save_series()
[docs] class ModelIOSequences( IOSequences[Sequences, TypeModelIOSequence_co, TypeFastAccessIOSequence_co], ModelSequences[TypeModelIOSequence_co, TypeFastAccessIOSequence_co], ): """Base class for handling model-related subgroups of |IOSequence| objects."""
[docs] def load_data(self, idx: int) -> None: """Call method |FastAccessIOSequence.load_data| of the |FastAccessIOSequence| object handled as attribute `fastaccess`.""" self.fastaccess.load_data(idx)
[docs] def save_data(self, idx: int) -> None: """Call method |FastAccessIOSequence.save_data| of the |FastAccessIOSequence| object handled as attribute `fastaccess`.""" self.fastaccess.save_data(idx)
[docs] class InputSequences(ModelIOSequences["InputSequence", FastAccessInputSequence]): """Base class for handling |InputSequence| objects.""" _CLS_FASTACCESS_PYTHON = FastAccessInputSequence
[docs] class OutputSequences( ModelIOSequences[TypeOutputSequence_co, FastAccessOutputSequence] ): """Base class for handling |OutputSequence| objects.""" _CLS_FASTACCESS_PYTHON = FastAccessOutputSequence
[docs] def update_outputs(self) -> None: """Call method |FastAccessOutputSequence.update_outputs| of the |FastAccessOutputSequence| object handled as attribute `fastaccess`.""" if self: self.fastaccess.update_outputs()
@property def numericsequences(self) -> Iterator[TypeOutputSequence_co]: """Iterator for "numerical" sequences. "numerical" means that the |Sequence_.NUMERIC| class attribute of the actual sequence is |True|: >>> from hydpy import prepare_model >>> model = prepare_model("dam_v001") >>> len(model.sequences.fluxes) 16 >>> for seq in model.sequences.fluxes.numericsequences: ... print(seq) adjustedprecipitation(nan) actualevaporation(nan) inflow(nan) actualrelease(nan) flooddischarge(nan) outflow(nan) """ for flux in self: if flux.NUMERIC: yield flux
[docs] class FactorSequences(OutputSequences["FactorSequence"]): """Base class for handling |FactorSequence| objects."""
[docs] class FluxSequences(OutputSequences["FluxSequence"]): """Base class for handling |FluxSequence| objects.""" @property def name(self) -> str: """Always return the string "fluxes".""" return "fluxes"
[docs] class StateSequences(OutputSequences["StateSequence"]): """Base class for handling |StateSequence| objects.""" fastaccess_new: FastAccessOutputSequence fastaccess_old: variabletools.FastAccess def _init_fastaccess(self) -> None: super()._init_fastaccess() self.fastaccess_new = self.fastaccess if (self._cls_fastaccess is None) or (self._cymodel is None): self.fastaccess_old = variabletools.FastAccess() else: setattr(self._cymodel.sequences, "new_states", self.fastaccess) self.fastaccess_old = self._cls_fastaccess() setattr(self._cymodel.sequences, "old_states", self.fastaccess_old)
[docs] def new2old(self) -> None: """Call method |StateSequence.new2old| of all handled |StateSequence| objects.""" for seq in self: seq.new2old()
[docs] def reset(self) -> None: """Call method |ConditionSequence.reset| of all handled |StateSequence| objects.""" for seq in self: seq.reset()
[docs] class LogSequences(ModelSequences["LogSequence", variabletools.FastAccess]): """Base class for handling |LogSequence| objects.""" _CLS_FASTACCESS_PYTHON = variabletools.FastAccess
[docs] def reset(self) -> None: """Call method |ConditionSequence.reset| of all handled |LogSequence| objects.""" for seq in self: seq.reset()
[docs] class AideSequences(ModelSequences["AideSequence", variabletools.FastAccess]): """Base class for handling |AideSequence| objects.""" _CLS_FASTACCESS_PYTHON = variabletools.FastAccess
[docs] class LinkSequences(ModelIOSequences[TypeLinkSequence_co, FastAccessLinkSequence]): """Base class for handling |LinkSequence| objects.""" _CLS_FASTACCESS_PYTHON = FastAccessLinkSequence
[docs] class InletSequences(LinkSequences["InletSequence"]): """Base class for handling "inlet" |LinkSequence| objects."""
[docs] class OutletSequences(LinkSequences["OutletSequence"]): """Base class for handling "outlet" |LinkSequence| objects."""
[docs] class ObserverSequences(LinkSequences["ObserverSequence"]): """Base class for handling "observer" |LinkSequence| objects."""
[docs] class ReceiverSequences(LinkSequences["ReceiverSequence"]): """Base class for handling "receiver" |LinkSequence| objects."""
[docs] class SenderSequences(LinkSequences["SenderSequence"]): """Base class for handling "sender" |LinkSequence| objects."""
[docs] class Sequence_(variabletools.Variable): """Base class for defining different kinds of sequences. Note that model developers should not derive their model-specific sequence classes from |Sequence_| directly but from the "final" subclasses provided in module |sequencetools| (e.g. |FluxSequence|). From the model developer perspective and especially from the user perspective, |Sequence_| is only a small extension of its base class |Variable|. One relevant extension is that (only the) 0-dimensional sequence objects come with a predefined shape: >>> from hydpy import prepare_model >>> model = prepare_model("lland_dd") >>> model.sequences.fluxes.qa.shape () >>> nkor = model.sequences.fluxes.nkor >>> nkor.shape Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: Shape information for variable \ `nkor` can only be retrieved after it has been defined. For consistency with the usage of |Parameter| subclasses, |Sequence_| objects are also "callable" for setting their values (but in a much less and flexible manner): >>> nkor.shape = 3 >>> nkor(2.0) >>> nkor nkor(2.0, 2.0, 2.0) Under the hood, class |Sequence_| also prepares some attributes of its |FastAccess| object, used for performing the actual simulation calculations. Framework developers should note that the respective `fastaccess` attributes contain both the name of the sequence and the name of the original attribute in lowercase letters. We take `NDIM` as an example: >>> nkor.fastaccess._nkor_ndim 1 Some of these attributes require updating in some situations. For example, other sequences than |AideSequence| objects require a "length" attribute, which needs updating each time the sequence's shape changes: >>> nkor.fastaccess._nkor_length 3 """ TYPE: type[float] = float INIT: float = 0.0 NUMERIC: bool subvars: ( SubSequences[Sequences, Sequence_, variabletools.FastAccess] | SubSequences[devicetools.Node, Sequence_, variabletools.FastAccess] ) """The subgroup to which the sequence belongs.""" subseqs: ( SubSequences[Sequences, Sequence_, variabletools.FastAccess] | SubSequences[devicetools.Node, Sequence_, variabletools.FastAccess] ) """Alias for |Sequence_.subvars|.""" strict_valuehandling: bool = False def __hydpy__connect_variable2subgroup__(self) -> None: super().__hydpy__connect_variable2subgroup__() self.__hydpy__set_fastaccessattribute__("ndim", self.NDIM) self.__hydpy__set_fastaccessattribute__("length", 0) for idx in range(self.NDIM): self.__hydpy__set_fastaccessattribute__(f"length_{idx}", 0) def __hydpy__get_fastaccessattribute__( self, suffix: str, default: object = None ) -> Any: return getattr(self.fastaccess, f"_{self.name}_{suffix}", default) def __hydpy__set_fastaccessattribute__(self, suffix: str, value: Any) -> None: setattr(self.fastaccess, f"_{self.name}_{suffix}", value) def _finalise_connections(self) -> None: """A hook method, called at the end of method `__hydpy__connect_variable2subgroup__` for initialising values and some attributes.""" if not self.NDIM: self.shape = () @property def initinfo(self) -> tuple[float | pointerutils.Double, bool]: """A |tuple| containing the initial value and |True| or a missing value and |False|, depending on the actual |Sequence_| subclass and the actual value of option |Options.usedefaultvalues|. In the following, we do not explain property |Sequence_.initinfo| itself but show how it affects initialising new |Sequence_| objects. Therefore, let us define a sequence test class and prepare a function for initialising it and connecting the resulting instance to a |ModelSequences| object: >>> from hydpy.core.sequencetools import Sequence_, ModelSequences >>> from hydpy.core.variabletools import FastAccess >>> class Test(Sequence_): ... NDIM = 0 ... _CLS_FASTACCESS_PYTHON = FastAccess >>> class SubGroup(ModelSequences): ... CLASSES = (Test,) ... _CLS_FASTACCESS_PYTHON = FastAccess >>> def prepare(): ... subseqs = SubGroup(None) ... test = Test(subseqs) ... test.__hydpy__connect_variable2subgroup__() ... return test By default, making use of the `INIT` attribute is disabled: >>> prepare() test(nan) Enable it by setting |Options.usedefaultvalues| to |True|: >>> from hydpy import pub >>> with pub.options.usedefaultvalues(True): ... prepare() test(0.0) Attribute `INIT` of class |Sequence_| comes with the value `0.0` by default, which should be reasonable for most |Sequence_| subclasses. However, subclasses can define other values. Most importantly, note the possibility to set `INIT` to `None` for sequences that do not allow specifying a reasonabe initial value for all possible situations: >>> Test.INIT = None >>> prepare() test(nan) >>> with pub.options.usedefaultvalues(True): ... prepare() test(nan) """ if hydpy.pub.options.usedefaultvalues and self.INIT is not None: return self.INIT, True return numpy.nan, False def __repr__(self) -> str: brackets = (self.NDIM == 2) and (self.shape[0] != 1) return variabletools.to_repr(self, self.value, brackets)
[docs] class IOSequence(Sequence_): """Base class for sequences with input/output functionalities. The documentation on modules |filetools| and |netcdftools| in some detail explains how to read and write time series files. However, due to efficiency, reading and writing time series files are disabled by default. Therefore, you must first prepare the |IOSequence.series| attribute of the relevant |IOSequence| objects. Typically, you call methods like |HydPy.prepare_inputseries| of class |HydPy|. Here, we instead use the related features of the |IOSequence| class itself. We use the `HydPy-H-Lahn` example project and focus on the `input`, `factor`, `fluxes`, and `state` sequences: >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> inputs = hp.elements.land_lahn_marb.model.sequences.inputs >>> factors = hp.elements.land_lahn_marb.model.sequences.factors >>> fluxes = hp.elements.land_lahn_marb.model.sequences.fluxes >>> states = hp.elements.land_lahn_marb.model.sequences.states Each |IOSequence| object comes four flags, answering the following questions: * |IOSequence.ramflag|: can its time series can be available in RAM? * |IOSequence.diskflag_reading|: read its values "on the fly" from a NetCDF file during simulation runs? * |IOSequence.diskflag_writing|:write its values "on the fly" to a NetCDF file during simulation runs? * |IOSequence.diskflag|: is |IOSequence.diskflag_reading| and/or |IOSequence.diskflag_writing| activated? For input sequences as |hland_inputs.T|, it is common to store their time series data (required for any simulation run) in RAM, which is much faster than (repeatedly) reading data "on the fly" and should be preferred, as long as limited available RAM is not an issue. For convenience, function |prepare_full_example_2| prepared |hland_inputs.T| (and the other input sequences) accordingly: >>> inputs.t.ramflag True >>> inputs.t.diskflag_reading False >>> inputs.t.diskflag_writing False >>> inputs.t.diskflag False >>> from hydpy import round_ >>> round_(inputs.t.series) -0.7, -1.5, -4.6, -8.2 |prepare_full_example_2| also activated the |IOSequence.ramflag| of all factor, flux, and state sequences, which is unnecessary to perform a successful simulation. However, it is required to directly access the complete time series of simulated values afterwards (otherwise, only the last computed value(s) were available in RAM after a simulation run): >>> factors.tc.ramflag True >>> factors.tc.diskflag False >>> round_(factors.tc.series[:, 0]) nan, nan, nan, nan Use |IOSequence.prepare_series| to force a sequence to handle time series data in RAM or to read or write it on the fly. We now activate the reading functionality of input sequence |hland_inputs.T| (while still keeping its time series in RAM, which we set to zero beforehand) and the writing feature of the factor sequences |hland_factors.ContriArea| and |hland_factors.TC| (without handling their data in RAM) and the writing feature of the state sequences |hland_states.SM| and |hland_states.SP| (while handling their data in RAM simultaneously): >>> inputs.t.series = 0.0 >>> inputs.t.prepare_series(allocate_ram=True, read_jit=True) >>> factors.contriarea.prepare_series(allocate_ram=False, write_jit=True) >>> factors.tc.prepare_series(allocate_ram=False, write_jit=True) >>> states.sm.prepare_series(allocate_ram=True, write_jit=True) >>> states.sp.prepare_series(allocate_ram=True, write_jit=True) Use the properties |IOSequence.ramflag|, |IOSequence.diskflag_reading|, |IOSequence.diskflag_writing|, and |IOSequence.diskflag| for querying the current configuration of individual |IOSequence| objects: >>> inputs.t.ramflag True >>> inputs.t.diskflag_reading True >>> inputs.t.diskflag_writing False >>> inputs.t.diskflag True >>> round_(inputs.t.series) 0.0, 0.0, 0.0, 0.0 >>> factors.contriarea.ramflag False >>> factors.contriarea.diskflag_reading False >>> factors.contriarea.diskflag_writing True >>> factors.contriarea.diskflag True >>> factors.contriarea.series Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: Sequence `contriarea` of element \ `land_lahn_marb` is not requested to make any time series data available. >>> states.sm.ramflag True >>> states.sm.diskflag_reading False >>> states.sm.diskflag_writing True >>> states.sm.diskflag True >>> round_(states.sm.series[:, 0]) nan, nan, nan, nan Now we perform a simulation run. Note that we need to change the current working directory to the `iotesting` directory temporarily (by using class |TestIO|) because the relevant NetCDF files are now read and written on the fly: >>> with TestIO(), pub.options.threads(0): ... hp.simulate() After the simulation run, the read (|hland_inputs.T|) and calculated (|hland_states.SM| and |hland_states.SP|) time series of the sequences with an activated |IOSequence.ramflag| are directly available: >>> round_(inputs.t.series) -0.7, -1.5, -4.6, -8.2 >>> round_(states.sm.series[:, 0]) 99.1369, 99.01204, 98.93674, 98.91913 >>> round_(states.sp.series[:, 0, 0]) 0.0, 0.0, 0.0, 0.0 To inspect the time series of |hland_factors.ContriArea| and |hland_factors.TC|, you must first activate their |IOSequence.ramflag| and then load their data manually with method |IOSequence.load_series|. The latter requires some additional configuration effort (see the documentation on module |netcdftools| for further information): >>> factors.contriarea.prepare_series() >>> factors.tc.prepare_series() >>> pub.sequencemanager.filetype = "nc" >>> with TestIO(): ... pub.sequencemanager.open_netcdfreader() ... factors.contriarea.load_series() ... factors.tc.load_series() ... pub.sequencemanager.close_netcdfreader() >>> round_(factors.contriarea.series) 0.431311, 0.430524, 0.430049, 0.429938 >>> round_(factors.tc.series[:, 0]) 0.453086, -0.346914, -3.446914, -7.046914 We also load time series of |hland_states.SM| and |hland_states.SP| to demonstrate that the data written to the respective NetCDF files are identical with the data directly stored in RAM: >>> with TestIO(): ... pub.sequencemanager.open_netcdfreader() ... states.sm.load_series() ... states.sp.load_series() ... pub.sequencemanager.close_netcdfreader() >>> round_(states.sm.series[:, 0]) 99.1369, 99.01204, 98.93674, 98.91913 >>> round_(states.sp.series[:, 0, 0]) 0.0, 0.0, 0.0, 0.0 Writing the time series of input sequences on the fly is supported but not simultaneously with reading them (at best, one would overwrite the same file with the same data; at worst, one could corrupt it): >>> inputs.t.prepare_series(read_jit=True, write_jit=True) Traceback (most recent call last): ... ValueError: Reading from and writing into the same NetCDF file "just in time" \ during a simulation run is not supported but tried for sequence `t` of element \ `land_lahn_marb`. For simplifying the following examples, we now handle all model time series in RAM: >>> pub.sequencemanager.filetype = "asc" >>> hp.prepare_modelseries() >>> with TestIO(): ... hp.load_inputseries() You cannot only access the time series data of individual |IOSequence| objects, but you can also modify it. See, for example, the simulated time series for flux sequence |hland_fluxes.PC| (adjusted precipitation), which is zero because the values of input sequence |hland_inputs.P| (given precipitation) are also zero: >>> round_(fluxes.pc.series[:, 0]) 0.0, 0.105611, 0.0, 0.0 We can assign different values to attribute |IOSequence.series| of sequence |hland_inputs.P|, perform a new simulation run, and see that the newly calculated time series of sequence |hland_fluxes.PC| reflects our data modification: >>> inputs.p.series = 10.0 >>> hp.simulate() >>> round_(fluxes.pc.series[:, 0]) 9.154557, 10.561131, 10.665633, 10.665633 Another convenience property is |IOSequence.seriesshape|, which combines the length of the simulation period with the shape of the individual |IOSequence| object: >>> inputs.p.seriesshape (4,) >>> fluxes.pc.seriesshape (4, 13) Note that resetting the |IOSequence.shape| of an |IOSequence| object does not change how it handles its internal time series data but results in a loss of current information: >>> factors.tc.seriesshape (4, 13) >>> factors.fastaccess._tc_length 13 >>> round_(factors.tc.series[:, 0], 1) 0.5, -0.3, -3.4, -7.0 >>> factors.tc.shape = 2, >>> factors.tc.seriesshape (4, 2) >>> factors.fastaccess._tc_length 2 >>> round_(factors.tc.series[:, 0]) nan, nan, nan, nan Resetting the |IOSequence.shape| of |IOSequence| objects with a deactivated |IOSequence.ramflag| data works likewise: >>> fluxes.pc.prepare_series(allocate_ram=False) >>> fluxes.pc.seriesshape (4, 13) >>> fluxes.fastaccess._pc_length 13 >>> fluxes.pc.series Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: Sequence `pc` of element \ `land_lahn_marb` is not requested to make any time series data available. >>> fluxes.pc.shape = (2,) >>> fluxes.pc.seriesshape (4, 2) >>> fluxes.fastaccess._pc_length 2 >>> fluxes.pc.series = 1.0 Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: Sequence `pc` of element \ `land_lahn_marb` is not requested to make any time series data available. .. testsetup:: >>> from hydpy import Node, Element >>> Node.clear_all() >>> Element.clear_all() """ subvars: ( IOSequences[Sequences, IOSequence, FastAccessIOSequence] | IOSequences[devicetools.Node, IOSequence, FastAccessIOSequence] ) """The subgroup to which the IO sequence belongs.""" subseqs: ( IOSequences[Sequences, IOSequence, FastAccessIOSequence] | IOSequences[devicetools.Node, IOSequence, FastAccessIOSequence] ) """Alias for |IOSequence.subvars|.""" fastaccess: FastAccessIOSequence """Object for accessing the IO sequence's data with little overhead.""" def _finalise_connections(self) -> None: self.__hydpy__set_fastaccessattribute__("ramflag", False) self.__hydpy__set_fastaccessattribute__("diskflag_reading", False) self.__hydpy__set_fastaccessattribute__("diskflag_writing", False) super()._finalise_connections() @propertytools.DefaultPropertySeriesFileType def filetype(self) -> SeriesFileType: """"Ending of the time series data file. Usually, |IOSequence| objects query the current file type from the |SequenceManager| object available in the global |pub| module: >>> from hydpy import pub >>> from hydpy.core.filetools import SequenceManager >>> pub.sequencemanager = SequenceManager() >>> from hydpy.core.sequencetools import InputSequence >>> inputsequence = InputSequence(None) >>> inputsequence.filetype 'asc' Alternatively, you can specify the file type for each |IOSequence| object individually: >>> inputsequence.filetype = "npy" >>> inputsequence.filetype 'npy' >>> inputsequence.filetype = "nc" >>> inputsequence.filetype 'nc' Use the `del` statement to reset the object-specific setting: >>> del inputsequence.filetype >>> inputsequence.filetype 'asc' If neither a specific definition nor a |SequenceManager| object is available, property |IOSequence.filetype| raises the following error: >>> del pub.sequencemanager >>> inputsequence.filetype Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: Sequence `inputsequence` does \ not know its file type. Either set it manually or prepare `pub.sequencemanager` \ correctly. """ try: return cast(SeriesFileType, hydpy.pub.sequencemanager.filetype) except exceptiontools.AttributeNotReady: raise exceptiontools.AttributeNotReady( f"Sequence {objecttools.devicephrase(self)} does not know its file " f"type. Either set it manually or prepare `pub.sequencemanager` " f"correctly." ) from None @propertytools.DefaultPropertySeriesAggregationType def aggregation(self) -> SeriesAggregationType: """Type of aggregation for writing the time series to a data file. Usually, |IOSequence| objects query the current aggregation mode from the |SequenceManager| object available in the global |pub| module: >>> from hydpy import pub >>> from hydpy.core.filetools import SequenceManager >>> pub.sequencemanager = SequenceManager() >>> from hydpy.core.sequencetools import FluxSequence >>> fluxsequence = FluxSequence(None) >>> fluxsequence.aggregation 'none' Alternatively, you can specify the aggregation for each |IOSequence| object individually: >>> fluxsequence.aggregation = "mean" >>> fluxsequence.aggregation 'mean' Use the `del` statement to reset the object-specific setting: >>> del fluxsequence.aggregation >>> fluxsequence.aggregation 'none' If neither a specific definition nor a |SequenceManager| object is available, property |IOSequence.aggregation| raises the following error: >>> del pub.sequencemanager >>> fluxsequence.aggregation Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: Sequence `fluxsequence` does not \ know its aggregation mode. Either set it manually or prepare `pub.sequencemanager` \ correctly. """ try: return cast(SeriesAggregationType, hydpy.pub.sequencemanager.aggregation) except exceptiontools.AttributeNotReady: raise exceptiontools.AttributeNotReady( f"Sequence {objecttools.devicephrase(self)} does not know its " f"aggregation mode. Either set it manually or prepare " f"`pub.sequencemanager` correctly." ) from None @propertytools.DefaultPropertyBool def overwrite(self) -> bool: """True/False flag indicating if overwriting an existing data file is allowed or not. Usually, |IOSequence| objects query the current overwrite flag from the |SequenceManager| object available in the global |pub| module: >>> from hydpy import pub >>> from hydpy.core.filetools import SequenceManager >>> pub.sequencemanager = SequenceManager() >>> from hydpy.core.sequencetools import FluxSequence >>> fluxsequence = FluxSequence(None) >>> fluxsequence.overwrite 0 Alternatively, you can specify the overwrite flag for each |IOSequence| object individually: >>> fluxsequence.overwrite = True >>> fluxsequence.overwrite 1 Use the `del` statement to reset the object-specific setting: >>> del fluxsequence.overwrite >>> fluxsequence.overwrite 0 If neither a specific definition nor a |SequenceManager| object is available, property |IOSequence.overwrite| raises the following error: >>> del pub.sequencemanager >>> fluxsequence.overwrite Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: Sequence `fluxsequence` does not \ know its overwrite flag. Either set it manually or prepare `pub.sequencemanager` \ correctly. """ try: return bool(hydpy.pub.sequencemanager.overwrite) except exceptiontools.AttributeNotReady: raise exceptiontools.AttributeNotReady( f"Sequence {objecttools.devicephrase(self)} does not know its " f"overwrite flag. Either set it manually or prepare " f"`pub.sequencemanager` correctly." ) from None @propertytools.DefaultPropertyStr def filename(self) -> str: """The filename of the relevant time series file. By default, the filenames of file types that store time series of single sequence instance consists of |IOSequence.descr_device|, |IOSequence.descr_sequence|, and |IOSequence.filetype|: >>> from hydpy.core.sequencetools import StateSequence >>> class S(StateSequence): ... descr_device = "device" ... descr_sequence = "group_sequence" ... filetype = "npy" ... aggregation = "none" >>> s = S(None) >>> s.filename 'device_group_sequence.npy' For file types that store time series of multiple sequence instances, |IOSequence.descr_device| is omitted: >>> s.filetype = "nc" >>> s.filename 'group_sequence.nc' When dealing with aggregated time series, the aggregation mode is suffixed: >>> s.aggregation = "mean" >>> s.filename 'group_sequence_mean.nc' >>> s.filetype = "asc" >>> s.filename 'device_group_sequence_mean.asc' """ if (agg := self.aggregation) == "none": aggregation = "" else: aggregation = f"_{agg}" if (filetype := self.filetype) == "nc": return f"{self.descr_sequence}{aggregation}.nc" return f"{self.descr_device}_{self.descr_sequence}{aggregation}.{filetype}" @propertytools.DefaultPropertyStr def dirpath(self) -> str: """The absolute path to the time series directory. As long as it is not overwritten, |IOSequence.dirpath| is identical to the attribute |FileManager.currentpath| of the |SequenceManager| object available in module |pub|: >>> from hydpy import pub, repr_ >>> from hydpy.core.filetools import SequenceManager >>> class SM(SequenceManager): ... currentpath = "temp" >>> pub.sequencemanager = SM() >>> from hydpy.core.sequencetools import StateSequence >>> repr_(StateSequence(None).dirpath) 'temp' """ return hydpy.pub.sequencemanager.currentpath @propertytools.DefaultPropertyStr def filepath(self) -> str: """The absolute path to the time series file. The path pointing to the file consists of |IOSequence.dirpath| and |IOSequence.filename|: >>> from hydpy.core.sequencetools import StateSequence >>> seq = StateSequence(None) >>> seq.dirpath = "path" >>> seq.filename = "file.npy" >>> from hydpy import repr_ >>> repr_(seq.filepath) 'path/file.npy' """ return os.path.join(self.dirpath, self.filename)
[docs] def update_fastaccess(self) -> None: """Update the |FastAccessIOSequence| object handled by the actual |IOSequence| object. Users do not need to apply the method |IOSequence.update_fastaccess| directly. The following information should be relevant for framework developers only. The main documentation on class |Sequence_| mentions that the |FastAccessIOSequence| attribute handles some information about its sequences, but it needs to be kept up-to-date by the sequences themselves. This updating is the task of method |IOSequence.update_fastaccess|, being called by some other methods class |IOSequence| call. We show this via the hidden attribute `length`, which is 0 after initialisation, and automatically set to another value when assigning it to property |IOSequence.shape| of |IOSequence| subclasses as |lland_fluxes.NKor|: >>> from hydpy import prepare_model >>> model = prepare_model("lland_dd") >>> nkor = model.sequences.fluxes.nkor >>> nkor.fastaccess._nkor_length 0 >>> nkor.shape = (3,) >>> nkor.fastaccess._nkor_length 3 """ length = 1 for idx in range(self.NDIM): length *= self.shape[idx] self.__hydpy__set_fastaccessattribute__(f"length_{idx}", self.shape[idx]) self.__hydpy__set_fastaccessattribute__("length", length)
[docs] def connect_netcdf(self, ncarray: NDArrayFloat) -> None: """Connect the current |IOSequence| object to the given buffer array for reading from or writing to a NetCDF file on the fly during a simulation run.""" self.__hydpy__set_fastaccessattribute__("ncarray", ncarray)
[docs] def prepare_series( self, allocate_ram: bool | None = True, read_jit: bool | None = False, write_jit: bool | None = False, ) -> None: """Define how to handle the time series data of the current |IOSequence| object. See the main documentation on class |IOSequence| for general information on method |IOSequence.prepare_series|. Here, we only discuss the special case of passing |None| to it to preserve predefined settings. When leaving out certain arguments, |IOSequence.prepare_series| takes their boolean defaults. That means subsequent calls overwrite previous ones: >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> t = hp.elements.land_lahn_marb.model.sequences.inputs.t >>> t.prepare_series(allocate_ram=False, read_jit=True) >>> t.ramflag, t.diskflag_reading, t.diskflag_writing (False, True, False) >>> t.prepare_series(write_jit=True) >>> t.ramflag, t.diskflag_reading, t.diskflag_writing (True, False, True) If you want to change one setting without modifying the others, pass |None| to the latter: >>> t.prepare_series(allocate_ram=False, read_jit=None, write_jit=None) >>> t.ramflag, t.diskflag_reading, t.diskflag_writing (False, False, True) >>> t.prepare_series(allocate_ram=None, read_jit=True, write_jit=False) >>> t.ramflag, t.diskflag_reading, t.diskflag_writing (False, True, False) >>> t.prepare_series(allocate_ram=None, read_jit=None, write_jit=None) >>> t.ramflag, t.diskflag_reading, t.diskflag_writing (False, True, False) The check for configurations attempting to both read and write "just in time" takes predefined flags into account: >>> t.prepare_series(read_jit=None, write_jit=True) Traceback (most recent call last): ... ValueError: Reading from and writing into the same NetCDF file "just in time" \ during a simulation run is not supported but tried for sequence `t` of element \ `land_lahn_marb`. >>> t.prepare_series(read_jit=False, write_jit=True) >>> t.prepare_series(read_jit=True, write_jit=None) Traceback (most recent call last): ... ValueError: Reading from and writing into the same NetCDF file "just in time" \ during a simulation run is not supported but tried for sequence `t` of element \ `land_lahn_marb`. """ readflag = read_jit or ((read_jit is None) and self.diskflag_reading) writeflag = write_jit or ((write_jit is None) and self.diskflag_writing) if readflag and writeflag: raise ValueError( f'Reading from and writing into the same NetCDF file "just in time" ' f"during a simulation run is not supported but tried for sequence " f"{objecttools.devicephrase(self)}." ) if allocate_ram is not None: ramflag = self.ramflag if allocate_ram and not ramflag: self.__set_array( numpy.full(self.seriesshape, numpy.nan, dtype=config.NP_FLOAT) ) if ramflag and not allocate_ram: del self.series self.__hydpy__set_fastaccessattribute__("ramflag", allocate_ram) if read_jit is not None: inflag = self.__hydpy__get_fastaccessattribute__("inputflag", False) self.__hydpy__set_fastaccessattribute__( "diskflag_reading", read_jit and not inflag ) if write_jit is not None: self.__hydpy__set_fastaccessattribute__("diskflag_writing", write_jit) self.update_fastaccess()
@property def ramflag(self) -> bool: """A flag telling if the actual |IOSequence| object makes its time series data directly available in RAM. See the main documentation on class |IOSequence| for further information. """ return self.__hydpy__get_fastaccessattribute__("ramflag") @property def diskflag_reading(self) -> bool: """A flag telling if the actual |IOSequence| reads its time series data on the fly from a NetCDF file during a simulation run. See the main documentation on class |IOSequence| for further information. """ return self.__hydpy__get_fastaccessattribute__("diskflag_reading") @property def diskflag_writing(self) -> bool: """A flag telling if the actual |IOSequence| writes its time series data on the fly to a NetCDF file during a simulation run. See the main documentation on class |IOSequence| for further information. """ return self.__hydpy__get_fastaccessattribute__("diskflag_writing") @property def diskflag(self) -> bool: """A flag telling if |IOSequence.diskflag_reading| and/or |IOSequence.diskflag_writing| of the current |IOSequence| object is |True|: >>> from hydpy.core.sequencetools import StateSequence >>> for reading in (False, True): ... for writing in (False, True): ... class S(StateSequence): ... diskflag_reading = reading ... diskflag_writing = writing ... print(reading, writing, S(None).diskflag) False False False False True True True False True True True True """ return self.diskflag_reading or self.diskflag_writing @property def memoryflag(self) -> bool: """A flag telling if either |IOSequence.ramflag| and/or |IOSequence.diskflag| of the current |IOSequence| object is |True|: >>> from hydpy.core.sequencetools import StateSequence >>> for ram in (False, True): ... for disk in (False, True): ... class S(StateSequence): ... ramflag = ram ... diskflag = disk ... print(ram, disk, S(None).memoryflag) False False False False True True True False True True True True """ return self.ramflag or self.diskflag @property def seriesmode(self) -> SeriesMode: """A combination of property |IOSequence.ramflag|, |IOSequence.diskflag_reading|, and |IOSequence.diskflag_writing|. |IOSequence.seriesmode| allows querying and changing all mentioned properties in one step: >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> t = hp.elements.land_lahn_marb.model.sequences.inputs.t >>> t.prepare_series(read_jit=True) >>> sm_t = t.seriesmode >>> sm_t SeriesMode(ramflag=True, diskflag_reading=True, diskflag_writing=False) >>> p = hp.elements.land_lahn_marb.model.sequences.inputs.p >>> p.prepare_series(allocate_ram=False, write_jit=True) >>> sm_p = p.seriesmode >>> sm_p SeriesMode(ramflag=False, diskflag_reading=False, diskflag_writing=True) >>> t.seriesmode = sm_p >>> t.seriesmode SeriesMode(ramflag=False, diskflag_reading=False, diskflag_writing=True) >>> p.seriesmode = sm_t >>> p.seriesmode SeriesMode(ramflag=True, diskflag_reading=True, diskflag_writing=False) """ return SeriesMode( ramflag=self.ramflag, diskflag_reading=self.diskflag_reading, diskflag_writing=self.diskflag_writing, ) @seriesmode.setter def seriesmode(self, sm: SeriesMode) -> None: self.prepare_series( allocate_ram=sm.ramflag, read_jit=sm.diskflag_reading, write_jit=sm.diskflag_writing, ) def __set_array(self, values): values = numpy.array(values, dtype=config.NP_FLOAT) self.__hydpy__set_fastaccessattribute__("array", values) def _get_shape(self) -> tuple[int, ...]: """A tuple containing the actual lengths of all dimensions. When setting a new |IOSequence.shape| of an |IOSequence| object, one automatically calls method |IOSequence.update_fastaccess| and, if necessary, prepares the new internal |IOSequence.series| array. See the main documentation on class |IOSequence| for further information. """ return super()._get_shape() def _set_shape(self, shape: int | tuple[int, ...]): super()._set_shape(shape) if self.ramflag: values = numpy.full(self.seriesshape, numpy.nan, dtype=config.NP_FLOAT) self.__set_array(values) self.update_fastaccess() shape = propertytools.Property(fget=_get_shape, fset=_set_shape) @property def seriesshape(self) -> tuple[int, ...]: """The shape of the whole time series (time being the first dimension).""" seriesshape = [len(hydpy.pub.timegrids.init)] seriesshape.extend(self.shape) return tuple(seriesshape) def _get_series(self) -> InfoArray: """The complete time series data of the current |IOSequence| object within an |InfoArray| covering the whole initialisation period (defined by the |Timegrids.init| |Timegrid| of the global |Timegrids| object available in module |pub|).""" if self.ramflag: array = numpy.asarray(self.__hydpy__get_fastaccessattribute__("array")) return InfoArray(array, aggregation="unmodified") raise exceptiontools.AttributeNotReady( f"Sequence {objecttools.devicephrase(self)} is not requested to make any " f"time series data available." ) def _set_series(self, values) -> None: if self.ramflag: self.__set_array( numpy.full(self.seriesshape, values, dtype=config.NP_FLOAT) ) self.check_completeness() else: raise exceptiontools.AttributeNotReady( f"Sequence {objecttools.devicephrase(self)} is not requested to make " f"any time series data available." ) def _del_series(self) -> None: if self.ramflag: self.__hydpy__set_fastaccessattribute__("array", None) self.__hydpy__set_fastaccessattribute__("ramflag", False) series = property(_get_series, _set_series, _del_series) def _get_simseries(self) -> InfoArray: """Read and write access to the subset of the data of property |IOSequence.series| covering the actual simulation period (defined by the |Timegrids.sim| |Timegrid| of the global |Timegrids| object available in module |pub|). >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> t = hp.elements.land_lahn_marb.model.sequences.inputs.t >>> pub.timegrids.sim.dates = "1996-01-02", "1996-01-04" >>> from hydpy import print_vector >>> print_vector(t.series) -0.7, -1.5, -4.6, -8.2 >>> print_vector(t.simseries) -1.5, -4.6 >>> t.simseries = 1.0, 2.0 >>> print_vector(t.series) -0.7, 1.0, 2.0, -8.2 .. testsetup:: >>> from hydpy import Element, Node >>> Element.clear_all() >>> Node.clear_all() """ idx0, idx1 = hydpy.pub.timegrids.simindices return self.series[idx0:idx1] def _set_simseries(self, values) -> None: idx0, idx1 = hydpy.pub.timegrids.simindices self.series[idx0:idx1] = values simseries = property(_get_simseries, _set_simseries) def _get_evalseries(self) -> InfoArray: """Read and write access to the subset of the data of property | IOSequence.series| covering the actual evaluation period (defined by the |Timegrids.eval_| |Timegrid| of the global |Timegrids| object available in module |pub|). >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> t = hp.elements.land_lahn_marb.model.sequences.inputs.t >>> pub.timegrids.eval_.dates = "1996-01-02", "1996-01-04" >>> from hydpy import print_vector >>> print_vector(t.series) -0.7, -1.5, -4.6, -8.2 >>> print_vector(t.evalseries) -1.5, -4.6 >>> t.evalseries = 1.0, 2.0 >>> print_vector(t.series) -0.7, 1.0, 2.0, -8.2 .. testsetup:: >>> from hydpy import Element, Node >>> Element.clear_all() >>> Node.clear_all() """ idx0, idx1 = hydpy.pub.timegrids.evalindices return self.series[idx0:idx1] def _set_evalseries(self, values) -> None: idx0, idx1 = hydpy.pub.timegrids.evalindices self.series[idx0:idx1] = values evalseries = property(_get_evalseries, _set_evalseries)
[docs] def load_series(self) -> None: """Read time series data from a file. Method |IOSequence.load_series| only calls method |SequenceManager.load_file| of class |SequenceManager| passing itself as the only argument. Hence, see the documentation on the class |SequenceManager| for further information. The following example only shows the error messages when |SequenceManager.load_file| is missing due to incomplete project configurations: .. testsetup:: >>> from hydpy import pub >>> del pub.sequencemanager >>> from hydpy.core.sequencetools import StateSequence >>> StateSequence(None).load_series() Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: While trying to load the time \ series data of `statesequence`, the following error occurred: Attribute \ sequencemanager of module `pub` is not defined at the moment. """ try: sequencemanager = hydpy.pub.sequencemanager except BaseException: objecttools.augment_excmessage( f"While trying to load the time series data of " f"{objecttools.devicephrase(self)}" ) sequencemanager.load_file(self)
[docs] def adjust_series( self, timegrid_data: timetools.Timegrid, values: NDArrayFloat ) -> NDArrayFloat: """Adjust a time series to the current initialisation period. Note that, in most *HydPy* applications, method |IOSequence.adjust_series| is called by other methods related to reading data from files and does not need to be called by the user directly. However, if you want to call it directly for some reason, you need to make sure that the shape of the given |numpy| |numpy.ndarray| fits the given |Timegrid| object. Often, time series data available in data files cover a longer period than required for an actual simulation run. Method |IOSequence.adjust_series| selects the relevant data by comparing the initialisation |Timegrid| available in module |pub| and the given "data" |Timegrid| object. We explain this behaviour by using the `HydPy-H-Lahn` example project and focussing on the |Obs| sequence of |Node| `dill_assl`: >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> obs = hp.nodes.dill_assl.sequences.obs With identical initialisation and data time grids, method |IOSequence.adjust_series| returns the given data completely: >>> from hydpy import print_vector, Timegrid >>> import numpy >>> with TestIO(), pub.options.checkseries(False): ... print_vector(obs.adjust_series( ... Timegrid("1996-01-01", "1996-01-05", "1d"), ... numpy.arange(4, dtype=float))) 0.0, 1.0, 2.0, 3.0 For "too long" data, it only returns the relevant one: >>> with TestIO(), pub.options.checkseries(False): ... print_vector(obs.adjust_series( ... Timegrid("1995-12-31", "1996-01-07", "1d"), ... numpy.arange(7, dtype=float))) 1.0, 2.0, 3.0, 4.0 For "too short" data, the behaviour differs depending on option |Options.checkseries|. With |Options.checkseries| being enabled, method |IOSequence.adjust_series| raises a |RuntimeError|. With |Options.checkseries| being disabled, it extends the given array with |numpy.nan| values (using method |IOSequence.adjust_short_series|): >>> with TestIO(), pub.options.checkseries(True): ... obs.adjust_series(Timegrid("1996-01-02", "1996-01-04", "1d"), ... numpy.zeros((3,))) # doctest: +ELLIPSIS Traceback (most recent call last): ... RuntimeError: For sequence `obs` of node `dill_assl` the initialisation time \ grid (Timegrid("1996-01-01 00:00:00", "1996-01-05 00:00:00", "1d")) does not define a \ subset of the time grid of the data file `...dill_assl_obs_q.asc` \ (Timegrid("1996-01-02 00:00:00", "1996-01-04 00:00:00", "1d")). >>> with TestIO(), pub.options.checkseries(False): ... print_vector(obs.adjust_series( ... Timegrid("1996-01-02", "1996-01-04", "1d"), numpy.zeros((2,)))) nan, 0.0, 0.0, nan Additional checks raise errors in case of non-matching shapes or time information: >>> with TestIO(): ... obs.adjust_series(Timegrid("1996-01-01", "1996-01-05", "1d"), ... numpy.zeros((5, 2))) # doctest: +ELLIPSIS Traceback (most recent call last): ... RuntimeError: The shape of sequence `obs` of node `dill_assl` is `()` but \ according to the data file `...dill_assl_obs_q.asc` it should be `(2,)`. >>> with TestIO(): ... obs.adjust_series(Timegrid("1996-01-01", "1996-01-05", "1h"), ... numpy.zeros((24*5,))) # doctest: +ELLIPSIS Traceback (most recent call last): ... RuntimeError: According to data file `...dill_assl_obs_q.asc`, the date time \ step of sequence `obs` of node `dill_assl` is `1h` but the actual simulation time \ step is `1d`. .. testsetup:: >>> from hydpy import Node, Element >>> Node.clear_all() >>> Element.clear_all() """ if self.shape != values.shape[1:]: raise RuntimeError( f"The shape of sequence {objecttools.devicephrase(self)} is " f"`{self.shape}` but according to the data file `{self.filepath}` it " f"should be `{values.shape[1:]}`." ) if hydpy.pub.timegrids.init.stepsize != timegrid_data.stepsize: raise RuntimeError( f"According to data file `{self.filepath}`, the date time step of " f"sequence {objecttools.devicephrase(self)} is " f"`{timegrid_data.stepsize}` but the actual simulation time step is " f"`{hydpy.pub.timegrids.init.stepsize}`." ) if hydpy.pub.timegrids.init not in timegrid_data: if hydpy.pub.options.checkseries: raise RuntimeError( f"For sequence {objecttools.devicephrase(self)} the initialisation " f"time grid ({hydpy.pub.timegrids.init}) does not define a subset " f"of the time grid of the data file `{self.filepath}` " f"({timegrid_data})." ) return self.adjust_short_series(timegrid_data, values) idx1 = timegrid_data[hydpy.pub.timegrids.init.firstdate] idx2 = timegrid_data[hydpy.pub.timegrids.init.lastdate] return values[idx1:idx2]
[docs] def adjust_short_series( self, timegrid: timetools.Timegrid, values: NDArrayFloat ) -> NDArrayFloat: """Adjust a short time series to a longer time grid. Mostly, time series data to be read from files should span (at least) the whole initialisation period of a *HydPy* project. However, incomplete time series might also be helpful for some variables used only for comparison (e.g. observed runoff used for calibration). Method |IOSequence.adjust_short_series| adjusts such incomplete series to the public initialisation time grid stored in module |pub|. It is automatically called in method |IOSequence.adjust_series| when necessary, provided that the option |Options.checkseries| is disabled. Assume the initialisation period of a HydPy project spans five days: >>> from hydpy import pub >>> pub.timegrids = "2000.01.10", "2000.01.15", "1d" Prepare a node series object for observational data: >>> from hydpy.core.sequencetools import Obs >>> obs = Obs(None) Prepare a test function that expects the time grid of the data and the data itself, which returns the adjusted array through invoking the method |IOSequence.adjust_short_series|: >>> import numpy >>> def test(timegrid): ... values = numpy.ones(len(timegrid)) ... return obs.adjust_short_series(timegrid, values) The following calls to the test function show the arrays returned for different kinds of misalignments: >>> from hydpy import print_vector, Timegrid >>> print_vector(test(Timegrid("2000.01.05", "2000.01.20", "1d"))) 1.0, 1.0, 1.0, 1.0, 1.0 >>> print_vector(test(Timegrid("2000.01.12", "2000.01.15", "1d"))) nan, nan, 1.0, 1.0, 1.0 >>> print_vector(test(Timegrid("2000.01.12", "2000.01.17", "1d"))) nan, nan, 1.0, 1.0, 1.0 >>> print_vector(test(Timegrid("2000.01.10", "2000.01.13", "1d"))) 1.0, 1.0, 1.0, nan, nan >>> print_vector(test(Timegrid("2000.01.08", "2000.01.13", "1d"))) 1.0, 1.0, 1.0, nan, nan >>> print_vector(test(Timegrid("2000.01.12", "2000.01.13", "1d"))) nan, nan, 1.0, nan, nan >>> print_vector(test(Timegrid("2000.01.05", "2000.01.10", "1d"))) nan, nan, nan, nan, nan >>> print_vector(test(Timegrid("2000.01.05", "2000.01.08", "1d"))) nan, nan, nan, nan, nan >>> print_vector(test(Timegrid("2000.01.15", "2000.01.18", "1d"))) nan, nan, nan, nan, nan >>> print_vector(test(Timegrid("2000.01.16", "2000.01.18", "1d"))) nan, nan, nan, nan, nan After enabling option |Options.usedefaultvalues|, the missing values are initialised with zero instead of nan: >>> with pub.options.usedefaultvalues(True): ... print_vector(test(Timegrid("2000.01.12", "2000.01.17", "1d"))) 0.0, 0.0, 1.0, 1.0, 1.0 """ idxs = [ timegrid[hydpy.pub.timegrids.init.firstdate], timegrid[hydpy.pub.timegrids.init.lastdate], ] valcopy = values values = numpy.full(self.seriesshape, float(self.initinfo[0])) len_ = len(valcopy) jdxs = [] for idx in idxs: if idx < 0: jdxs.append(0) elif idx <= len_: jdxs.append(idx) else: jdxs.append(len_) valcopy = valcopy[jdxs[0] : jdxs[1]] zdx1 = max(-idxs[0], 0) zdx2 = zdx1 + jdxs[1] - jdxs[0] values[zdx1:zdx2] = valcopy return values
[docs] def apply_adjusted_series( self, timegrid_data: timetools.Timegrid, series: NDArrayFloat ) -> None: """Take the values of the given "adjusted series". The "adjusted series" is usually returned by method |IOSequence.adjust_series|. The behaviour of method |IOSequence.apply_adjusted_series| depends on option |SequenceManager.reset|. By default, "resetting" is enabled, meaning that |numpy.nan| values due to incomplete time series files overwrite previously available data. We demonstrate this using the NetCDF data provided by function |prepare_full_example_2| but changing the initialisation period (only advised for testing purposes): >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> pub.timegrids.init.firstdate = "1989-10-30" >>> pub.timegrids.init.lastdate = "1989-11-03" >>> t = hp.elements.land_dill_assl.model.sequences.inputs.t >>> t.series = -99.9 >>> opt = pub.options >>> sm = pub.sequencemanager >>> with ( ... TestIO(), sm.filetype("nc"), opt.checkseries(False), sm.netcdfreading() ... ): ... t.load_series() >>> from hydpy import round_ >>> round_(t.series) nan, nan, 10.1, 10.0 With option |SequenceManager.reset| disabled, method |IOSequence.apply_adjusted_series| keeps the already available data: >>> t.series = 99.9 >>> with TestIO(), sm.reset(False), sm.filetype("nc"), opt.checkseries(False): ... with sm.netcdfreading(): ... t.load_series() >>> round_(t.series) 99.9, 99.9, 10.1, 10.0 If the available data covers the complete initialisation period, option |SequenceManager.reset| does not make a difference: >>> pub.timegrids.init.firstdate = "1990-01-01" >>> pub.timegrids.init.lastdate = "2020-01-01" >>> with TestIO(), sm.filetype("nc"), opt.checkseries(False): ... t.series = -99.9 ... with sm.netcdfreading(): ... t.load_series() ... values = t.series.copy() ... t.series = 99.9 ... with sm.reset(False), sm.netcdfreading(): ... t.load_series() >>> import numpy >>> assert numpy.array_equal(values, t.series) """ if hydpy.pub.sequencemanager.reset: self.series = series else: init = hydpy.pub.timegrids.init i0 = max(init[timegrid_data.firstdate], 0) i1 = min(init[timegrid_data.lastdate], len(init)) self.series[i0:i1] = series[i0:i1]
[docs] def check_completeness(self) -> None: """Raise a |RuntimeError| if the |IOSequence.series| contains at least one |numpy.nan| value and if the option |Options.checkseries| is enabled. >>> from hydpy import pub >>> pub.timegrids = "2000-01-01", "2000-01-11", "1d" >>> from hydpy.core.sequencetools import StateSequence, StateSequences >>> class Seq(StateSequence): ... NDIM = 0 ... NUMERIC = False >>> class StateSequences(StateSequences): ... CLASSES = (Seq,) >>> seq = Seq(StateSequences(None)) >>> seq.__hydpy__connect_variable2subgroup__() >>> seq.prepare_series() >>> seq.check_completeness() Traceback (most recent call last): ... RuntimeError: The series array of sequence `seq` contains 10 nan values. >>> seq.series = 1.0 >>> seq.check_completeness() >>> seq.series[3] = numpy.nan >>> seq.check_completeness() Traceback (most recent call last): ... RuntimeError: The series array of sequence `seq` contains 1 nan value. >>> with pub.options.checkseries(False): ... seq.check_completeness() """ if hydpy.pub.options.checkseries: isnan = numpy.isnan(self.series) if numpy.any(isnan): nmb = numpy.sum(isnan) valuestring = "value" if nmb == 1 else "values" raise RuntimeError( f"The series array of sequence {objecttools.devicephrase(self)} " f"contains {nmb} nan {valuestring}." )
[docs] def save_series(self) -> None: """Write the time series data of the current |IOSequence| object to a file. Method |IOSequence.save_series| only calls method |SequenceManager.save_file| of class |SequenceManager|, passing itself as the only argument. Hence, see the documentation on class the |SequenceManager| for further information. The following example only shows the error messages when |SequenceManager.save_file| is missing due to incomplete project configurations: .. testsetup:: >>> from hydpy import pub >>> del pub.sequencemanager >>> from hydpy.core.sequencetools import StateSequence >>> StateSequence(None).save_series() Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: While trying to save the time \ series data of `statesequence`, the following error occurred: Attribute \ sequencemanager of module `pub` is not defined at the moment. """ try: sequencemanager = hydpy.pub.sequencemanager except BaseException: objecttools.augment_excmessage( f"While trying to save the time series data of " f"{objecttools.devicephrase(self)}" ) sequencemanager.save_file(self)
[docs] def save_mean(self, *args, **kwargs) -> None: """Average the time series data with method |IOSequence.average_series| of class |IOSequence| and write the result to file using method |SequenceManager.save_file| of class |SequenceManager|. The main documentation on class |SequenceManager| provides some examples. """ array = InfoArray(self.average_series(*args, **kwargs), aggregation="mean") with hydpy.pub.sequencemanager.aggregation("mean"): hydpy.pub.sequencemanager.save_file(self, array=array)
@property def seriesmatrix(self) -> MatrixFloat: """The actual |IOSequence| object's time series, arranged in a 2-dimensional matrix. For a 1-dimensional sequence object, property |IOSequence.seriesmatrix| returns the original values without modification: >>> from hydpy import pub >>> pub.timegrids = "2000-01-01", "2000-01-04", "1d" >>> from hydpy.models.hland import * >>> parameterstep("1d") >>> nmbzones(2) >>> fluxes.pc.prepare_series() >>> fluxes.pc.series = [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]] >>> from hydpy import print_vector >>> for values in fluxes.pc.seriesmatrix: ... print_vector(values) 1.0, 2.0 3.0, 4.0 5.0, 6.0 For all other sequences, |IOSequence.seriesmatrix| raises the following error by default: >>> inputs.p.seriesmatrix Traceback (most recent call last): ... NotImplementedError: Sequence `p` does not implement a method for converting \ its series to a 2-dimensional matrix. """ if self.NDIM == 1: return self.series raise NotImplementedError( f"Sequence {objecttools.devicephrase(self)} does not implement a method " f"for converting its series to a 2-dimensional matrix." )
[docs] def average_series(self, *args, **kwargs) -> InfoArray: """Average the actual time series of the |IOSequence| object for all time points. Method |IOSequence.average_series| works similarly to method |Variable.average_values| of class |Variable|, from which we borrow some examples. However, we must first prepare a |Timegrids| object to define the |IOSequence.series| length: >>> from hydpy import pub >>> pub.timegrids = "2000-01-01", "2000-01-04", "1d" As shown for method |Variable.average_values|, for 0-dimensional |IOSequence| objects, the result of method |IOSequence.average_series| equals |IOSequence.series| itself: >>> from hydpy.core.sequencetools import StateSequence, StateSequences >>> class SoilMoisture(StateSequence): ... NDIM = 0 ... NUMERIC = False >>> class StateSequences(StateSequences): ... CLASSES = (SoilMoisture,) >>> sm = SoilMoisture(StateSequences(None)) >>> sm.__hydpy__connect_variable2subgroup__() >>> sm.prepare_series() >>> import numpy >>> sm.series = numpy.array([190.0, 200.0, 210.0]) >>> sm.average_series() InfoArray([190., 200., 210.]) We require a weighting parameter for |IOSequence| objects with an increased dimensionality: >>> SoilMoisture.NDIM = 1 >>> sm.shape = 3 >>> sm.prepare_series() >>> sm.series = ([190.0, 390.0, 490.0], ... [200.0, 400.0, 500.0], ... [210.0, 410.0, 510.0]) >>> from hydpy.core.parametertools import Parameter >>> class Area(Parameter): ... NDIM = 1 ... shape = (3,) ... value = numpy.array([1.0, 1.0, 2.0]) >>> area = Area(None) >>> SoilMoisture.refweights = property(lambda self: area) >>> sm.average_series() InfoArray([390., 400., 410.]) The documentation on method |Variable.average_values| provides many examples of using different masks in different ways. Here, we only show the results of method |IOSequence.average_series| for a mask selecting the first two entries, for a mask selecting no entry at all, and for an ill-defined mask: >>> from hydpy.core.masktools import DefaultMask >>> class Soil(DefaultMask): ... @classmethod ... def new(cls, variable, **kwargs): ... return cls.array2mask(maskvalues) >>> SoilMoisture.mask = Soil() >>> maskvalues = [True, True, False] >>> sm.average_series() InfoArray([290., 300., 310.]) >>> maskvalues = [False, False, False] >>> sm.average_series() InfoArray([nan, nan, nan]) >>> maskvalues = [True, True] >>> sm.average_series() # doctest: +ELLIPSIS Traceback (most recent call last): ... IndexError: While trying to calculate the mean value of the internal time \ series of sequence `soilmoisture`, the following error occurred: While trying to \ access the value(s) of variable `area` with key `[ True True]`, the following error \ occurred: boolean index did not match indexed array ... """ try: if not self.NDIM: array = self.series else: mask = self.get_submask(*args, **kwargs) if numpy.any(mask): weights = self.refweights[mask] weights /= numpy.sum(weights) series = self.seriesmatrix[:, mask] array = numpy.sum(weights * series, axis=1) else: array = numpy.full( len(self.series), numpy.nan, dtype=config.NP_FLOAT ) return InfoArray(array, aggregation="mean") except BaseException: objecttools.augment_excmessage( f"While trying to calculate the mean value of the internal time " f"series of sequence {objecttools.devicephrase(self)}" )
[docs] def aggregate_series(self, *args, **kwargs) -> InfoArray: """Aggregate the time series data based on the actual |IOSequence.aggregation| attribute of the current |IOSequence| object. We prepare some nodes and elements with the help of method |prepare_io_example_1| and select a 1-dimensional flux sequence of type |lland_fluxes.NKor|: >>> from hydpy.core.testtools import prepare_io_example_1 >>> nodes, elements = prepare_io_example_1() >>> seq = elements.element3.model.sequences.fluxes.nkor If |IOSequence.aggregation| is `none`, the original time series values are returned: >>> seq.aggregation 'none' >>> seq.aggregate_series() InfoArray([[24., 25., 26.], [27., 28., 29.], [30., 31., 32.], [33., 34., 35.]]) If |IOSequence.aggregation| is `mean`, method |IOSequence.aggregate_series| is called: >>> seq.aggregation = "mean" >>> seq.aggregate_series() InfoArray([25., 28., 31., 34.]) In case the state of the sequence is invalid: >>> seq.aggregation = "nonexistent" >>> seq.aggregate_series() Traceback (most recent call last): ... RuntimeError: Unknown aggregation mode `nonexistent` for sequence `nkor` of \ element `element3`. The following technical test confirms the propr passing of all potential positional and keyword arguments: >>> seq.aggregation = "mean" >>> from unittest import mock >>> seq.average_series = mock.MagicMock() >>> _ = seq.aggregate_series(1, x=2) >>> seq.average_series.assert_called_with(1, x=2) """ mode = self.aggregation if mode == "none": # pylint: disable=comparison-with-callable return self.series if mode == "mean": # pylint: disable=comparison-with-callable return self.average_series(*args, **kwargs) raise RuntimeError( f"Unknown aggregation mode `{mode}` for sequence " f"{objecttools.devicephrase(self)}." )
@property @abc.abstractmethod def descr_sequence(self) -> str: """Description of the |IOSequence| object and its context.""" @property @abc.abstractmethod def descr_device(self) -> str: """Description of the |Device| object the |IOSequence| object belongs to."""
[docs] class ModelSequence(Sequence_): """Base class for sequences to be handled by |Model| objects.""" subvars: ModelSequences[ModelSequence, variabletools.FastAccess] """The subgroup to which the model sequence belongs.""" subseqs: ModelSequences[ModelSequence, variabletools.FastAccess] """Alias for |ModelSequence.subvars|.""" def __init__( self, subvars: ModelSequences[ModelSequence, variabletools.FastAccess] ) -> None: super().__init__(subvars) self.subseqs = subvars @property def descr_sequence(self) -> str: """Description of the |ModelSequence| object itself and the |Model| type and |SubSequences| group it belongs to. >>> from hydpy import prepare_model >>> from hydpy.models import test_stiff0d >>> model = prepare_model(test_stiff0d) >>> model.sequences.fluxes.q.descr_sequence 'test_stiff0d_flux_q' """ return ( f"{self.subseqs.seqs.model}_" f"{type(self.subseqs).__name__[:-9].lower()}_" f"{self.name}" ) @property def descr_model(self) -> str: """Description of the |Model| the |ModelSequence| object belongs to. >>> from hydpy import prepare_model >>> from hydpy.models import test, test_stiff0d >>> model = prepare_model(test) >>> model.sequences.fluxes.q.descr_model 'test' >>> model = prepare_model(test_stiff0d) >>> model.sequences.fluxes.q.descr_model 'test_stiff0d' """ return self.subseqs.seqs.model.__module__.split(".")[2] @property def descr_device(self) -> str: """Description of the |Element| object the |ModelSequence| object belongs to. >>> from hydpy import prepare_model, pub, Element >>> element = Element("my_element", outlets="outlet") >>> from hydpy.models.lland_knauf import * >>> parameterstep() >>> model.sequences.inputs.windspeed.descr_device '?' >>> element.model = model >>> model.sequences.inputs.windspeed.descr_device 'my_element' >>> from hydpy import pub >>> pub.timegrids = "2000-01-01", "2001-01-02", "1d" >>> nhru(1) >>> ft(1.0) >>> fhru(1.0) >>> gh(100.0) >>> lnk(ACKER) >>> measuringheightwindspeed(10.0) >>> lai(10.0) >>> wmax(300.0) >>> with model.add_aetmodel_v1("evap_aet_morsim"): ... pass >>> model.aetmodel.sequences.inputs.windspeed.descr_device 'my_element' .. testsetup:: >>> from hydpy import Node >>> Node.clear_all() >>> Node.clear_all() """ try: return self.subseqs.seqs.model.element.name except exceptiontools.AttributeNotReady: return "?" @property def numericshape(self) -> tuple[int, ...]: """The shape of the array of temporary values required for the relevant numerical solver. The class |ELSModel|, being the base of the "dam_llake" model, uses the "Explicit Lobatto Sequence" for solving differential equations and therefore requires up to eleven array fields for storing temporary values. Hence, the |ModelSequence.numericshape| of the 0-dimensional sequence |dam_fluxes.Inflow| is eleven: >>> from hydpy import prepare_model >>> model = prepare_model("dam_llake") >>> model.sequences.fluxes.inflow.numericshape (11,) Changing the |IOSequence.shape| through a little trick (just for demonstration purposes) shows that there are eleven entries for each "normal" |dam_fluxes.Inflow| value: >>> from hydpy.models.dam.dam_fluxes import Inflow >>> shape = Inflow.shape >>> Inflow.shape = (2,) >>> model.sequences.fluxes.inflow.numericshape (11, 2) >>> Inflow.shape = shape Erroneous configurations result in the following error: >>> del model.numconsts >>> model.sequences.fluxes.inflow.numericshape Traceback (most recent call last): ... AttributeError: The `numericshape` of a sequence like `inflow` depends on the \ configuration of the actual integration algorithm. While trying to query the \ required configuration data `nmb_stages` of the model associated with element `?`, \ the following error occurred: 'Model' object has no attribute 'numconsts' """ from hydpy.core import modeltools # pylint: disable=import-outside-toplevel try: model = self.subseqs.seqs.model assert isinstance(model, modeltools.ELSModel) # ToDo numericshape = [model.numconsts.nmb_stages] except AttributeError: objecttools.augment_excmessage( f"The `numericshape` of a sequence like `{self.name}` depends on the " f"configuration of the actual integration algorithm. While trying to " f"query the required configuration data `nmb_stages` of the model " f"associated with element `{objecttools.devicename(self)}`" ) numericshape.extend(self.shape) return tuple(numericshape)
[docs] class ModelIOSequence(ModelSequence, IOSequence): """Base class for sequences with time series functionalities to be handled by |Model| objects.""" subvars: ModelIOSequences[ModelIOSequence, FastAccessIOSequence] """The subgroup to which the model IO sequence belongs.""" subseqs: ModelIOSequences[ModelIOSequence, FastAccessIOSequence] """Alias for |ModelIOSequence.subvars|.""" node2idx: dict[devicetools.Node, int | None] """The connected |Node| instances and, for 1-dimensional link sequences, the corresponding column's indices.""" def __init__( self, subvars: ModelIOSequences[ModelIOSequence, FastAccessIOSequence] ) -> None: super().__init__(subvars) self.node2idx = {}
[docs] class BaseLinkInputSequence(ModelIOSequence): """Base class for |LinkSequence| and |InputSequence|."""
[docs] def connect_to_nodes( self, group: LinkInputSequenceGroup, available_nodes: list[devicetools.Node], applied_nodes: list[devicetools.Node], report_noconnect: bool, ) -> None: """Establish pointer connections with the relevant nodes. See the documentation on method |modeltools.Model.connect| for more information. """ self.node2idx.clear() selected_nodes = [] for node in available_nodes: if isinstance(var := node.variable, devicetools.FusedVariable): if self in var: selected_nodes.append(node) else: name = var.lower() if isinstance(var, str) else var.name if name == self.name: selected_nodes.append(node) if self.NDIM == 0: if not selected_nodes: if (group == "inputs") or not report_noconnect: return raise RuntimeError( f"Sequence {objecttools.elementphrase(self)} cannot be connected " f"due to no available node handling variable " f"`{self.name.upper()}`." ) if len(selected_nodes) > 1: raise RuntimeError( f"Sequence `{self.name}` cannot be connected as it is " f"0-dimensional but multiple nodes are available which are " f"handling variable `{type(self).__name__}`." ) node = selected_nodes[0] applied_nodes.append(node) assert isinstance(self, (InputSequence, LinkSequence)) self.set_pointer(node.get_double(group)) self.node2idx[node] = None elif self.NDIM == 1: self.shape = len(selected_nodes) for idx, node in enumerate(selected_nodes): applied_nodes.append(node) assert isinstance(self, LinkSequence) self.set_pointer(node.get_double(group), idx) self.node2idx[node] = idx
[docs] class InputSequence(BaseLinkInputSequence): """Base class for input sequences of |Model| objects. |InputSequence| objects provide their master model with input data, which is possible in two ways: either by providing their individually managed data (usually read from a file) or data shared with an input node (usually calculated by another model). This flexibility allows, for example, to let application model |hland_96| read already preprocessed precipitation time series or to couple it with application models like |conv_nn|, which interpolates precipitation during the simulation run. The second mechanism (coupling |InputSequence| objects with input nodes) is relatively new, and we might adjust the relevant interfaces in the future. As soon as we finally settle things, we will improve the following example and place it more prominently. In short, it shows that working with both types of input data sources at the same time works well and that the different |Node.deploymode| options are supported: >>> from hydpy.core.testtools import prepare_full_example_1 >>> prepare_full_example_1() >>> from hydpy import Element, FusedVariable, HydPy, Node, print_vector, pub, TestIO >>> from hydpy.aliases import hland_inputs_T, hland_inputs_P >>> hp = HydPy("HydPy-H-Lahn") >>> pub.timegrids = "1996-01-01", "1996-01-06", "1d" >>> node_t = Node("node_t", variable=hland_inputs_T) >>> node_p = Node("node_p", variable=FusedVariable("Precip", hland_inputs_P)) >>> node_q = Node("node_q") >>> land_dill_assl = Element("land_dill_assl", inputs=[node_t, node_p], ... outlets=node_q) >>> import os >>> with TestIO(): ... os.chdir("HydPy-H-Lahn/control/default") ... with open("land_dill_assl.py") as controlfile: ... exec(controlfile.read(), {}, locals()) ... parameters.update() ... land_dill_assl.model = model >>> aetmodel = model.aetmodel >>> petmodel = model.aetmodel.petmodel >>> model.sequences.inputs.t.inputflag True >>> model.sequences.inputs.p.inputflag True >>> petmodel.sequences.inputs.normalevapotranspiration.inputflag False >>> hp.update_devices(nodes=[node_t, node_p, node_q], elements=land_dill_assl) >>> hp.prepare_inputseries() >>> hp.prepare_factorseries() >>> hp.prepare_fluxseries() >>> with TestIO(): ... hp.load_inputseries() >>> hp.nodes.prepare_allseries() >>> node_t.deploymode = "oldsim" >>> node_t.sequences.sim.series = 1.0, 2.0, 3.0, 4.0, 5.0 >>> node_p.deploymode = "obs" >>> node_p.sequences.obs.series = 0.0, 4.0, 0.0, 8.0, 0.0 >>> hp.simulate() >>> print_vector(model.sequences.inputs.t.series) 1.0, 2.0, 3.0, 4.0, 5.0 >>> print_vector(model.sequences.factors.tc.series[:, 0]) 2.323207, 3.323207, 4.323207, 5.323207, 6.323207 >>> print_vector(model.sequences.inputs.p.series) 0.0, 4.0, 0.0, 8.0, 0.0 >>> print_vector(model.sequences.fluxes.pc.series[:, 0]) 0.0, 3.2514, 0.0, 6.5028, 0.0 >>> print_vector(petmodel.sequences.inputs.normalevapotranspiration.series) 0.3, 0.3, 0.3, 0.3, 0.3 >>> print_vector( ... aetmodel.sequences.fluxes.potentialsoilevapotranspiration.series[:, 0]) 0.309, 0.317657, 0.369, 0.352975, 0.432 .. testsetup:: >>> Element.clear_all() >>> Node.clear_all() >>> FusedVariable.clear_registry() """ subvars: InputSequences """The subgroup to which the input sequence belongs.""" subseqs: InputSequences """Alias for |InputSequence.subvars|.""" fastaccess: FastAccessInputSequence """Object for accessing the input sequence's data with little overhead.""" STANDARD_NAME: ClassVar[StandardInputNames] _CLS_FASTACCESS_PYTHON = FastAccessInputSequence def __hydpy__connect_variable2subgroup__(self) -> None: super().__hydpy__connect_variable2subgroup__() if self.NDIM == 0: self.__hydpy__set_fastaccessattribute__("inputflag", False) @property def descr_sequence(self) -> str: """Either a model-specific or a standard HydPy string describing the input sequence instance. By default, the returned string equals those of other |ModelSequence| subclasses: >>> from hydpy import pub >>> from hydpy.core.filetools import SequenceManager >>> pub.sequencemanager = SequenceManager() >>> from hydpy.models.hland_96 import * >>> parameterstep() >>> inputs.t.descr_sequence 'hland_96_input_t' When activating the standard "HydPy" convention instead of the "model-specific" convention, |InputSequence.descr_sequence| returns the standard name selected by the respective |InputSequence| subclass: >>> with pub.sequencemanager.convention("HydPy"): ... inputs.t.descr_sequence <StandardInputNames.AIR_TEMPERATURE: 'air_temperature'> """ if hydpy.pub.sequencemanager.convention == "model-specific": return super().descr_sequence return self.STANDARD_NAME
[docs] def set_pointer(self, double: pointerutils.Double) -> None: """Prepare a pointer referencing the given |Double| object. Method |InputSequence.set_pointer| should be relevant for framework developers and eventually for some model developers only. """ pdouble = pointerutils.PDouble(double) self.fastaccess.set_pointerinput(self.name, pdouble) self.__hydpy__set_fastaccessattribute__("inputflag", True) self.__hydpy__set_fastaccessattribute__("diskflag_reading", False) self.__hydpy__set_fastaccessattribute__("diskflag_writing", False)
@property def inputflag(self) -> bool: """A flag telling if the actual |InputSequence| object queries its data from an input node (|True|) or uses individually managed data, usually read from a data file (|False|). See the main documentation on class |InputSequence| for further information. """ return self.__hydpy__get_fastaccessattribute__("inputflag")
[docs] class OutputSequence(ModelIOSequence): """Base class for |FactorSequence|, |FluxSequence| and |StateSequence|. |OutputSequence| subclasses implement an optional output mechanism. Generally, as all instances of |ModelSequence| subclasses, output sequences handle values calculated within a simulation time step. With an activated |OutputSequence.outputflag|, they also pass their internal values to an output node (see the documentation on class |Element|), which makes them accessible to other models. This output mechanism (coupling |OutputSequence| objects with output nodes) is relatively new, and we might adjust the relevant interfaces in the future. Additionally, it works for 0-dimensional output sequences only so far. As soon as we finally settle things, we will improve the following example and place it more prominently. In short, it shows that everything works well for the different |Node.deploymode| options: >>> from hydpy.core.testtools import prepare_full_example_1 >>> prepare_full_example_1() >>> from hydpy import Element, HydPy, Node, print_vector, pub, Selection, TestIO >>> from hydpy.aliases import ( ... hland_fluxes_Perc, hland_fluxes_Q0, hland_fluxes_Q1, hland_states_UZ) >>> hp = HydPy("HydPy-H-Lahn") >>> pub.timegrids = "1996-01-01", "1996-01-06", "1d" >>> node_q0 = Node("node_q0", variable=hland_fluxes_Q0) >>> node_q1 = Node("node_q1", variable=hland_fluxes_Q1) >>> node_perc = Node("node_perc", variable=hland_fluxes_Perc) >>> node_uz = Node("node_uz", variable=hland_states_UZ) >>> node_q = Node("node_q") >>> land_dill_assl = Element("land_dill_assl", ... outlets=node_q, ... outputs=[node_q0, node_q1, node_perc, node_uz]) >>> import os >>> with TestIO(): ... os.chdir("HydPy-H-Lahn/control/default") ... with open("land_dill_assl.py") as controlfile: ... exec(controlfile.read(), {}, locals()) ... parameters.update() ... land_dill_assl.model = model >>> model.sequences.fluxes.q0.outputflag True >>> model.sequences.fluxes.q1.outputflag True >>> model.sequences.fluxes.perc.outputflag True >>> model.sequences.fluxes.qt.outputflag False >>> model.sequences.states.uz.outputflag True >>> model.sequences.states.lz.outputflag False >>> hp.update_devices(nodes=[node_q, node_q0, node_q1, node_perc, node_uz], ... elements=land_dill_assl) >>> with TestIO(): ... hp.load_conditions() >>> hp.prepare_inputseries() >>> with TestIO(): ... hp.load_inputseries() >>> hp.prepare_fluxseries() >>> hp.prepare_stateseries() >>> hp.nodes.prepare_allseries() >>> node_q0.deploymode = "oldsim" >>> node_q0.sequences.sim.series = 1.0 >>> node_q0.sequences.obs.series = 2.0 >>> node_q1.deploymode = "obs" >>> node_q1.sequences.obs.series = 3.0 >>> node_perc.deploymode = "newsim" >>> node_perc.sequences.obs.series = 4.0 >>> node_uz.sequences.obs.series = 5.0 >>> hp.simulate() >>> print_vector(node_q0.sequences.sim.series) 1.0, 1.0, 1.0, 1.0, 1.0 >>> print_vector(node_q0.sequences.obs.series) 2.0, 2.0, 2.0, 2.0, 2.0 >>> print_vector(model.sequences.fluxes.q1.series) 0.530782, 0.539976, 0.548629, 0.556786, 0.564477 >>> print_vector(node_q1.sequences.sim.series) 0.530782, 0.539976, 0.548629, 0.556786, 0.564477 >>> print_vector(node_q1.sequences.obs.series) 3.0, 3.0, 3.0, 3.0, 3.0 >>> print_vector(model.sequences.fluxes.perc.series) 0.694084, 0.693611, 0.693239, 0.693098, 0.693012 >>> print_vector(node_perc.sequences.sim.series) 0.694084, 0.693611, 0.693239, 0.693098, 0.693012 >>> print_vector(node_perc.sequences.obs.series) 4.0, 4.0, 4.0, 4.0, 4.0 >>> print_vector(model.sequences.states.uz.series) 5.628278, 4.368269, 3.337343, 2.452946, 1.662766 >>> print_vector(node_uz.sequences.sim.series) 5.628278, 4.368269, 3.337343, 2.452946, 1.662766 >>> print_vector(node_uz.sequences.obs.series) 5.0, 5.0, 5.0, 5.0, 5.0 .. testsetup:: >>> Element.clear_all() >>> Node.clear_all() """ subvars: OutputSequences[OutputSequence] """The subgroup to which the output sequence belongs.""" subseqs: OutputSequences[OutputSequence] """Alias for |OutputSequence.subvars|.""" fastaccess: FastAccessOutputSequence """Object for accessing the output sequence's data with little overhead.""" _CLS_FASTACCESS_PYTHON = FastAccessOutputSequence def __hydpy__connect_variable2subgroup__(self) -> None: super().__hydpy__connect_variable2subgroup__() if self.NDIM == 0: self.__hydpy__set_fastaccessattribute__("outputflag", False)
[docs] def set_pointer(self, double: pointerutils.Double) -> None: """Prepare a pointer referencing the given |Double| object. Method |OutputSequence.set_pointer| should be relevant for framework developers and eventually for some model developers only. """ pdouble = pointerutils.PDouble(double) self.fastaccess.set_pointeroutput(self.name, pdouble) self.__hydpy__set_fastaccessattribute__("outputflag", True)
@property def outputflag(self) -> bool: """A flag telling if the actual |OutputSequence| object passes its data to an output node (|True|) or not (|False|). See the main documentation on class |OutputSequence| for further information. """ return self.__hydpy__get_fastaccessattribute__("outputflag")
[docs] class DependentSequence(OutputSequence): """Base class for |FactorSequence| and |FluxSequence|.""" def _finalise_connections(self) -> None: from hydpy.core import modeltools # pylint: disable=import-outside-toplevel super()._finalise_connections() if self.NUMERIC and isinstance(self.subseqs.seqs.model, modeltools.ELSModel): values = None if self.NDIM else numpy.zeros(self.numericshape) self.__hydpy__set_fastaccessattribute__("points", values) self.__hydpy__set_fastaccessattribute__("integrals", copy.copy(values)) self.__hydpy__set_fastaccessattribute__("results", copy.copy(values)) value = None if self.NDIM else 0.0 self.__hydpy__set_fastaccessattribute__("sum", value) def _get_shape(self) -> tuple[int, ...]: """A tuple containing the actual lengths of all dimensions. |FactorSequence| and |FluxSequence| objects come with some additional `fastaccess` attributes, which should only be of interest to framework developers. One such attribute is the `results` array, handling the (intermediate or final) calculation results for factor and flux sequences, as shown in the following example for the 0-dimensional flux sequence |wland_fluxes.RH| of the |wland_wag| model: >>> from hydpy import prepare_model, print_vector, pub >>> model = prepare_model("wland_wag") >>> print_vector(model.sequences.fluxes.rh.fastaccess._rh_results) 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 For 1-dimensional numerical factor and flux sequences, the `results` attribute is |None| initially, as property |ModelSequence.numericshape| is unknown. Setting the |DependentSequence.shape| attribute of the respective |FactorSequence| or |FluxSequence| object (we select |wland_fluxes.EI| as an example) prepares all "fastaccess attributes" automatically: >>> ei = model.sequences.fluxes.ei >>> ei.fastaccess._ei_results >>> ei.shape = (2,) >>> ei.shape (2,) >>> ei.fastaccess._ei_results.shape (11, 2) """ return super()._get_shape() def _set_shape(self, shape: int | tuple[int, ...]) -> None: super()._set_shape(shape) if self.NDIM and self.NUMERIC: self.__hydpy__set_fastaccessattribute__( "points", numpy.zeros(self.numericshape) ) self.__hydpy__set_fastaccessattribute__( "integrals", numpy.zeros(self.numericshape) ) self.__hydpy__set_fastaccessattribute__( "results", numpy.zeros(self.numericshape) ) self.__hydpy__set_fastaccessattribute__("sum", numpy.zeros(self.shape)) shape = propertytools.Property(fget=_get_shape, fset=_set_shape)
[docs] class FactorSequence(DependentSequence): """Base class for factor sequences of |Model| objects.""" subvars: FactorSequences """The subgroup to which the factor sequence belongs.""" subseqs: FactorSequences """Alias for |FactorSequence.subvars|.""" NUMERIC = False # Changing this requires implementing the related functionalites
# in modules `modeltools` and `modeltutils`.[docs] class FluxSequence(DependentSequence): """Base class for flux sequences of |Model| objects.""" subvars: FluxSequences """The subgroup to which the flux sequence belongs.""" subseqs: FluxSequences """Alias for |FluxSequence.subvars|."""
[docs] class ConditionSequence(ModelSequence): """Base class for |StateSequence| and |LogSequence|. Class |ConditionSequence| should not be subclassed by model developers directly. Inherit from |StateSequence| or |LogSequence| instead. """ _oldargs: tuple[Any, ...] | None = None def __call__(self, *args) -> None: """The prefered way to pass values to |Sequence_| instances within initial condition files.""" super().__call__(*args) self.trim() self._oldargs = copy.deepcopy(args)
[docs] def trim(self, lower=None, upper=None) -> bool: """Apply |trim| of module |variabletools|.""" return variabletools.trim(self, lower, upper)
[docs] def reset(self): """Reset the value of the actual |StateSequence| or |LogSequence| object to the last value defined by "calling" the object. We use the |lland_knauf| application model, which handles sequences derived from |StateSequence| (taking |lland_states.Inzp| as an example) and from |LogSequence| (taking |lland_logs.LoggedSunshineDuration| as an example): >>> from hydpy import prepare_model, pub >>> model = prepare_model("lland_knauf") After defining their shapes, both sequences contain |numpy.nan| values: >>> inzp = model.sequences.states.inzp >>> inzp.shape = (2,) >>> inzp inzp(nan, nan) >>> lsd = model.sequences.logs.loggedsunshineduration >>> lsd.shape = 2 >>> lsd loggedsunshineduration(nan, nan) Before "calling" the sequences method |ConditionSequence.reset| does nothing: >>> inzp.values = 0.0 >>> inzp.reset() >>> inzp inzp(0.0, 0.0) >>> lsd.values = 0.0 >>> lsd.reset() >>> lsd loggedsunshineduration(0.0, 0.0) After "calling" the sequences method |ConditionSequence.reset| reuses the respective arguments: >>> with pub.options.warntrim(False): ... inzp(0.0, 1.0) >>> inzp.values = 0.0 >>> inzp inzp(0.0, 0.0) >>> with pub.options.warntrim(False): ... inzp.reset() >>> inzp inzp(0.0, 1.0) >>> lsd(1.0, 2.0) >>> lsd.values = 3.0 >>> lsd loggedsunshineduration(3.0, 3.0) >>> lsd.reset() >>> lsd loggedsunshineduration(1.0, 2.0) """ if self._oldargs: self(*self._oldargs)
[docs] class StateSequence(OutputSequence, ConditionSequence): """Base class for state sequences of |Model| objects. Each |StateSequence| object can handle states at two different "time points": at the beginning of a simulation step via property |StateSequence.old| and the end of a simulation step via property |StateSequence.new|. These properties are reflected by two different `fastaccess` attributes. `fastaccess_new` is an alias for the standard `fastaccess` attribute storing the customary information. `fastaccess_old` is an additional feature for keeping the supplemental information. We demonstrate the above explanations using state sequence |hland_states.SM| of the base model |hland_96| with a shape of two: >>> from hydpy import prepare_model, print_vector >>> model = prepare_model("hland") >>> model.parameters.control.fc.shape = (2,) >>> model.parameters.control.fc = 100.0 >>> sm = model.sequences.states.sm >>> sm.shape = (2,) Initially, no values are available at all: >>> sm sm(nan, nan) >>> print_vector(sm.values) nan, nan >>> print_vector(sm.new) nan, nan >>> print_vector(sm.old) nan, nan The typical way to define state values, especially within condition files, is to "call" state sequence objects, which sets both the "old" and the "new" states to the given value(s): >>> sm(1.0) >>> print_vector(sm.values) 1.0, 1.0 >>> print_vector(sm.new) 1.0, 1.0 >>> print_vector(sm.old) 1.0, 1.0 Alternatively, one can assign values to property |StateSequence.new| or property |StateSequence.old| (note that using |StateSequence.new| is identical with using the |Variable.value| property): >>> sm.new = 2.0, 3.0 >>> sm sm(2.0, 3.0) >>> print_vector(sm.values) 2.0, 3.0 >>> print_vector(sm.new) 2.0, 3.0 >>> print_vector(sm.old) 1.0, 1.0 >>> sm.old = 200.0 >>> sm sm(2.0, 3.0) >>> print_vector(sm.values) 2.0, 3.0 >>> print_vector(sm.new) 2.0, 3.0 >>> print_vector(sm.old) 200.0, 200.0 If you assign problematic values to property |StateSequence.old|, it raises similar error messages as property |Variable.value|: >>> sm.old = 1.0, 2.0, 3.0 Traceback (most recent call last): ... ValueError: While trying to set the old value(s) of state sequence `sm`, the \ following error occurred: While trying to convert the value(s) `(1.0, 2.0, 3.0)` to a \ numpy ndarray with shape `(2,)` and type `float`, the following error occurred: could \ not broadcast input array from shape (3,) into shape (2,) Just for completeness: Method |StateSequence.new2old| effectively takes the new values as old ones, but more efficiently than using the properties |StateSequence.new| and |StateSequence.old| (the Python method |StateSequence.new2old| is usually replaced by model-specific, cythonized version when working in Cython mode): >>> sm.new2old() >>> print_vector(sm.values) 2.0, 3.0 >>> print_vector(sm.new) 2.0, 3.0 >>> print_vector(sm.old) 2.0, 3.0 """ subvars: StateSequences """The subgroup to which the state sequence belongs.""" subseqs: StateSequences """Alias for |StateSequence.subvars|.""" fastaccess_new: FastAccessOutputSequence fastaccess_old: variabletools.FastAccess def __call__(self, *args) -> None: """The prefered way to pass values to |Sequence_| instances within initial condition files.""" super().__call__(*args) self.new2old() def _finalise_connections(self) -> None: from hydpy.core import modeltools # pylint: disable=import-outside-toplevel super()._finalise_connections() if self.NUMERIC and isinstance(self.subseqs.seqs.model, modeltools.ELSModel): value = None if self.NDIM else numpy.zeros(self.numericshape) self.__hydpy__set_fastaccessattribute__("points", value) self.__hydpy__set_fastaccessattribute__("results", copy.copy(value)) self.fastaccess_old = self.subseqs.fastaccess_old self.fastaccess_new = self.subseqs.fastaccess_new if self.NDIM: setattr(self.fastaccess_old, self.name, None) else: setattr(self.fastaccess_old, self.name, 0.0) def _get_shape(self) -> tuple[int, ...]: """A tuple containing the actual lengths of all dimensions. |StateSequence| objects come with some additional `fastaccess` attributes, which should only be of interest to framework developers. One such attribute is the `results` array, handling the (intermediate or final) calculation results for state sequence, as shown in the following example for the 0-dimensional sequence |wland_states.HS| of the |wland_wag| model: >>> from hydpy import prepare_model, print_vector, pub >>> model = prepare_model("wland_wag") >>> print_vector(model.sequences.states.hs.fastaccess._hs_results) 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 For 1-dimensional numerical state sequences, the `results` attribute is |None| initially, as property |ModelSequence.numericshape| is unknown. Setting the |StateSequence.shape| attribute of the respective |StateSequence| object (we select |wland_states.IC| as an example) prepares all "fastaccess attributes" automatically: >>> ic = model.sequences.states.ic >>> ic.fastaccess._ic_results >>> ic.shape = (2,) >>> ic.shape (2,) >>> ic.fastaccess._ic_results.shape (11, 2) """ return super()._get_shape() def _set_shape(self, shape: int | tuple[int, ...]): from hydpy.core import modeltools # pylint: disable=import-outside-toplevel super()._set_shape(shape) if self.NDIM: setattr(self.fastaccess_old, self.name, self.new.copy()) if self.NUMERIC and isinstance( self.subseqs.seqs.model, modeltools.ELSModel ): self.__hydpy__set_fastaccessattribute__( "points", numpy.zeros(self.numericshape) ) self.__hydpy__set_fastaccessattribute__( "results", numpy.zeros(self.numericshape) ) shape = propertytools.Property(fget=_get_shape, fset=_set_shape) @property def new(self): """State(s) after calling a |Model| calculation method. (Alias for property |Variable.value|). Property |StateSequence.new| handles, in contrast to property |StateSequence.old|, the newly calculated state values during each simulation step. It supports testing and debugging of individual |Model| methods but is typically irrelevant when scripting *HydPy* workflows. """ return self.value @new.setter def new(self, value): self.value = value @property def old(self): """State(s) before calling a |Model| calculation method. Note the similarity to property |StateSequence.new|. However, property |StateSequence.old| references the initial states of the respective simulation step, which should not be changed by |Model| calculation methods. """ return self._prepare_getvalue( True, getattr(self.fastaccess_old, self.name, None) ) @old.setter def old(self, value): try: setattr(self.fastaccess_old, self.name, self._prepare_setvalue(value)) except BaseException: objecttools.augment_excmessage( f"While trying to set the old value(s) of state sequence " f"{objecttools.devicephrase(self)}" )
[docs] def new2old(self) -> None: """Assign the |StateSequence.new| state values to the |StateSequence.old| values. See the main documentation on class |StateSequence| for further information. Note that method |StateSequence.new2old| is replaced by a model-specific, cythonized method when working in Cython mode. """ if self.NDIM: self.old[:] = self.new[:] else: self.old = self.new
[docs] class LogSequence(ConditionSequence): """Base class for logging values required for later calculations. Class |LogSequence| serves similar purposes as |StateSequence| but is less strict in its assumptions. While |StateSequence| objects always handle two states (the |StateSequence.old| and the |StateSequence.new| one), |LogSequence| objects are supposed to remember an arbitrary or sequence-specific number of values, which can be state values but, for example, also flux values. A typical use case is to store "old" values of effective precipitation to calculate "new" values of direct discharge using the unit hydrograph concept in later simulation steps. It is up to the model developer to ensure that a |LogSequence| subclass has the correct dimensionality and shape to store the required information. By convention, the "memory" of each |LogSequence| should be placed on the first axis for non-scalar properties. As |StateSequence| objects, |LogSequence| objects store relevant information to start a new simulation run where another one has ended and are thus written into and read from condition files. """ subvars: LogSequences """The subgroup to which the log sequence belongs.""" subseqs: LogSequences """Alias for |LogSequence.subvars|.""" _CLS_FASTACCESS_PYTHON = variabletools.FastAccess
[docs] class LogSequenceFixed(LogSequence): """Base class for log sequences with a fixed shape.""" NDIM = 1 SHAPE: int def _finalise_connections(self): self.shape = (self.SHAPE,) def _get_shape(self): """Sequences derived from |LogSequenceFixed| initialise themselves with a predefined shape. We take parameter |dam_logs.LoggedRequiredRemoteRelease| of base model |dam| as an example: >>> from hydpy.models.dam import * >>> parameterstep() >>> logs.loggedrequiredremoterelease.shape (1,) Property |LogSequenceFixed.shape| results in the following exception when you try to set a new shape: >>> logs.loggedrequiredremoterelease.shape = 2 Traceback (most recent call last): ... AttributeError: The shape of sequence `loggedrequiredremoterelease` cannot be \ changed, but this was attempted for element `?`. See the documentation on property |Variable.shape| of class |Variable| for further information. """ return super()._get_shape() def _set_shape(self, shape): if exceptiontools.attrready(self, "shape"): raise AttributeError( f"The shape of sequence `{self.name}` cannot be changed, but this was " f"attempted for element `{objecttools.devicename(self)}`." ) super()._set_shape(shape) shape = propertytools.Property(fget=_get_shape, fset=_set_shape)
[docs] class AideSequence(ModelSequence): """Base class for aide sequences of |Model| objects. Aide sequences store data only relevant for calculating an individual simulation time step but must be shared between different calculation methods of a |Model| object. """ subvars: AideSequences """The subgroup to which the aide sequence belongs.""" subseqs: AideSequences """Alias for |AideSequence.subvars|.""" _CLS_FASTACCESS_PYTHON = variabletools.FastAccess
[docs] class LinkSequence(BaseLinkInputSequence): """Base class for link sequences of |Model| objects. |LinkSequence| objects do not only handle values themselves but also point to the values handled by |NodeSequence| objects, using the functionalities provided by the Cython module |pointerutils|. Multiple |LinkSequence| objects of different application models can query and modify the same |NodeSequence| values, allowing different |Model| objects to share information and interact with each other. A note for developers: |LinkSequence| subclasses must be either 0-dimensional or 1-dimensional. We demonstrate the pointer functionalities by using the `HydPy-H-Lahn` example project by invoking function |prepare_full_example_2|: >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() We focus on the |musk_classic| application model `stream_lahn_marb_lahn_leun` routing inflow from node `lahn_marb` to node `lahn_leun`: >>> model = hp.elements.stream_lahn_marb_lahn_leun.model The first example shows that the 0-dimensional outlet sequence |musk_outlets.Q| points to the |Sim| sequence of node `lahn_leun`: >>> hp.nodes.lahn_leun.sequences.sim sim(0.0) >>> model.sequences.states.discharge discharge(10.0, 10.0) >>> model.update_outlets() >>> hp.nodes.lahn_leun.sequences.sim sim(10.0) The second example shows that the 1-dimensional inlet sequence |musk_inlets.Q| points to the |Sim| sequence of node `lahn_marb`: >>> model.sequences.inlets.q q(nan) >>> hp.nodes.lahn_marb.sequences.sim = 1.0 >>> model.update_inlets() >>> model.sequences.inlets.q q(1.0) In the example above, the 1-dimensional inlet sequence |musk_inlets.Q| only points to a single |NodeSequence| value. We now prepare a |exch_branch_hbv96| application model instance to show what happens when connecting a 1-dimensional |LinkSequence| object (|exch_outlets.Branched|) with three |NodeSequence| objects (see the documentation of application model |exch_branch_hbv96| for more details): >>> from hydpy import Element, Nodes, prepare_model >>> model = prepare_model("exch_branch_hbv96") >>> nodes = Nodes("input1", "input2", "output1", "output2", "output3") >>> branch = Element("branch", ... inlets=["input1", "input2"], ... outlets=["output1", "output2", "output3"]) >>> model.parameters.control.xpoints(0.0, 2.0, 4.0, 6.0) >>> model.parameters.control.ypoints( ... output1=[0.0, 1.0, 2.0, 3.0], ... output2=[0.0, 1.0, 0.0, 0.0], ... output3=[0.0, 0.0, 2.0, 6.0]) >>> branch.model = model Each field of the values of a 1-dimensional |LinkSequence| object points to another |NodeSequence| object: >>> model.sequences.outlets.branched = 1.0, 2.0, 3.0 >>> model.update_outlets() >>> nodes.output1.sequences.sim sim(1.0) >>> nodes.output2.sequences.sim sim(2.0) >>> nodes.output3.sequences.sim sim(3.0) .. testsetup:: >>> from hydpy import Node, Element >>> Node.clear_all() >>> Element.clear_all() """ subvars: LinkSequences[LinkSequence] """The subgroup to which the link sequence belongs.""" subseqs: LinkSequences[LinkSequence] """Alias for |LinkSequence.subvars|.""" fastaccess: FastAccessLinkSequence """Object for accessing the link sequence's data with little overhead.""" _CLS_FASTACCESS_PYTHON = FastAccessLinkSequence _isready: bool = False
[docs] def set_pointer(self, double: pointerutils.Double, idx: int = 0) -> None: """Prepare a pointer referencing the given |Double| object. For 1-dimensional sequence objects, one also needs to specify the relevant index position of the pointer via argument `idx`. Method |LinkSequence.set_pointer| should be relevant for framework developers and eventually for some model developers only. """ if self.NDIM == 0: self.fastaccess.set_pointer0d(self.name, double) elif self.NDIM == 1: self.fastaccess.set_pointer1d(self.name, double, idx) self._isready = True
def _finalise_connections(self) -> None: super()._finalise_connections() value = pointerutils.PPDouble() if self.NDIM else None try: self.__hydpy__set_fastaccessattribute__("pointer", value) setattr(self.fastaccess, f"len_{self.name}", 0) except AttributeError: pass def _get_shape(self) -> tuple[int, ...]: """A tuple containing the actual lengths of all dimensions. Property |LinkSequence.shape| of class |LinkSequence| works similarly to the general |Variable.shape| property of class |Variable|. Still, you need to be extra careful due to the pointer mechanism underlying class |LinkSequence|. Change the shape of a link sequence for good reasons only. Please read the main documentation on class |LinkSequence| first and then see the following examples, which are, again, based on the `HydPy-H-Lahn` example project and application model |musk_classic|: >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> element = hp.elements.stream_lahn_marb_lahn_leun >>> model = element.model The default mechanisms of *HydPy* prepare both 0-dimensional and 1-dimensional link sequences with a proper shape (which, for inlet sequence |musk_inlets.Q|, depends on the number of connected |Node| objects): >>> model.sequences.outlets.q.shape () >>> model.sequences.inlets.q.shape (1,) You can use the property |LinkSequence.pointervalue| to investigate whether the link to node sequence connections are as you expect it: >>> from hydpy import round_ >>> element.inlets.lahn_marb.sequences.sim = 1.0 >>> element.outlets.lahn_leun.sequences.sim = 2.0 >>> round_(model.sequences.inlets.q.pointervalue) 1.0 >>> round_(model.sequences.outlets.q.pointervalue) 2.0 Attempting to set the only possible shape of 0-dimensional link sequences or any different shape results in the standard behaviour: >>> model.sequences.outlets.q.shape = () >>> model.sequences.outlets.q.shape = (1,) Traceback (most recent call last): ... ValueError: While trying to set the shape of link sequence`q` of element \ `stream_lahn_marb_lahn_leun`, the following error occurred: The shape information of \ 0-dimensional variables as `q` of element `stream_lahn_marb_lahn_leun` can only \ be `()`, but `(1,)` is given. Changing the shape of 1-dimensional link sequences is supported but destroys the connection to the |NodeSequence| values of the respective nodes. Therefore, the following exception prevents segmentation faults until proper connections are available: >>> model.sequences.inlets.q.shape = (1,) >>> model.update_inlets() >>> model.sequences.inlets.q.shape (1,) >>> model.sequences.inlets.q.shape = 2 >>> model.sequences.inlets.q.pointervalue Traceback (most recent call last): ... RuntimeError: The pointer of the actual `PPDouble` instance at index `0` \ requested, but not prepared yet via `set_pointer`. .. testsetup:: >>> from hydpy import Node, Element >>> Node.clear_all() >>> Element.clear_all() """ return super()._get_shape() def _set_shape(self, shape: int | tuple[int, ...]): try: old_shape = exceptiontools.getattr_(self, "shape", None) super()._set_shape(shape) if (shape != old_shape) and (self.NDIM == 1): self.fastaccess.dealloc_pointer(self.name) self.fastaccess.alloc_pointer(self.name, shape) setattr(self.fastaccess, "len_" + self.name, shape) except BaseException: objecttools.augment_excmessage( f"While trying to set the shape of link sequence" f"{objecttools.elementphrase(self)}" ) shape = propertytools.Property(fget=_get_shape, fset=_set_shape) @property def pointervalue(self) -> float | NDArrayFloat: """The actual value(s) referenced by the pointer(s) of the |LinkSequence| object.""" return self.fastaccess.get_pointervalue(self.name)
[docs] class InletSequence(LinkSequence): """Base class for inlet link sequences of |Model| objects.""" subvars: InletSequences """The subgroup to which the inlet sequence belongs.""" subseqs: InletSequences """Alias for |InletSequence.subvars|."""
[docs] class OutletSequence(LinkSequence): """Base class for outlet link sequences of |Model| objects.""" subvars: OutletSequences """The subgroup to which the outlet sequence belongs.""" subseqs: OutletSequences """Alias for |OutletSequence.subvars|."""
[docs] class ObserverSequence(LinkSequence): """Base class for observer link sequences of |Model| objects.""" subvars: ObserverSequences """The subgroup to which the observer sequence belongs.""" subseqs: ObserverSequences """Alias for |ObserverSequence.subvars|."""
[docs] class ReceiverSequence(LinkSequence): """Base class for receiver link sequences of |Model| objects.""" subvars: ReceiverSequences """The subgroup to which the receiver sequence belongs.""" subseqs: ReceiverSequences """Alias for |ReceiverSequence.subvars|."""
[docs] class SenderSequence(LinkSequence): """Base class for sender link sequences of |Model| objects.""" subvars: SenderSequences """The subgroup to which the sender sequence belongs.""" subseqs: SenderSequences """Alias for |SenderSequence.subvars|."""
[docs] class NodeSequence(IOSequence): """Base class for all sequences to be handled by |Node| objects.""" subvars: NodeSequences """The subgroup to which the node sequence belongs.""" subseqs: NodeSequences """Alias for |NodeSequence.subvars|.""" fastaccess: FastAccessNodeSequence """Object for accessing the node sequence's data with little overhead.""" NDIM: int = 0 NUMERIC: bool = False _CLS_FASTACCESS_PYTHON = FastAccessNodeSequence def __init__(self, subvars: NodeSequences) -> None: super().__init__(subvars) self.subseqs = subvars @property def initinfo(self) -> tuple[pointerutils.Double, bool]: """Return a |Double| instead of a |float| object as the first tuple entry.""" if hydpy.pub.options.usedefaultvalues: return pointerutils.Double(0.0), True return pointerutils.Double(numpy.nan), False @property def descr_sequence(self) -> str: """Description of the |NodeSequence| object, including the |Node.variable| to be represented. >>> from hydpy import Node >>> Node("test_node_1", "T").sequences.sim.descr_sequence 'sim_t' >>> from hydpy import FusedVariable >>> from hydpy.aliases import hland_inputs_T, lland_inputs_TemL >>> Temp = FusedVariable("Temp", hland_inputs_T, lland_inputs_TemL) >>> Node("test_node_2", Temp).sequences.sim.descr_sequence 'sim_temp' .. testsetup:: >>> Node.clear_all() """ return f"{self.name}_{str(self.subseqs.node.variable).lower()}" @property def descr_device(self) -> str: """Description of the |Node| object the |NodeSequence| object belongs to. >>> from hydpy import Node >>> Node("test_node_2").sequences.sim.descr_device 'test_node_2' .. testsetup:: >>> Node.clear_all() """ return self.subseqs.node.name def _finalise_connections(self) -> None: super()._finalise_connections() setattr(self.fastaccess, self.name, pointerutils.Double(0.0)) setattr(self.fastaccess, "_reset_obsdata", False) def _get_value(self): """The actual sequence value. For framework users, the property |NodeSequence.value| of class |NodeSequence| works as usual (explained in the documentation on property |Variable.shape| of class |Variable|). However, framework developers should note that |NodeSequence| objects use |Double| objects for storing their values and making them accessible to |PDouble| and |PPDouble| objects as explained in detail in the documentation on class |LinkSequence|. This mechanism is hidden for framework users via conversions to type |float| for safety reasons: .. testsetup:: >>> from hydpy import Node >>> Node.clear_all() >>> from hydpy import Node >>> sim = Node("node").sequences.sim >>> sim(1.0) >>> sim sim(1.0) >>> sim.value 1.0 >>> sim.fastaccess.sim Double(1.0) >>> sim.value = 2.0 >>> sim sim(2.0) Node sequences return errors like the following if they receive misspecified values or are ill-configured: >>> sim.value = 1.0, 2.0 # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: While trying to assign the value `(1.0, 2.0)` to sequence `sim` of \ node `node`, the following error occurred: float() argument must be a string or a... \ number, not 'tuple' >>> sim.name = None >>> sim.value # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: While trying to query the value of sequence `None` of node `node`, \ the following error occurred: ...attribute name must be string... .. testsetup:: >>> Node.clear_all() """ try: return getattr(self.fastaccess, self.name)[0] except BaseException: objecttools.augment_excmessage( f"While trying to query the value of sequence " f"{objecttools.nodephrase(self)}" ) def _set_value(self, value): try: getattr(self.fastaccess, self.name)[0] = float(value) except BaseException: objecttools.augment_excmessage( f"While trying to assign the value `{value}` to sequence " f"{objecttools.nodephrase(self)}" ) value = property(fget=_get_value, fset=_set_value) @property def seriescomplete(self) -> bool: """True/False flag indicating whether simulated or observed data is fully available or not. We use the observation series of node `dill_assl` of the `HydPy-H-Lahn` project: >>> from hydpy.core.testtools import prepare_full_example_2 >>> hp, pub, TestIO = prepare_full_example_2() >>> obs = hp.nodes.dill_assl.sequences.obs When the sequence does not handle any time series data, |NodeSequence.seriescomplete| is |False|: >>> obs.prepare_series(allocate_ram=False) >>> obs.series Traceback (most recent call last): ... hydpy.core.exceptiontools.AttributeNotReady: Sequence `obs` of node \ `dill_assl` is not requested to make any time series data available. >>> obs.seriescomplete False As long as any time series data is missing, |NodeSequence.seriescomplete| is still |False|: >>> obs.prepare_series() >>> obs.series[:-1] = 1.0 >>> obs.series InfoArray([ 1., 1., 1., nan]) >>> obs.seriescomplete False Only with all data being not |numpy.nan|, |NodeSequence.seriescomplete| is |True|: >>> obs.series[-1] = 1.0 >>> obs.seriescomplete True .. testsetup:: >>> from hydpy import Node, Element >>> Node.clear_all() >>> Element.clear_all() """ return self.memoryflag and not numpy.any(numpy.isnan(self.series))
[docs] class Sim(NodeSequence): """Class for handling those values of |Node| objects that are "simulated", meaning calculated by hydrological models."""
[docs] def load_series(self) -> None: """Read time series data like method |IOSequence.load_series| of class |IOSequence| but with special handling of missing data. The method's "special handling" is to convert errors to warnings. We explain the reasons in the documentation on method |Obs.load_series| of class |Obs|, from which we borrow the following examples. The only differences are that method |Sim.load_series| of class |Sim| does not disable property |IOSequence.memoryflag| and uses the option |Options.warnmissingsimfile| instead of |Options.warnmissingobsfile|: >>> from hydpy.core.testtools import prepare_full_example_1 >>> prepare_full_example_1() >>> from hydpy import HydPy, pub, TestIO >>> hp = HydPy("HydPy-H-Lahn") >>> pub.timegrids = "1996-01-01", "1996-01-06", "1d" >>> with TestIO(): ... hp.prepare_network() ... hp.prepare_models() ... hp.prepare_simseries() >>> sim = hp.nodes.dill_assl.sequences.sim >>> with TestIO(): ... sim.load_series() # doctest: +ELLIPSIS Traceback (most recent call last): ... UserWarning: While trying to load the time series data of sequence `sim` of \ node `dill_assl`, the following error occurred: [Errno 2] No such file or directory: \ '...dill_assl_sim_q.asc' >>> sim.series InfoArray([nan, nan, nan, nan, nan]) >>> sim.series = 1.0 >>> with TestIO(): ... sim.save_series() >>> sim.series = 0.0 >>> with TestIO(): ... sim.load_series() >>> sim.series InfoArray([1., 1., 1., 1., 1.]) >>> import numpy >>> sim.series[2] = numpy.nan >>> with TestIO(), pub.sequencemanager.overwrite(True): ... sim.save_series() >>> with TestIO(): ... sim.load_series() Traceback (most recent call last): ... UserWarning: While trying to load the time series data of sequence `sim` of \ node `dill_assl`, the following error occurred: The series array of sequence `sim` of \ node `dill_assl` contains 1 nan value. >>> sim.series InfoArray([ 1., 1., nan, 1., 1.]) >>> sim.prepare_series(allocate_ram=False) >>> with TestIO(), pub.sequencemanager.filetype("nc"): ... pub.sequencemanager.currentdir = "new_series" ... hp.save_simseries() >>> hp.prepare_simseries() >>> with TestIO(), pub.sequencemanager.filetype("nc"): ... hp.load_simseries() # doctest: +ELLIPSIS Traceback (most recent call last): ... UserWarning: While trying to read data from NetCDF file `...sim_q.nc`, the \ following error occurred: No data for (sub)device `dill_assl` is available in NetCDF \ file `...sim_q.nc`. >>> sim.series = 2.0 >>> with TestIO(), pub.options.warnmissingsimfile(False): ... sim.load_series() >>> sim.series InfoArray([2., 2., 2., 2., 2.]) .. testsetup:: >>> from hydpy import Node, Element >>> Node.clear_all() >>> Element.clear_all() """ try: super().load_series() except BaseException: self.__hydpy__handle_missing_series_error__()
def __hydpy__handle_missing_series_error__(self) -> None: if hydpy.pub.options.warnmissingsimfile: warnings.warn(str(sys.exc_info()[1]))
[docs] class Obs(NodeSequence): """Class for handling those values of |Node| objects that are observed, meaning read from data files."""
[docs] def load_series(self) -> None: """Read time series data like method |IOSequence.load_series| of class |IOSequence| but with special handling of missing data. When reading incomplete time series data, *HydPy* usually raises a |RuntimeError| to prevent from performing erroneous calculations. This functionality makes sense for meteorological input data that is a strict requirement for hydrological simulations. However, the same often does not hold for the time series of |Obs| sequences, e.g. representing measured discharge. Measured discharge is often an optional input or only used for comparison purposes. According to this reasoning, *HydPy* raises (at most) a |UserWarning| in case of missing or incomplete external time series data of |Obs| sequences. The following examples show this based on the `HydPy-H-Lahn` project, mainly focussing on the |Obs| sequence of node `dill_assl`, which is ready for handling time series data at the end of the following steps: >>> from hydpy.core.testtools import prepare_full_example_1 >>> prepare_full_example_1() >>> from hydpy import HydPy, pub, TestIO >>> hp = HydPy("HydPy-H-Lahn") >>> pub.timegrids = "1996-01-01", "1996-01-06", "1d" >>> with TestIO(): ... hp.prepare_network() ... hp.prepare_models() ... hp.prepare_obsseries() >>> obs = hp.nodes.dill_assl.sequences.obs >>> obs.ramflag True Trying to read non-existing data raises the following warning and disables the sequence's ability to handle time series data: >>> import os >>> with TestIO(): ... os.remove(hp.nodes.dill_assl.sequences.obs.filepath) ... hp.load_obsseries() # doctest: +ELLIPSIS Traceback (most recent call last): ... UserWarning: The `memory flag` of sequence `obs` of node `dill_assl` had to \ be set to `False` due to the following problem: While trying to load the time series \ data of sequence `obs` of node `dill_assl`, the following error occurred: [Errno 2] \ No such file or directory: '...dill_assl_obs_q.asc' >>> obs.ramflag False After writing a complete data file, everything works fine: >>> obs.prepare_series() >>> obs.series = 1.0 >>> with TestIO(): ... obs.save_series() >>> obs.series = 0.0 >>> with TestIO(): ... obs.load_series() >>> obs.series InfoArray([1., 1., 1., 1., 1.]) Reading incomplete data also results in a warning message, but does not disable the |IOSequence.memoryflag|: >>> import numpy >>> obs.series[2] = numpy.nan >>> with TestIO(), pub.sequencemanager.overwrite(True): ... obs.save_series() >>> with TestIO(): ... obs.load_series() Traceback (most recent call last): ... UserWarning: While trying to load the time series data of sequence `obs` of \ node `dill_assl`, the following error occurred: The series array of sequence `obs` of \ node `dill_assl` contains 1 nan value. >>> obs.memoryflag True Option |Options.warnmissingobsfile| allows disabling the warning messages without altering the functionalities described above: >>> hp.prepare_obsseries() >>> with TestIO(): ... os.remove(hp.nodes.lahn_marb.sequences.obs.filepath) ... with pub.options.warnmissingobsfile(False): ... hp.load_obsseries() >>> obs.series InfoArray([ 1., 1., nan, 1., 1.]) >>> hp.nodes.lahn_marb.sequences.obs.memoryflag False The same logic holds when trying to load observed time series from NetCDF files that are missing entirely: >>> obs.series = 1.0 >>> with TestIO(), pub.sequencemanager.filetype("nc"): ... pub.sequencemanager.currentdir = "new_series" ... hp.save_obsseries() >>> hp.prepare_obsseries() >>> with TestIO(), pub.sequencemanager.filetype("nc"): ... hp.load_obsseries() # doctest: +ELLIPSIS Traceback (most recent call last): ... UserWarning: While trying to read data from NetCDF file `...obs_q.nc`, the \ following error occurred: The `memory flag` of sequence `obs` of node `lahn_marb` had \ to be set to `False` due to the following problem: No data for (sub)device \ `lahn_marb` is available in NetCDF file `...obs_q.nc`. >>> hp.nodes.lahn_marb.sequences.obs.memoryflag False .. testsetup:: >>> from hydpy import Node, Element >>> Node.clear_all() >>> Element.clear_all() """ try: super().load_series() except BaseException: self.__hydpy__handle_missing_series_error__()
def __hydpy__handle_missing_series_error__(self) -> None: exc_type, exc_value, _ = sys.exc_info() assert exc_type is not None if issubclass(exc_type, OSError): self.__hydpy__set_fastaccessattribute__("ramflag", False) self.__hydpy__set_fastaccessattribute__("diskflag_reading", False) if hydpy.pub.options.warnmissingobsfile: warnings.warn( f"The `memory flag` of sequence {objecttools.nodephrase(self)} " f"had to be set to `False` due to the following problem: " f"{exc_value}" ) else: if hydpy.pub.options.warnmissingobsfile: warnings.warn(str(exc_value))
[docs] class NodeSequences( IOSequences["devicetools.Node", NodeSequence, FastAccessNodeSequence] ): """Base class for handling |Sim| and |Obs| sequence objects. Basically, |NodeSequences| works like the different |ModelSequences| subclasses used for handling |ModelSequence| objects. The main difference is that they do not reference a |Sequences| object (which is only handled by |Element| objects but not by |Node| objects). Instead, they directly reference their master |Node| object via the attribute |NodeSequences.node|: >>> from hydpy import Node >>> node = Node("node") >>> node.sequences.node Node("node", variable="Q") The implemented methods just call the same method of the underlying `fastaccess` attribute, which is an instance of (a Cython extension class of) the Python class |sequencetools.FastAccessNodeSequence|. .. testsetup:: >>> Node.clear_all() """ CLASSES = (Sim, Obs) node: devicetools.Node sim: Sim obs: Obs _cymodel: CyModelProtocol | None _CLS_FASTACCESS_PYTHON = FastAccessNodeSequence def __init__( self, master: devicetools.Node, cls_fastaccess: type[FastAccessNodeSequence] | None = None, cymodel: CyModelProtocol | None = None, ) -> None: self.node = master self._cls_fastaccess = cls_fastaccess self._cymodel = cymodel super().__init__(master) def _init_fastaccess(self) -> None: if hydpy.pub.options.usecython: self.fastaccess = sequenceutils.FastAccessNodeSequence() else: self.fastaccess = self._CLS_FASTACCESS_PYTHON()
[docs] def load_data(self, idx: int) -> None: """Call method |sequencetools.FastAccessNodeSequence.load_data| of the current `fastaccess` attribute. >>> from hydpy import Node, pub >>> with pub.options.usecython(False): ... node = Node("node") >>> from unittest import mock >>> method = "hydpy.core.sequencetools.FastAccessNodeSequence.load_data" >>> with mock.patch(method) as mocked: ... node.sequences.load_data(5) >>> mocked.call_args_list [call(5)] .. testsetup:: >>> Node.clear_all() """ self.fastaccess.load_data(idx)
[docs] def load_simdata(self, idx: int) -> None: """Call method |sequencetools.FastAccessNodeSequence.load_simdata| of the current `fastaccess` attribute. >>> from hydpy import Node, pub >>> with pub.options.usecython(False): ... node = Node("node") >>> from unittest import mock >>> method = "hydpy.core.sequencetools.FastAccessNodeSequence.load_simdata" >>> with mock.patch(method) as mocked: ... node.sequences.load_simdata(5) >>> mocked.call_args_list [call(5)] .. testsetup:: >>> Node.clear_all() """ self.fastaccess.load_simdata(idx)
[docs] def load_obsdata(self, idx: int) -> None: """Call method |sequencetools.FastAccessNodeSequence.load_obsdata| of the current `fastaccess` attribute. >>> from hydpy import Node, pub >>> with pub.options.usecython(False): ... node = Node("node") >>> from unittest import mock >>> method = "hydpy.core.sequencetools.FastAccessNodeSequence.load_obsdata" >>> with mock.patch(method) as mocked: ... node.sequences.load_obsdata(5) >>> mocked.call_args_list [call(5)] .. testsetup:: >>> Node.clear_all() """ self.fastaccess.load_obsdata(idx)
[docs] def save_data(self, idx: int) -> None: """Call method |sequencetools.FastAccessNodeSequence.save_data| of the current `fastaccess` attribute. >>> from hydpy import Node, pub >>> with pub.options.usecython(False): ... node = Node("node") >>> from unittest import mock >>> method = "hydpy.core.sequencetools.FastAccessNodeSequence.save_data" >>> with mock.patch(method) as mocked: ... node.sequences.save_data(5) >>> mocked.call_args_list [call(5)] .. testsetup:: >>> Node.clear_all() """ self.fastaccess.save_data(idx)
[docs] def save_simdata(self, idx: int) -> None: """Call method |sequencetools.FastAccessNodeSequence.save_simdata| of the current `fastaccess` attribute. >>> from hydpy import Node, pub >>> with pub.options.usecython(False): ... node = Node('node') >>> from unittest import mock >>> method = "hydpy.core.sequencetools.FastAccessNodeSequence.save_simdata" >>> with mock.patch(method) as mocked: ... node.sequences.save_simdata(5) >>> mocked.call_args_list [call(5)] .. testsetup:: >>> Node.clear_all() """ self.fastaccess.save_simdata(idx)