exch_v001

Bidirectional water exchange over a weir.

Version 1 of HydPy-Exch implements the general weir formula. We implemented it on behalf of the German Federal Institute of Hydrology (BfG) to connect different dam_v006 instances (lake models), enabling them to exchange water based on water level differences. This specific combination serves to model some huge, connected (sub)lakes of the Rhine basin similar to HBV96 (Lindström et al., 1997). Combinations with other models providing (something like) water level information and allowing for an additional inflow that can be positive and negative are possible.

Integration tests

Note

When new to HydPy, consider reading section How to understand integration tests? first.

We perform all integration tests over a month with a simulation step of one day:

>>> from hydpy import Element, FusedVariable, Nodes, PPoly, prepare_model, pub
>>> pub.timegrids = "2000-01-01", "2000-02-01", "1d"

The following examples demonstrate how exch_v001 interacts with lake models like dam_v006. Therefore, we must set up one exch_v001 instance and two dam_v006 instances.

First, we define the eight required Node objects:

  • inflow1 and inflow2 pass the inflow into the first and the second lake.

  • outflow1 and outflow2 receive the lakes’ outflows.

  • overflow1 and overflow2 exchange water between the lakes.

  • waterlevel1 and waterlevel2 inform the exchange model about the lakes’ current water levels.

We define the variable type of all nodes explicitly. For the inflow and outflow nodes, we stick to the default by using the string literal “Q”, telling dam_v006 to connect these nodes to the inlet sequence Q and outlet sequence Q, respectively:

>>> inflow1, inflow2  = Nodes("inflow1", "inflow2", defaultvariable="Q")
>>> outflow1, outflow2  = Nodes("outflow1", "outflow2", defaultvariable="Q")

The overflow nodes do not connect both lakes directly but the lakes with the exchange model. Still, we can use a single string literal (“E”) because the exchange-related inlet sequence of dam_v006 (E) and the only outlet sequence of exch_v001 (E) have the same name:

>>> overflow1, overflow2 = Nodes("overflow1", "overflow2", defaultvariable="E")

The water level nodes require a little more effort in first defining a FusedVariable. This fused variable combines the string literal “L” (telling exch_v001 to connect both nodes to the receiver sequence L) and the alias of output sequence WaterLevel of dam_v006:

>>> from hydpy.outputs import dam_WaterLevel
>>> WaterLevel = FusedVariable("L", dam_WaterLevel)
>>> waterlevel1, waterlevel2 = Nodes("waterlevel1", "waterlevel2", defaultvariable=WaterLevel)

Now we prepare the two Element objects holding the dam_v006 instances. The configuration is similar to the one in the documentation on dam_v006, except in connecting waterlevel1 and waterlevel2 as additional output nodes:

>>> lake1 = Element("lake1",
...                 inlets=(inflow1, overflow1),
...                 outlets=outflow1,
...                 outputs=waterlevel1)
>>> lake2 = Element("lake2",
...                 inlets=(inflow2, overflow2),
...                 outlets=outflow2,
...                 outputs=waterlevel2)

From the perspective of the exchange element, waterlevel1 and waterlevel2 are receiver nodes, while overflow1 and overflow2 are outlet nodes. At the beginning of each simulation step, exch_v001 receives water level information from both lakes. Then, it calculates the correct exchange and sends it to both lakes via the overflow nodes, but with different signs. If the first lake’s water level is higher, it passes a negative value to overflow1 (the first lake loses water) and a positive value to overflow2 (the second lake gains water), and vice versa:

>>> exchange = Element("exchange",
...                    receivers=(waterlevel1, waterlevel2),
...                    outlets=(overflow1, overflow2))

In our test configuration, both the nodes’ names and the order in which we give them to the constructor of class Element agree with the nodes’ target lakes. This practice seems advisable for keeping clarity, but it is not a technical requirement. The documentation on class Model explains the internal sorting mechanisms and plausibility checks underlying the connection-related functionalities of exch_v001.

We parameterise both lake models identically. All of the following values stem from the documentation on dam_v006. We will use them in all examples:

>>> lake1.model = prepare_model("dam_v006")
>>> lake2.model = prepare_model("dam_v006")
>>> from numpy import inf
>>> for model_ in (lake1.model, lake2.model):
...     control = model_.parameters.control
...     control.catchmentarea(86.4)
...     control.surfacearea(1.44)
...     control.correctionprecipitation(1.2)
...     control.correctionevaporation(1.2)
...     control.weightevaporation(0.8)
...     control.thresholdevaporation(0.0)
...     control.dischargetolerance(0.1)
...     control.toleranceevaporation(0.001)
...     control.allowedwaterleveldrop(inf)
...     control.watervolume2waterlevel(PPoly.from_data(xs=[0.0, 1.0], ys=[0.0, 1.0]))
...     control.pars.update()

Now we prepare the exchange model. We will use common values for the flow coefficient and exponent throughout the following examples:

>>> from hydpy.models.exch_v001 import *
>>> parameterstep("1d")
>>> flowcoefficient(0.62)
>>> flowexponent(1.5)
>>> exchange.model = model

An IntegrationTest object will help us to perform the individual examples:

>>> from hydpy.core.testtools import IntegrationTest
>>> test = IntegrationTest(exchange)
>>> test.plotting_options.axis1 = (factors.waterlevel,)
>>> test.plotting_options.axis2 = (fluxes.potentialexchange, fluxes.actualexchange)

For simplicity, we set both lakes’ inflow, precipitation, and evaporation to zero:

>>> inflow1.sequences.sim.series = 0.0
>>> inflow2.sequences.sim.series = 0.0
>>> for model_ in (lake1.model, lake2.model):
...     inputs = model_.sequences.inputs
...     for seq in (inputs.precipitation, inputs.evaporation):
...         seq.prepare_series()
...         seq.series = 0.0

The only difference between both lakes is their initial state. The first lake starts empty, and the second lake starts with a water volume of 1 million m³. Note that exch_v001 requires the same information. We must give it to the log sequence LoggedWaterLevel:

>>> test.inits = [(lake1.model.sequences.states.watervolume, 0.0),
...               (lake1.model.sequences.logs.loggedadjustedevaporation, 0.0),
...               (lake2.model.sequences.states.watervolume, 1.0),
...               (lake2.model.sequences.logs.loggedadjustedevaporation, 0.0),
...               (logs.loggedwaterlevel, (0.0, 1.0))]

base scenario

Our base scenario defines a small crest width of 0.2 meters, enabling only limited water exchange:

>>> crestwidth(0.2)

The crest height of 0.0 m and the allowed exchange of 5.0 m³/s are so low and high, respectively, that they do not affect the following results:

>>> crestheight(0.0)
>>> allowedexchange(5.0)

We define a linear relationship between the water level and the outflow for both lakes:

>>> for model_ in (lake1.model, lake2.model):
...     model_.parameters.control.waterlevel2flooddischarge(
...         PPoly.from_data(xs=[0.0, 1.0], ys=[0.0, 2.0]))

The following results show that the first lake’s water level drops fast due to releasing water to the second lake and its outlet. The second lake receives this overflow through the whole simulation period but with a decreasing tendency. Hence, the water level rises initially but then falls again because of the lake’s outflow:

>>> test("exch_v001_base_scenario")
Click to see the table
Click to see the graph

crest height

In this example, we increase the crest’s width and, more importantly, set its height to 0.5 m, matching a water volume of 0.5 million m³:

>>> crestwidth(20.0)
>>> crestheight(0.5)

The crest height now influences the lakes’ interaction significantly. For clarification, we disallow both lakes to release any water:

>>> for model_ in (lake1.model, lake2.model):
...     model_.parameters.control.waterlevel2flooddischarge(
...         PPoly.from_data(xs=[0.0], ys=[0.0]))

Due to the identical parameter values of both models and the symmetrical initial conditions of 0.0 and 1.0 million m³, both water levels move towards the defined crest height asymptotically:

>>> test("exch_v001_crest_height")
Click to see the table
Click to see the graph

numerical accuracy

exch_v001 is a very flexible tool but requires the user to apply it wisely. One crucial aspect is numerical accuracy. One can expect sufficiently accurate results only if the simulation step size is relatively short compared to water level dynamics. In this example, we illustrate what happens if there is too much exchange due to a large crest width:

>>> crestwidth(200.0)

We see substantial numerical oscillations in the results. Due to the stiffness of the underlying system of differential equations, a further increase of the crest width would even result in a numerical overflow error that might be hard to trace back in a real-world application:

>>> test("exch_v001_numerical_accuracy")
Click to see the table
Click to see the graph

allowed exchange

Sometimes there might be hydrological reasons to limit the water exchange. Still, here we use the related parameter AllowedExchange only as a stop-gap for stabilising simulation results affected by numerical instability by setting its value to 2.0 m³/s:

>>> allowedexchange(2.0)

The results are far from perfect (the initial water levels change too slowly and still oscillate for a few days) but are at least stable and not overly wrong:

>>> test("exch_v001_allowed_exchange")
Click to see the table
Click to see the graph
class hydpy.models.exch_v001.Model[source]

Bases: AdHocModel

Version 1 of the HydPy-Exch.

Before continuing, please first read the general documentation on application model exch_v001.

To work correctly, each exch_v001 must know which water level node and which overflow node belong to the same lake model. The following examples might provide insight into how we deal with this issue but are merely there for testing that we handle all expected cases well.

We recreate the configuration of the Node and Element objects of the main documentation, neglecting the lakes’ inflow and outflow nodes, which are not relevant for connecting exch_v001:

>>> from hydpy.models.exch_v001 import *
>>> parameterstep()
>>> from hydpy import Element, FusedVariable, Node, Nodes
>>> from hydpy.outputs import dam_WaterLevel
>>> WaterLevel = FusedVariable("L", dam_WaterLevel)
>>> Element.clear_all()
>>> Node.clear_all()
>>> overflow1, overflow2 = Nodes("overflow1", "overflow2", defaultvariable="E")
>>> waterlevel1, waterlevel2 = Nodes("waterlevel1", "waterlevel2", defaultvariable=WaterLevel)
>>> lake1 = Element("lake1", inlets=overflow1, outputs=waterlevel1)
>>> lake2 = Element("lake2", inlets=overflow2, outputs=waterlevel2)
>>> exchange = Element("exchange",
...                    receivers=(waterlevel1, waterlevel2),
...                    outlets=(overflow1, overflow2))
>>> exchange.model = model

The water levels of lake1 and lake2 are available via the first and the second entry of the receiver sequence L, respectively:

>>> waterlevel1.sequences.sim = 1.0
>>> waterlevel2.sequences.sim = 2.0
>>> receivers.l
l(1.0, 2.0)

Likewise, the first and the second entry of the outlet sequence E are available to the overflow nodes lake1 and lake2, respectively:

>>> outlets.e = 3.0, 4.0
>>> overflow1.sequences.sim
sim(3.0)
>>> overflow2.sequences.sim
sim(4.0)

We recreate this configuration multiple times, each time changing one aspect (marked by exclamation marks). First, we connect node waterlevel2 with èlement lake1 and node waterlevel1 with element lake2:

>>> Element.clear_all()
>>> Node.clear_all()
>>> overflow1, overflow2 = Nodes("overflow1", "overflow2", defaultvariable="E")
>>> waterlevel1, waterlevel2 = Nodes("waterlevel1", "waterlevel2", defaultvariable=WaterLevel)
>>> lake1 = Element("lake1", inlets=overflow1, outputs=waterlevel2)  # !!!
>>> lake2 = Element("lake2", inlets=overflow2, outputs=waterlevel1)  # !!!
>>> exchange = Element("exchange",
...                    receivers=(waterlevel1, waterlevel2),
...                    outlets=(overflow1, overflow2))
>>> exchange.model = model

Due to this swap, the first of the outlet sequence E connects to node overflow2 and the second one to node overflow1:

>>> waterlevel1.sequences.sim = 1.0
>>> waterlevel2.sequences.sim = 2.0
>>> receivers.l
l(1.0, 2.0)
>>> outlets.e = 3.0, 4.0
>>> overflow1.sequences.sim
sim(4.0)
>>> overflow2.sequences.sim
sim(3.0)

Swapping the nodes overflow1 and overflow2 instead of waterlevel1 and waterlevel2 leads to the same results (we arbitrarily decided to ground the internal sorting on the alphabetical order of the receiver nodes’ names):

>>> Element.clear_all()
>>> Node.clear_all()
>>> overflow1, overflow2 = Nodes("overflow1", "overflow2", defaultvariable="E")
>>> waterlevel1, waterlevel2 = Nodes("waterlevel1", "waterlevel2", defaultvariable=WaterLevel)
>>> lake1 = Element("lake1", inlets=overflow2, outputs=waterlevel1)  # !!!
>>> lake2 = Element("lake2", inlets=overflow1, outputs=waterlevel2)  # !!!
>>> exchange = Element("exchange",
...                    receivers=(waterlevel1, waterlevel2),
...                    outlets=(overflow1, overflow2))
>>> exchange.model = model
>>> waterlevel1.sequences.sim = 1.0
>>> waterlevel2.sequences.sim = 2.0
>>> receivers.l
l(1.0, 2.0)
>>> outlets.e = 3.0, 4.0
>>> overflow1.sequences.sim
sim(4.0)
>>> overflow2.sequences.sim
sim(3.0)

Now we (accidentally) connect node waterlevel2 to both lakes. Therefore, exch_v001 cannot find a water level node connected to the same lake model as outlet node overflow1:

>>> Element.clear_all()
>>> Node.clear_all()
>>> overflow1, overflow2 = Nodes("overflow1", "overflow2", defaultvariable="E")
>>> waterlevel1, waterlevel2 = Nodes("waterlevel1", "waterlevel2", defaultvariable=WaterLevel)
>>> lake1 = Element("lake1", inlets=overflow1, outputs=waterlevel2)  # !!!
>>> lake2 = Element("lake2", inlets=overflow2, outputs=waterlevel2)
>>> exchange = Element("exchange",
...                    receivers=(waterlevel1, waterlevel2),
...                    outlets=(overflow1, overflow2))
>>> exchange.model = model
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `outlet` sequences of the model handled by element `exchange`, the following error occurred: Outlet node `overflow1` does not correspond to any available receiver node.

exch_v001 raises the following error if there are not precisely two water level nodes available:

>>> Element.clear_all()
>>> Node.clear_all()
>>> overflow1, overflow2 = Nodes("overflow1", "overflow2", defaultvariable="E")
>>> waterlevel1, waterlevel2 = Nodes("waterlevel1", "waterlevel2", defaultvariable=WaterLevel)
>>> lake1 = Element("lake1", inlets=overflow1, outputs=waterlevel1)
>>> lake2 = Element("lake2", inlets=overflow2, outputs=waterlevel2)
>>> exchange = Element("exchange",
...                    receivers=waterlevel1,  # !!!
...                    outlets=(overflow1, overflow2))
>>> exchange.model = model
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `receiver` sequences of the model handled by element `exchange`, the following error occurred: There must be exactly 2 outlet receiver but the following `1` receiver nodes are defined: waterlevel1.

Correspondingly, exch_v001 raises the following error if there are not precisely two overflow nodes available:

>>> Element.clear_all()
>>> Node.clear_all()
>>> overflow1, overflow2 = Nodes("overflow1", "overflow2", defaultvariable="E")
>>> waterlevel1, waterlevel2 = Nodes("waterlevel1", "waterlevel2", defaultvariable=WaterLevel)
>>> lake1 = Element("lake1", inlets=overflow1, outputs=waterlevel1)
>>> lake2 = Element("lake2", inlets=overflow2, outputs=waterlevel2)
>>> exchange = Element("exchange",
...                    receivers=(waterlevel1, waterlevel2),
...                    outlets=(overflow1, overflow2, waterlevel2))  # !!!
>>> exchange.model = model
Traceback (most recent call last):
...
RuntimeError: While trying to build the node connection of the `outlet` sequences of the model handled by element `exchange`, the following error occurred: There must be exactly 2 outlet nodes but the following `3` outlet nodes are defined: overflow1, overflow2, and waterlevel2.
The following “receiver update methods” are called in the given sequence before performing a simulation step:
The following “run methods” are called in the given sequence during each simulation step:
The following “outlet update methods” are called in the given sequence at the end of each simulation step:
class hydpy.models.exch_v001.ControlParameters(master: Parameters, cls_fastaccess: Type[FastAccessParameter] | None = None, cymodel: CyModelProtocol | None = None)

Bases: SubParameters

Control parameters of model exch_v001.

The following classes are selected:
class hydpy.models.exch_v001.FactorSequences(master: Sequences, cls_fastaccess: Type[TypeFastAccess_co] | None = None, cymodel: CyModelProtocol | None = None)

Bases: FactorSequences

Factor sequences of model exch_v001.

The following classes are selected:
class hydpy.models.exch_v001.FluxSequences(master: Sequences, cls_fastaccess: Type[TypeFastAccess_co] | None = None, cymodel: CyModelProtocol | None = None)

Bases: FluxSequences

Flux sequences of model exch_v001.

The following classes are selected:
class hydpy.models.exch_v001.LogSequences(master: Sequences, cls_fastaccess: Type[TypeFastAccess_co] | None = None, cymodel: CyModelProtocol | None = None)

Bases: LogSequences

Log sequences of model exch_v001.

The following classes are selected:
class hydpy.models.exch_v001.OutletSequences(master: Sequences, cls_fastaccess: Type[TypeFastAccess_co] | None = None, cymodel: CyModelProtocol | None = None)

Bases: OutletSequences

Outlet sequences of model exch_v001.

The following classes are selected:
  • E() Bidirectional water exchange [m³].

class hydpy.models.exch_v001.ReceiverSequences(master: Sequences, cls_fastaccess: Type[TypeFastAccess_co] | None = None, cymodel: CyModelProtocol | None = None)

Bases: ReceiverSequences

Receiver sequences of model exch_v001.

The following classes are selected:
  • L() Water level [m].