# -*- coding: utf-8 -*-
# pylint: disable=missing-module-docstring
# import...
# ...from standard-library
from typing import *
# ...from site-packages
import numpy
# ...from HydPy
from hydpy.core import objecttools
from hydpy.core import parametertools
from hydpy.core.typingtools import *
[docs]
class Responses(parametertools.Parameter):
"""Assigns different ARMA models to different discharge thresholds.
Parameter |Responses| is not involved in the actual calculations
during the simulation run. Instead, it is thought for the intuitive
handling of different ARMA models. It can be applied as follows.
Initially, each new `responses` object is emtpy:
>>> from hydpy.models.arma import *
>>> parameterstep()
>>> responses
responses()
One can assign ARMA models as attributes to it:
>>> responses.th_0_0 = ((1.0, 2.0), (3.0, 4.0, 6.0))
`th_0_0` stands for a threshold discharge value of 0.0 m³/s, which the
given ARMA model corresponds to. For integer discharge values, one can
omit the decimal digit:
>>> responses.th_1 = ((), (7.0,))
One can also omit the leading letters, but not the underscore:
>>> responses.th_2_5 = ([8.0], range(9, 20))
Internally, all threshold keys are brought into the standard format:
>>> responses
responses(th_0_0=((1.0, 2.0),
(3.0, 4.0, 6.0)),
th_1_0=((),
(7.0,)),
th_2_5=((8.0,),
(9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0,
18.0, 19.0)))
All ARMA models are available via attribute access and their attribute
names are made available to function |dir|:
>>> "th_1_0" in dir(responses)
True
Note that all iterables containing the AR and MA coefficients are
converted to tuples, to prevent them from being changed by accident:
>>> responses.th_1[1][0]
7.0
>>> responses.th_1_0[1][0] = 77.0
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
Instead, one can delete and or overwrite existing ARMA models:
>>> del responses.th_2_5
>>> responses.th_1 = ((), (77.0,))
>>> responses
responses(th_0_0=((1.0, 2.0),
(3.0, 4.0, 6.0)),
th_1_0=((),
(77.0,)))
Names that cannot be identified as threshold values result in an exception:
>>> responses.test = ((), ())
Traceback (most recent call last):
...
AttributeError: To define different response functions for parameter \
`responses` of element `?`, one has to pass them as keyword arguments or \
set them as additional attributes. The used name must meet a specific \
format (see the documentation for further information). The given name \
`test` does not meet this format.
Suitable get-related attribute exceptions are also implemented:
>>> responses.test
Traceback (most recent call last):
...
AttributeError: Parameter `responses` of element `?` does not have \
an attribute named `test` and the name `test` is also not a valid \
threshold value identifier.
>>> responses._0_1
Traceback (most recent call last):
...
AttributeError: Parameter `responses` of element `?` does not have an attribute \
named `_0_1` nor an arma model corresponding to a threshold value named `th_0_1`.
The above examples show that all AR and MA coefficients are converted to
floating point values. It this is not possible or something else goes
totally wrong during the definition of a new ARMA model, errors like the
following are raised:
>>> responses.th_10 = ()
Traceback (most recent call last):
...
IndexError: While trying to set a new threshold (th_10) coefficient \
pair for parameter `responses` of element `?`, the following error \
occurred: tuple index out of range
Except for the mentioned conversion to floating point values, there are
no plausibility checks performed. You have to use other tools to gain
plausible coefficients. The HydPy framework offers the module
|iuhtools| for such purposes.
Prepare one instantaneous unit hydrograph (iuh) based on the
Translation Diffusion Equation and another one based on the Linear
Storage Cascade:
>>> from hydpy.auxs.iuhtools import TranslationDiffusionEquation
>>> tde = TranslationDiffusionEquation(d=5.0, u=2.0, x=4.0)
>>> from hydpy.auxs.iuhtools import LinearStorageCascade
>>> lsc = LinearStorageCascade(n=2.5, k=1.0)
The following line deletes the coefficients defined above and assigns the
ARMA approximations of both iuh models:
>>> responses(lsc, _2=tde)
One can change the parameter values of the translation diffusion iuh and
assign it to the `responses` parameter, without affecting the ARMA
coefficients of the first tde parametrization:
>>> tde.u = 1.0
>>> responses._5 = tde
>>> responses
responses(th_0_0=((1.001744, -0.32693, 0.034286),
(0.050456, 0.199156, 0.04631, -0.004812, -0.00021)),
th_2_0=((2.028483, -1.447371, 0.420257, -0.039595, -0.000275),
(0.165732, 0.061819, -0.377523, 0.215754, -0.024597,
-0.002684)),
th_5_0=((3.032315, -3.506645, 1.908546, -0.479333, 0.042839,
0.00009),
(0.119252, -0.054959, -0.342744, 0.433585, -0.169102,
0.014189, 0.001967)))
One may have noted the Linear Storage Cascade model was passed as
a positional argument and was assigned to a treshold value of 0.0 m³/s
automatically, which is the default value. As each treshold value has to
be unique, one can pass only one positional argument:
>>> responses(tde, lsc)
Traceback (most recent call last):
...
ValueError: For parameter `responses` of element `?` at most one \
positional argument is allowed, but `2` are given.
Checks for the repeated definition of the same threshold values are also
performed:
>>> responses(tde, _0=lsc, _1=tde, _1_0=lsc)
Traceback (most recent call last):
...
ValueError: For parameter `responses` of element `?` `4` arguments \
have been given but only `2` response functions could be prepared. \
Most probably, you defined the same threshold value(s) twice.
The number of response functions and the number of the respective AR and
MA coefficients of a given `responses` parameter can be easily queried:
>>> responses(_0=((1.0, 2.0),
... (3.0, 4.0, 6.0)),
... _1=((),
... (7.0,)))
>>> len(responses)
2
>>> responses.ar_orders
(2, 0)
>>> responses.ma_orders
(3, 1)
The threshold values and AR coefficients and the MA coefficients can all
be queried as numpy arrays:
>>> responses.thresholds
array([0., 1.])
>>> responses.ar_coefs
array([[ 1., 2.],
[nan, nan]])
>>> responses.ma_coefs
array([[ 3., 4., 6.],
[ 7., nan, nan]])
Technical notes:
The implementation of this class is much to tricky for subpackage `models`.
It should be generalized and moved to the framework core later.
Furthermore, it would be nice to avoid the `nan` values in the coefficent
representations. But this would possibly require to define a specialized
`arrays in list` type in Cython.
"""
_coefs: Dict[str, Tuple[Vector[float], Vector[float]]]
NDIM, TYPE, TIME, SPAN = 0, float, None, (None, None)
def __init__(self, subvars: parametertools.SubParameters) -> None:
with objecttools.ResetAttrFuncs(self):
super().__init__(subvars)
self.fastaccess = None
self._coefs = {}
def __hydpy__connect_variable2subgroup__(self) -> None:
"""Do nothing due to the reasons explained in the main
documentation on class |Responses|."""
def __call__(self, *args, **kwargs) -> None:
self._coefs.clear()
if len(args) > 1:
raise ValueError(
f"For parameter {objecttools.elementphrase(self)} at most one "
f"positional argument is allowed, but `{len(args)}` are given."
)
for key, value in kwargs.items():
setattr(self, key, value)
if len(args) == 1:
setattr(self, "th_0_0", args[0])
if len(args) + len(kwargs) != len(self):
raise ValueError(
f"For parameter `{self.name}` of element "
f"`{objecttools.devicename(self.subpars)}` "
f"`{len(args)+len(kwargs)}` arguments have been given "
f"but only `{len(self)}` response functions could be "
f"prepared. Most probably, you defined the same "
f"threshold value(s) twice."
)
def __getattr__(self, key: str) -> Tuple[Vector[float], Vector[float]]:
try:
std_key = self._standardize_key(key)
except AttributeError as exc:
raise AttributeError(
f"Parameter {objecttools.elementphrase(self)} does not have an "
f"attribute named `{key}` and the name `{key}` is also not a valid "
f"threshold value identifier."
) from exc
if std_key in self._coefs:
return self._coefs[std_key]
raise AttributeError(
f"Parameter {objecttools.elementphrase(self)} does not have an attribute "
f"named `{key}` nor an arma model corresponding to a threshold value "
f"named `{std_key}`."
)
def __setattr__(self, key: str, value: object) -> None:
if hasattr(self, key) and not key.startswith("th_"):
object.__setattr__(self, key, value)
else:
std_key = self._standardize_key(key)
try:
try:
self._coefs[std_key] = value.arma.coefs
except AttributeError:
self._coefs[std_key] = (
tuple(float(v) for v in value[0]),
tuple(float(v) for v in value[1]),
)
except BaseException:
objecttools.augment_excmessage(
f"While trying to set a new threshold ({key}) coefficient pair "
f"for parameter {objecttools.elementphrase(self)}"
)
def __delattr__(self, key: str) -> None:
std_key = self._standardize_key(key)
if std_key in self._coefs:
del self._coefs[std_key]
def _standardize_key(self, key: str) -> str:
try:
tuple_ = str(key).split("_")
if (len(tuple_) > 1) and tuple_[-2].isdigit():
integer = int(tuple_[-2])
decimal = int(tuple_[-1])
else:
integer = int(tuple_[-1])
decimal = 0
return "_".join(("th", str(integer), str(decimal)))
except BaseException as exc:
raise AttributeError(
f"To define different response functions for parameter "
f"{objecttools.elementphrase(self)}, one has to pass them as keyword "
f"arguments or set them as additional attributes. The used name must "
f"meet a specific format (see the documentation for further "
f"information). The given name `{key}` does not meet this format."
) from exc
@property
def thresholds(self) -> Vector[float]:
"""Threshold values of the response functions."""
return numpy.array(
sorted(self._key2float(key) for key in self._coefs), dtype=float
)
@staticmethod
def _key2float(key: str) -> float:
return float(key[3:].replace("_", "."))
def _get_orders(self, index: int) -> Tuple[int, ...]:
orders = []
for _, coefs in self:
orders.append(len(coefs[index]))
return tuple(orders)
@property
def ar_orders(self) -> Tuple[int, ...]:
"""Number of AR coefficients of the different response functions."""
return self._get_orders(0)
@property
def ma_orders(self) -> Tuple[int, ...]:
"""Number of MA coefficients of the different response functions."""
return self._get_orders(1)
def _get_coefs(self, index: int) -> Matrix[float]:
orders = self._get_orders(index)
max_orders = max(orders) if orders else 0
coefs = numpy.full((len(self), max_orders), numpy.nan)
for idx, (order, (_, coef)) in enumerate(zip(orders, self)):
coefs[idx, :order] = coef[index]
return coefs
@property
def ar_coefs(self) -> Matrix[float]:
"""AR coefficients of the different response functions.
The first row contains the AR coefficients related to the the smallest
threshold value, the last row contains the AR coefficients related to
the highest threshold value. The number of columns depend on the
highest number of AR coefficients among all response functions."""
return self._get_coefs(0)
@property
def ma_coefs(self) -> Matrix[float]:
"""AR coefficients of the different response functions.
The first row contains the MA coefficients related to the the smallest
threshold value, the last row contains the AR coefficients related to
the highest threshold value. The number of columns depend on the
highest number of MA coefficients among all response functions."""
return self._get_coefs(1)
def __len__(self) -> int:
return len(self._coefs)
def __bool__(self) -> bool:
return len(self._coefs) > 0
def __iter__(self) -> Iterator[Tuple[str, Tuple[Vector[float], Vector[float]]]]:
for key in sorted(self._coefs.keys(), key=self._key2float):
yield key, self._coefs[key]
def __repr__(self) -> str:
strings = self.commentrepr
prefix = f"{self.name}("
blanks = " " * len(prefix)
if self:
for idx, (th, coefs) in enumerate(self):
subprefix = f"{prefix}{th}=" if idx == 0 else f"{blanks}{th}="
strings.append(
objecttools.assignrepr_tuple2(coefs, subprefix, 75) + ","
)
strings[-1] = strings[-1][:-1] + ")"
else:
strings.append(prefix + ")")
return "\n".join(strings)
def __dir__(self) -> List[str]:
return cast(List[str], super().__dir__()) + list(self._coefs.keys())