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
andElement
objects of the main documentation, neglecting the lakes’ inflow and outflow nodes, which are not relevant for connectingexch_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:
Pic_LoggedWaterLevel_V1
Pic the logged water level from a receiver node.
- The following “run methods” are called in the given sequence during each simulation step:
Update_WaterLevel_V1
Update the factor sequenceWaterLevel
.Calc_DeltaWaterLevel_V1
Calculate the effective difference of both water levels.Calc_PotentialExchange_V1
Calculate the potential exchange that strictly follows the weir formula without taking any other limitations into account.Calc_ActualExchange_V1
Calculate the actual exchange.
- The following “outlet update methods” are called in the given sequence at the end of each simulation step:
Pass_ActualExchange_V1
Pass the actual exchange to an outlet node.
- 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:
CrestHeight()
Crest height [m].CrestWidth()
Crest width [m].FlowCoefficient()
Flow coefficient [-].FlowExponent()
Flow exponent [-].AllowedExchange()
The highest water exchange allowed [m³/s].
- 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:
WaterLevel()
Water level [m].DeltaWaterLevel()
Effective difference of the two water levels [m].
- 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:
PotentialExchange()
The potential bidirectional water exchange [m³/s].ActualExchange()
The actual bidirectional water exchange [m³/s].
- 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:
LoggedWaterLevel()
Logged water level [m].
- 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].