Source code for hydpy.models.manager.manager_derived

# pylint: disable=missing-module-docstring

import itertools

import inflect
import networkx

from hydpy.core import devicetools
from hydpy.core import hydpytools
from hydpy.core import objecttools
from hydpy.core import parametertools
from hydpy.core.typingtools import *
from hydpy.auxs import smoothtools
from hydpy.models.manager import manager_parameters
from hydpy.models.manager import manager_control

# from hydpy.models import manager_lwc   actual import below


[docs] class Seconds(parametertools.SecondsParameter): """Length of the actual simulation step size [s]."""
[docs] class DischargeSmoothPar(parametertools.Parameter): """Smoothing parameter related to |DischargeTolerance| [m³/s].""" NDIM: Final[Literal[0]] = 0 TYPE: Final = float SPAN = (0.0, None) CONTROLPARAMETERS = (manager_control.DischargeTolerance,)
[docs] def update(self) -> None: """Calculate the smoothing parameter value. The documentation on module |smoothtools| explains the following example in detail: >>> from hydpy.models.manager import * >>> parameterstep() >>> dischargetolerance(0.0) >>> derived.dischargesmoothpar.update() >>> from hydpy.cythons.smoothutils import smooth_max1, smooth_min1 >>> from hydpy import round_ >>> round_(smooth_max1(4.0, 1.5, derived.dischargesmoothpar)) 4.0 >>> round_(smooth_min1(4.0, 1.5, derived.dischargesmoothpar)) 1.5 >>> dischargetolerance(2.5) >>> derived.dischargesmoothpar.update() >>> round_(smooth_max1(4.0, 1.5, derived.dischargesmoothpar)) 4.01 >>> round_(smooth_min1(4.0, 1.5, derived.dischargesmoothpar)) 1.49 """ metapar = self.subpars.pars.control.dischargetolerance self(smoothtools.calc_smoothpar_max1(metapar.value))
[docs] class VolumeSmoothPar(manager_parameters.ParameterSource): """Smoothing parameter related to |VolumeTolerance| [m³/s].""" TYPE: Final = float SPAN = (0.0, None) CONTROLPARAMETERS = (manager_control.VolumeTolerance,)
[docs] def update(self) -> None: """Calculate the smoothing parameter value. The documentation on module |smoothtools| explains the following example in detail: >>> from hydpy.models.manager import * >>> parameterstep() >>> sources("a", "b") >>> volumetolerance(0.0, 2.5) >>> derived.volumesmoothpar.update() >>> from hydpy.cythons.smoothutils import smooth_max1, smooth_min1 >>> from hydpy import round_ >>> round_(smooth_max1(4.0, 1.5, derived.volumesmoothpar.values[0])) 4.0 >>> round_(smooth_min1(4.0, 1.5, derived.volumesmoothpar.values[0])) 1.5 >>> round_(smooth_max1(4.0, 1.5, derived.volumesmoothpar.values[1])) 4.01 >>> round_(smooth_min1(4.0, 1.5, derived.volumesmoothpar.values[1])) 1.49 """ self.values = 0.0 values = self.values for i, value in enumerate(self.subpars.pars.control.volumetolerance.values): values[i] = smoothtools.calc_smoothpar_max1(value)
[docs] class MemoryLength(parametertools.NmbParameter): """Number of simulation steps to be covered by some log sequences [-].""" SPAN = (0, None) CONTROLPARAMETERS = (manager_control.TimeDelay, manager_control.TimeWindow)
[docs] def update(self) -> None: """Update the memory length according to :math:`MemoryLength = TimeDelay + TimeWindow`. >>> from hydpy.models.manager import * >>> parameterstep() >>> timedelay(2) >>> timewindow(3) >>> derived.memorylength.update() >>> derived.memorylength memorylength(5) """ control = self.subpars.pars.control self(control.timedelay.value + control.timewindow.value)
[docs] class Adjacency(parametertools.Parameter): """An (incomplete) adjacency matrix of the target node and all source elements [-]. See method |Adjacency.update| for more information. """ NDIM: Final[Literal[2]] = 2 TYPE: Final = bool SPAN = (False, True) def __hydpy__let_par_set_shape__(self, p: parametertools.NmbParameter, /) -> None: if isinstance(p, manager_control.Sources): self.__hydpy__change_shape_if_necessary__((p.value, p.value + 1))
[docs] def update(self) -> None: """Determine a directed subgraph that contains only the target node and all selected source elements. We create the following setting where the sources `d_1` and `d_2` release their water toward the target node `t`, `d_1a` releases its water to `d_1`, `d_2a` and `d_2b` release their water to `d_2`, and `d_2b1` releases its water to `d_2b`: >>> from hydpy import Element, FusedVariable, Node, Nodes >>> from hydpy.aliases import ( ... dam_observers_A, ... dam_states_WaterVolume, ... manager_senders_Request, ... manager_receivers_WaterVolume, ... ) >>> t = Node("t") >>> WaterVolume = FusedVariable( ... "WaterVolume", dam_states_WaterVolume, manager_receivers_WaterVolume ... ) >>> v_1, v_1a, v_2, v_2a, v_2b, v_2b1 = Nodes( ... "v_1", "v_1a", "v_2", "v_2a", "v_2b", "v_2b1", ... defaultvariable=WaterVolume, ... ) >>> Request = FusedVariable("Request", dam_observers_A, manager_senders_Request) >>> r_1, r_1a, r_2, r_2a, r_2b, r_2b1 = Nodes( ... "r_1", "r_1a", "r_2", "r_2a", "r_2b", "r_2b1", defaultvariable=Request, ... ) >>> d_1 = Element("d_1", inlets="q_1a_1", outlets=t, observers=r_1, outputs=v_1) >>> d_1a = Element("d_1a", outlets="q_1a_1", observers=r_1a, outputs=v_1a) >>> d_2 = Element( ... "d_2", inlets=("q_2a_2", "q_2b_2"), ... outlets=t, observers=r_2, outputs=v_2, ... ) >>> d_2a = Element("d_2a", outlets="q_2a_2", observers=r_2a, outputs=v_2a) >>> d_2b = Element( ... "d_2b", inlets="q_2b1_2b", ... outlets="q_2b_2", observers=r_2b, outputs=v_2b, ... ) >>> d_2b1 = Element("d_2b1", outlets="q_2b1_2b", observers=r_2b1, outputs=v_2b1) >>> lwc = Element( ... "lwc", ... receivers=[t, v_1, v_1a, v_2, v_2a, v_2b, v_2b1], ... senders=[r_1, r_1a, r_2, r_2a, r_2b, r_2b1], ... ) Method |Adjacency.update| converts this setting into an adjacency matrix. The first column marks those sources (`d1` and `d_2`) that release their water directly to the target node. The second column marks those sources (`d_1a`) that release their water to the first source (`d1`); the third column marks those sources (none) that release their water to the second source (`d_1a`), and so on. Note that the adjacency matrix is not square because we know that the target node does not release any water towards of the sources, which means we can omit the corresponding (first) row: >>> from hydpy.models.manager_lwc import * >>> parameterstep() >>> sources("d_1", "d_1a", "d_2", "d_2a", "d_2b", "d_2b1") >>> lwc.model = model >>> derived.adjacency.update() >>> derived.adjacency adjacency([[True, False, False, False, False, False, False], [False, True, False, False, False, False, False], [True, False, False, False, False, False, False], [False, False, False, True, False, False, False], [False, False, False, True, False, False, False], [False, False, False, False, False, True, False]]) The adjacency matrix only represents the subgraph of the relevant source elements: >>> sources("d_1a", "d_2", "d_2a", "d_2b1") >>> derived.adjacency.update() >>> derived.adjacency adjacency([[True, False, False, False, False], [True, False, False, False, False], [False, False, True, False, False], [False, False, True, False, False]]) All source elements must, of course, lie upstream of the target node: >>> d_3 = Element("d_3", outlets="q_3") >>> sources("d_1", "d_2", "d_3") >>> derived.adjacency.update() Traceback (most recent call last): ... RuntimeError: While trying to update parameter `adjacency` of element `lwc`, \ the following error occurred: There are zero paths between the source element `d_3` \ and the target node `t`, but there must be exactly one. A branching of the river network above the target node can mean trouble. |Adjacency.update| thus searches for multiple paths between the target node and all source elements (this strategy might not cover all problematic cases and might also complain about some unproblematic ones - we might improve the algorithm later): >>> sources("d_1", "d_1a", "d_2", "d_2a", "d_2b", "d_2b1") >>> d_1a.outlets.add_device("b", force=True) >>> d_2a.inlets.add_device("b", force=True) >>> derived.adjacency.update() Traceback (most recent call last): ... RuntimeError: While trying to update parameter `adjacency` of element `lwc`, \ the following error occurred: There are two paths between the source element `d_1a` \ and the target node `t`, but there must be exactly one. """ try: registry = devicetools._registry # pylint: disable=protected-access _nodes = tuple(registry[devicetools.Node].values()) nodes = devicetools.Nodes(_nodes) # type: ignore[arg-type] _elements = tuple(registry[devicetools.Element].values()) elements = devicetools.Elements(_elements) # type: ignore[arg-type] graph = hydpytools.create_directedgraph(nodes=nodes, elements=elements) sources = self.subpars.pars.control.sources name2element = {n: elements[n] for n in sources.sourcenames} target = self._target subgraph = networkx.DiGraph() subgraph.add_node(target) subgraph.add_nodes_from(name2element.values()) def _fill_subgraph( downstream: devicetools.NodeOrElement, upstream: devicetools.Element, check: bool, ) -> None: p = networkx.all_simple_paths(graph, target=downstream, source=upstream) paths = tuple(p) n = len(paths) if check: if n != 1: e = inflect.engine() number = e.number_to_words(n) # type: ignore[arg-type] raise RuntimeError( f"There {e.plural('is', n)} {number} " f"{e.plural('path', n)} between the source element " f"`{upstream}` and the target node `{target}`, but there " f"must be exactly one." ) if n > 0: if all(d.name not in name2element for d in paths[0][1:-1]): subgraph.add_edge(upstream, downstream) for source in name2element.values(): _fill_subgraph(upstream=source, downstream=target, check=True) for source, subtarget in itertools.permutations(name2element.values(), 2): _fill_subgraph(upstream=source, downstream=subtarget, check=False) self.value = networkx.to_numpy_array( subgraph, nodelist=(target,) + tuple(name2element.values()), dtype=bool )[1:, :] except BaseException: objecttools.augment_excmessage( f"While trying to update parameter {objecttools.elementphrase(self)}" )
@property def _target(self) -> devicetools.Node: receivers = self.subpars.pars.model.element.receivers potential_targets = [r for r in receivers if r.variable == "Q"] assert len(potential_targets) == 1 return potential_targets[0]
[docs] class Order(parametertools.Parameter): """The processing order of all source elements [-]. See method |Order.update| for more information. """ NDIM: Final[Literal[1]] = 1 TYPE: Final = int SPAN = (0, None) def __hydpy__let_par_set_shape__(self, p: parametertools.NmbParameter, /) -> None: if isinstance(p, manager_control.Sources): self.__hydpy__change_shape_if_necessary__((p.value,))
[docs] def update(self) -> None: """Determine the processing order based on a (reversed) topological sort. The following order ensures we do not process a source element before we have processed its downstream source elements: >>> from hydpy.models.manager import * >>> parameterstep() >>> sources("d_1", "d_1a", "d_2", "d_2a", "d_2b", "d_2b1") >>> derived.adjacency([[True, False, False, False, False, False, False], ... [False, True, False, False, False, False, False], ... [True, False, False, False, False, False, False], ... [False, False, False, True, False, False, False], ... [False, False, False, True, False, False, False], ... [False, False, False, False, False, True, False]]) >>> derived.order.update() >>> derived.order order(2, 4, 0, 5, 3, 1) """ subgraph = networkx.from_numpy_array( self.subpars.adjacency.values[:, 1:], create_using=networkx.DiGraph ) self.values = tuple(networkx.topological_sort(subgraph))[::-1]