HydPy-SW1D-Channel ("user model" for preparing single channels that will be combined and solved by HydPy-SW1D-Network)

The HydPy-SW1D model family member sw1d_channel allows combining different storage and routing submodels for representing the 1-dimensional flow processes within a single channel reach.

sw1d_channel is a “user model” whose task lies seldom in performing actual calculations but merely in specifying individual reaches of large channel networks on an Element basis, even if they must be solved later simultaneously (by the “composite model” sw1d_network). Both these models were initially developed on behalf of the German Federal Institute of Hydrology (BfG) to apply a 1-dimensional implementation of the “local inertial approximation of the shallow water equations” introduced by Bates et al. (2010) and “stabilised” by de Almeida et al. (2012). Still, they should also apply to other approaches that discretise the shallow water equations in a finite volume staggered grid manner.

Integration tests

Note

When new to HydPy, consider reading section Integration Tests first.

We select a simulation period of five hours and an “external” simulation step size of five minutes for all integration tests:

>>> from hydpy import pub
>>> pub.timegrids = "2000-01-01 00:00", "2000-01-01 05:00", "5m"

The considered 20 km channel consists of eight segments with alternating lengths of two and three kilometres:

>>> from hydpy.models.sw1d_channel import *
>>> parameterstep()
>>> nmbsegments(8)
>>> lengths = 2.0, 3.0, 2.0, 3.0, 2.0, 3.0, 2.0, 3.0

A valid sw1d_channel configuration requires one storage model that complies with the StorageModel_V1 at each segment. We prepare sw1d_storage submodels with identical channel bottom elevations and rectangular profiles. As for other submodels of the HydPy-SW1D model family, one specifies such geometries by sub-submodels that comply with CrossSectionModel_V2 interface. Here, we select wq_trapeze:

>>> for i, length_ in enumerate(lengths):
...     with model.add_storagemodel_v1("sw1d_storage", position=i):
...         length(length_)
...         with model.add_crosssection_v2("wq_trapeze"):
...             nmbtrapezes(1)
...             bottomlevels(5.0)
...             bottomwidths(5.0)
...             sideslopes(0.0)

Additionally, sw1d_channel requires one routing model that complies with the RoutingModel_V2 interface between each pair of segments. So, our example model requires a total of seven routing models. We select sw1d_lias and parametrise its bottom elevation and channel profile as explained above. Besides that, we need to define the Strickler coefficient and two factors for stabilising the numerical integration:

>>> for i in range(1, nmbsegments.value):
...     with model.add_routingmodel_v2("sw1d_lias", position=i):
...         lengthupstream(2.0 if i % 2 else 3.0)
...         lengthdownstream(3.0 if i % 2 else 2.0)
...         stricklercoefficient(1.0/0.03)
...         timestepfactor(0.7)
...         diffusionfactor(0.2)
...         with model.add_crosssection_v2("wq_trapeze"):
...             nmbtrapezes(1)
...             bottomlevels(5.0)
...             bottomwidths(5.0)
...             sideslopes(0.0)

The prepared model has neither a submodel for receiving nor releasing flow at the first or last channel segment. We leave it like that to assign it to an Element instance that is neither connected to any inlet nor outlet nodes:

>>> from hydpy import Element
>>> channel = Element("channel")
>>> channel.model = model

Next, we prepare a test function object to control the following test runs:

>>> from hydpy.core.testtools import IntegrationTest
>>> test = IntegrationTest(channel)
>>> test.plotting_options.axis1 = (factors.waterlevels,)

For convenience, we also define a function that prepares the storage models’ initial water volumes and the routing models’ “old” discharges based on general or individual water depth and discharge values:

>>> def prepare_inits(hs, qs):
...     if isinstance(hs, float):
...         hs = nmbsegments.value * [hs]
...     if isinstance(qs, float):
...         qs = (nmbsegments.value + 1) * [qs]
...     inits = []
...     for h, s in zip(hs, model.storagemodels):
...         length = s.parameters.control.length
...         c = s.crosssection.parameters.control
...         v = h * (c.bottomwidths[0] + h * c.sideslopes[0]) * length
...         inits.append((s.sequences.states.watervolume, v))
...     for q, r in zip(qs, model.routingmodels):
...         if r is not None:
...             inits.append((r.sequences.states.discharge, q))
...     test.inits = inits

Zero inflow and outflow

As mentioned above, there is no routing model at the inlet or outlet position, so there can be no inflow or outflow. Instead, we set the initial depths of 3 m for the first four and 1 m for the last four segments to enforce water movement in this first example:

>>> prepare_inits(hs=[3.0, 3.0, 3.0, 3.0, 1.0, 1.0, 1.0, 1.0], qs=0.0)

At the end of the simulation period, all segments’ water levels have nearly reached the average depth of 2 m. Closer inspection reveals a small “overshooting” with higher water levels in the last than in the first segments that reached its maximum after about four hours:

>>> conditions = test("sw1d_channel_zero_inflow_and_outflow", get_conditions="2000-01-01 00:00")
Click to see the table
Click to see the graph

There is no indication of an error in the water balance:

>>> from hydpy import round_
>>> round_(model.check_waterbalance(conditions))
0.0

Higher precision

Some water level trajectories calculated in the Zero inflow and outflow example show some “edges” in the first ten minutes, which indicates possible improvements by increasing numerical precision. We do this by setting TimeStepFactor to the rather small value of 0.1:

>>> for routingmodel in model.routingmodels.submodels[1:-1]:
...     routingmodel.parameters.control.timestepfactor(0.1)

Now, all water level trajectories have a smooth appearance:

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

There is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

No additional diffusion

However, despite the smoother appearance, one might still not be completely satisfied with the results gained in the Higher precision example. First, the levelling out of the water depths is slightly slowed down. Second, the first and second channel segments’ water level trajectories intersect after a few minutes. Please refer to the LIAS issue on GitHub, where we discuss this and related problems in more detail and hopefully find ways for improvement. Here, we show the simple solution of omitting the additional diffusion introduced by de Almeida et al. (2012) by setting DiffusionFactor to zero:

>>> for routingmodel in model.routingmodels.submodels[1:-1]:
...     routingmodel.parameters.control.diffusionfactor(0.0)

Now, everything looks as expected. Hence, omitting additional diffusion and setting the time step factor to small values seems like the solution to achieving good results. Still, one needs to remember that it comes with the cost of additional computation time:

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

There is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

Longitudinal inflow

So far, the considered channel has received no inflow. We could accomplish this by adding another sw1d_lias model upstream of the first channel segment. However, then we would have to connect the existing channel with at least one other channel, as sw1d_lias only works “between” channel segments. We demonstrate such couplings in the sw1d_network documentation. Here, we select the sw1d_q_in routing model, which allows using observed or previously simulated discharge series as inflow:

>>> with model.add_routingmodel_v1("sw1d_q_in", position=0):
...     lengthdownstream(2.0)
...     timestepfactor(0.7)
...     with model.add_crosssection_v2("wq_trapeze"):
...         nmbtrapezes(1)
...         bottomlevels(5.0)
...         bottomwidths(5.0)
...         sideslopes(0.0)

No matter if adding sw1d_lias, sw1d_q_in, or another routing model to the first position, we must now add an inlet node connectible to the inlet variable LongQ to ensure HydPy can build the required connections to other models (in case of sw1d_lias) or provide the inflow time series (in case of sw1d_q_in):

>>> from hydpy import Node
>>> inflow = Node("inflow", variable="LongQ")
>>> channel = Element("channel", inlets=inflow)
>>> channel.model = model

This extension of our project setting requires a fresh IntegrationTest instance:

>>> test = IntegrationTest(channel)
>>> test.plotting_options.axis1 = (factors.waterlevels,)

We reset the settings of the remaining routing models to the initial Zero inflow and outflow example:

>>> for routingmodel in model.routingmodels.submodels[1:-1]:
...     routingmodel.parameters.control.timestepfactor(0.7)
...     routingmodel.parameters.control.diffusionfactor(0.2)

The constant inflow is 1 m³/s, and the initial water depth is 1 m:

>>> inflow.sequences.sim.series = 1.0
>>> prepare_inits(hs=1.0, qs=0.0)

The simulated water levels rise as expected:

>>> conditions = test("sw1d_channel_longitudinal_inflow", get_conditions="2000-01-01 00:00")
Click to see the table
Click to see the graph

There is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

Lateral flow

In the Longitudinal inflow example, the sw1d_q_in adds “longitudinal” inflow to the channel’s first segment. With “longitudinal”, we mean that the entering water is already moving in the same direction as the water already in the channel. This configuration makes sense when simulating the inflow from an upstream channel.

The sw1d_storage models offers the alternative of adding (or subtracting) lateral flow via an additional node connected to its inlet sequence LatQ. One can use it for considering inflow from adjacent areas or other sources that let their water flow vertically to the channel’s direction. At the moment, all lateral inflow affects only the first channel segments’ water amount. More flexible approaches (e.g. distributing it evenly) are under discussion (see the LIAS issue on GitHub).

Additionally, one can query the water level (also, currently only of the first channel segment) via a node connectible to the sender WaterLevel sequence. This information helps if feedback to the above model is required, such as letting a pump stop working to prevent exceeding a dangerous water level.

We add the two mentioned nodes to our Element instance:

>>> latflow = Node("latflow", variable="LatQ")
>>> waterlevel = Node("waterlevel", variable="WaterLevel")
>>> channel = Element("channel", inlets=(inflow, latflow), senders=waterlevel)
>>> channel.model = model
>>> test = IntegrationTest(channel)
>>> test.plotting_options.axis1 = (factors.waterlevels,)

We set the same initial and inflow values as in the Longitudinal inflow example, except letting the constant inflow enter “laterally” instead of “longitudinally”:

>>> inflow.sequences.sim.series = 0.0
>>> latflow.sequences.sim.series = 1.0
>>> prepare_inits(hs=1.0, qs=0.0)

The results are similar. However, the first segment’s water level is now about 1 cm higher than in the Longitudinal inflow example due to assuming that the lateral’s flow velocity component into the channel direction is zero (note that the extent of this difference vastly depends on the value of DiffusionFactor):

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

There is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

Weir outflow

Adding an outflow routing model works similarly to adding an inflow routing model. Again, we could select a model like sw1d_lias if we were interested in coupling this channel to another one. But here, we choose the outflow routing model sw1d_weir_out, which simulates free weir flow (that does not consider the downstream water level):

>>> with model.add_routingmodel_v3("sw1d_weir_out", position=8):
...     lengthupstream(lengths[-1])
...     crestheight(7.0)
...     crestwidth(5.0)
...     flowcoefficient(0.58)
...     timestepfactor(0.7)

This time, we must add an outlet node responsible for the outlet variable LongQ, which necessitates another refreshing of the IntegrationTest instance:

>>> outflow = Node("outflow", variable="LongQ")
>>> channel = Element("channel", inlets=inflow, outlets=outflow)
>>> channel.model = model
>>> test = IntegrationTest(channel)
>>> test.plotting_options.axis1 = (factors.waterlevels,)

The constant (longitudinal) inflow is again 1 m³/s, but the initial water depth is 2 m, corresponding to the weir’s crest level:

>>> inflow.sequences.sim.series = 1.0
>>> latflow.sequences.sim.series = 0.0
>>> prepare_inits(hs=2.0, qs=0.0)

Sticking to the time step factor of 0.7 would result in numerical inaccuracies at the beginning of the simulation period, even more notable than in the Zero inflow and outflow example. We skip showing them and directly reduce the time step factor as in the Higher precision example:

>>> for routingmodel in model.routingmodels.submodels:
...     routingmodel.parameters.control.timestepfactor(0.1)

After a certain settling process, the water level profiles take a regular shape. However, even after five hours, their weir flow is still below 0.4 n³/s, showing that conditions are still not stationary:

>>> conditions = test("sw1d_channel_weir_outflow", get_conditions="2000-01-01 00:00")
Click to see the table
Click to see the graph

There is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

Initial flow

In all of the above examples, the initial flow is zero. One can set other values, which in real projects usually happens automatically via reading and writing condition files and allows one to stop and resume a simulation without impairing the results. Here, we repeat the Weir outflow example with an initial flow of 1 m³/s:

>>> prepare_inits(hs=2.0, qs=1.0)

Due to the water’s inertia, some of the water already stored in the channel now directly moves in the weir’s direction, which results in a water level rise at the weir’s front and a start of weir flow much earlier than in the Weir outflow example:

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

There is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

Unequal profiles

This experiment deals with non-identical channel profiles. To demonstrate the effect of a rapid change in geometry, we increase the bottom width of the last four channel segments from 5 m to 10 m:

>>> for storagemodel in model.storagemodels[4:]:
...     storagemodel.crosssection.parameters.control.bottomwidths(10.0)

As the sw1d_lias models need to know the channel geometry as well, we set the bottom width of the central one to the average value of 7.5 m and the bottom width of the last three to 10 m:

>>> model.routingmodels[4].crosssection.parameters.control.bottomwidths(7.5)
>>> for routingmodel in model.routingmodels[5:-1]:
...     routingmodel.crosssection.parameters.control.bottomwidths(10.0)

We reset the initial depth to 1 m and the initial discharge to zero:

>>> prepare_inits(hs=1.0, qs=0.0)

For water depths of about 1 m, no weir flow occurs. Hence, when using the same time step factor, the following results are readily comparable with the ones of the Longitudinal inflow example:

>>> for routingmodel in model.routingmodels.submodels:
...     routingmodel.parameters.control.timestepfactor(0.7)

Compared to the Longitudinal inflow example, there is now a more marked difference between the water level gradient of the first (narrower) and last (wider) channel segments:

>>> conditions = test("sw1d_channel_unequal_profiles", get_conditions="2000-01-01 00:00")
Click to see the table
Click to see the graph

There is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

Dry channel

The propagation of a flood wave through an initially dry channel brings extra challenges for any routing algorithm. Monitor the numerical accuracy (and stability) closely for such situations. Here, we again reduce the internal numerical step sizes for this reason:

>>> for routingmodel in model.routingmodels.submodels:
...     routingmodel.parameters.control.timestepfactor(0.1)

For simplicity, we reset all profiles to the same width:

>>> for storagemodel in model.storagemodels[4:]:
...     storagemodel.crosssection.parameters.control.bottomwidths(5.0)
>>> for routingmodel in model.routingmodels[4:-1]:
...     routingmodel.crosssection.parameters.control.bottomwidths(5.0)

At least when using sw1d_lias as the routing submodel, starting with zero initial water depth does not result in apparent errors (e.g. zero divisions). The water level trajectories look stable and plausible. However, inspecting the discharge between the first and the second segment reveals a slight tendency to instability (which magnifies when increasing the time step factor):

>>> prepare_inits(hs=0.0, qs=0.0)
>>> conditions = test("sw1d_channel_dry_channel", get_conditions="2000-01-01 00:00")
Click to see the table
Click to see the graph

There is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

Internal negative volumes

The internal water movement within the channel could result in undershooting a water depth of zero for individual segments, caused, for example, by strongly varying bottom levels. We create such a potentially problematic case by increasing the height of the first, the fourth, and the last segment by one metre:

>>> for i in [0, 3, 7]:
...     model.storagemodels[i].crosssection.parameters.control.bottomlevels(6.0)

We set all inflow to zero and the initial water depth to only 0.1 m:

>>> inflow.sequences.sim.series = 0.0
>>> prepare_inits(hs=0.1, qs=0.0)

The selected submodels decide how they handle potentially negative water depths. sw1d_lias relies on method Update_Discharge_V1 to prevent or at least limit taking too much water from a channel segment, and sw1d_storage uses the sub-submodel wq_trapeze, whose method Calc_WaterDepth_V2 determines a depth of zero even for negative volumes. So, the following results look impeccable:

>>> conditions = test("sw1d_channel_internal_negative_volumes", get_conditions="2000-01-01 00:00")
Click to see the table
Click to see the graph

Also, there is no indication of an error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

However, looking into the details, one finds the final water volumes of the third segment to be slightly negative:

>>> from hydpy import print_vector
>>> print_vector(s.sequences.states.watervolume.value for s in model.storagemodels)
0.0, 2.553539, 1.698536, -0.003351, 1.529698, 2.414455, 1.807123, 0.0

This deficit is due to the limitation of method Update_Discharge_V1 only to consider the water loss caused by one routing model and not all involved routing models. Implementing a safer approach would likely increase computation time and complexity for little benefit. Hence, we keep it that way until applications suggest an improvement is necessary.

Excessive water withdrawal

More significant deficits can result from misleading inflow and outflow data used as external forcing. To apply such forcings in all possible ways, we replace the sw1d_weir_out submodel at the channel’s outlet with a sw1d_q_out submodel. The Bifurcations example of sw1d_network explains the following necessary steps:

>>> channel.model.storagemodels[-1].routingmodelsdownstream.number = 0
>>> channel.model.routingmodels[-2].routingmodelsdownstream.number = 0
>>> from hydpy.models import sw1d_q_out
>>> with channel.model.add_routingmodel_v3(sw1d_q_out, position=8):
...     lengthupstream(2.0)
...     timestepfactor(0.7)
...     with model.add_crosssection_v2("wq_trapeze"):
...         nmbtrapezes(1)
...         bottomlevels(5.0)
...         bottomwidths(5.0)
...         sideslopes(0.0)
>>> channel.model.connect()
>>> test = IntegrationTest()
>>> outflow.deploymode = "oldsim_bi"

We reset all bottom levels to 5 m for simplicity:

>>> for i in [0, 3, 7]:
...     model.storagemodels[i].crosssection.parameters.control.bottomlevels(5.0)

The simulation starts with a consistent water depth of a half metre:

>>> prepare_inits(hs=0.5, qs=0.0)

The longitudinal inflow and the lateral flow are each -0.5 m³/s (subtracting 1 m³/s from the first segment), and the longitudinal outflow is 1 m³/s (removing this amount of water from the last segment):

>>> inflow.sequences.sim.series = -0.5
>>> latflow.sequences.sim.series = -0.5
>>> outflow.sequences.sim.series = 1.0

Despite the symmetry of the external forcing, the first segment’s water depth reaches zero a little earlier due to its smaller lengths and the correspondingly smaller initial water volume:

>>> conditions = test("sw1d_channel_excessive_water_withdrawal", get_conditions="2000-01-01 00:00")
Click to see the table
Click to see the graph

Again, there seems to be no error in the water balance:

>>> round_(model.check_waterbalance(conditions))
0.0

However, the first and the last segments “bookmark” the impossible subtractions as negative water volumes:

>>> from hydpy import print_vector
>>> print_vector((s.sequences.states.watervolume.value for s in model.storagemodels), width=80)
-10.126869, 6.272359, 4.206635, 6.17089, 4.101102, 6.057642, 3.526298, -6.208057

Hence, when applying external time series that extract water from channels, one has to ensure such problems never emerge to a problematic extent.

Flood wave

All previous examples dealt with constant inflows and outflows, rectangular channel profiles, and zero channel slopes. Here, we demonstrate the propagation in a flood wave through a trapezoidal, inclined channel.

We take the following configuration from the Base example on musk_mct to allow for comparison between both routing approaches.

The simulation period and step size are identical:

>>> pub.timegrids = "2000-01-01", "2000-01-05", "30m"

We also prepare a 50 km long channel and divide it into 50 equally long and shaped segments. musk_mct, we cannot set the bottom slope directly but must ensure the individual bottom levels agree with it:

>>> nmbsegments(50)
>>> for i in range(50):
...     with model.add_storagemodel_v1("sw1d_storage", position=i):
...         length(2.0)
...         with model.add_crosssection_v2("wq_trapeze"):
...             nmbtrapezes(1)
...             bottomlevels(-1000.0 * (i * 2.0 + 1.0) * 0.00025)
...             bottomwidths(15.0)
...             sideslopes(5.0)
>>> for i in range(1, 50):
...     with model.add_routingmodel_v2("sw1d_lias", position=i):
...         lengthupstream(2.0)
...         lengthdownstream(2.0)
...         stricklercoefficient(1.0/0.035)
...         timestepfactor(0.7)
...         diffusionfactor(0.2)
...         with model.add_crosssection_v2("wq_trapeze"):
...             nmbtrapezes(1)
...             bottomlevels(-1000.0 * (i * 2.0) * 0.00025)
...             bottomwidths(15.0)
...             sideslopes(5.0)

We use a sw1d_q_in submodel to provide the channel inflow:

>>> with model.add_routingmodel_v1("sw1d_q_in", position=0):
...     lengthdownstream(2.0)
...     timestepfactor(0.7)
...     with model.add_crosssection_v2("wq_trapeze"):
...         nmbtrapezes(1)
...         bottomlevels(0.0)
...         bottomwidths(15.0)
...         sideslopes(5.0)

In contrast to musk_mct, we also need an explicit assumption for the channel outflow and select the sw1d_weir_out submodel for this purpose. We set its crest height equal to the channel bottom, its crest width to 10 m, and its flow coefficient so that its the weir flow is 100 m³/s for the initial water depth defined below:

>>> with model.add_routingmodel_v3("sw1d_weir_out", position=50):
...     lengthupstream(2.0)
...     crestheight(-1000.0 * (50 * 2.0) * 0.00025)
...     crestwidth(10.0)
...     flowcoefficient(0.472396985)
...     timestepfactor(0.7)

Now we reset the outflow node’s deploymode, refresh all connections, and prepare a new IntegrationTest instance:

>>> outflow.deploymode = "newsim"
>>> channel.model = model
>>> from hydpy.core.testtools import IntegrationTest
>>> test = IntegrationTest(channel)
>>> test.plotting_options.axis1 = (fluxes.discharges,)

The simulation starts with a base flow of 100 m³/s and a sufficiently consistent water depth:

>>> prepare_inits(hs=3.717832, qs=100.0)

We calculate the inflow time series as in the Base example on musk_mct.

>>> import numpy
>>> q_base, q_peak, t_peak, beta = 100.0, 900.0, 24.0, 16.0
>>> ts = pub.timegrids.init.to_timepoints()
>>> inflow.sequences.sim.series = q_base + (
...     (q_peak - q_base) * ((ts / t_peak) * numpy.exp(1.0 - ts / t_peak)) ** beta)
>>> latflow.sequences.sim.series = 0.0

There is a good agreement between the results of musk_mct and the following results for most of the given discharge time series. Striking differences occur only for those segments in the vicinity of the weir:

>>> conditions = test("sw1d_flood_wave", get_conditions="2000-01-01 00:00")
Click to see the table
Click to see the graph

There is no indication of an error in the water balance:

>>> from hydpy import round_
>>> round_(model.check_waterbalance(conditions))
0.0
class hydpy.models.sw1d_channel.Model[source]

Bases: SubstepModel, ChannelModel_V1

HydPy-SW1D-Channel (“user model” for preparing single channels that will be combined and solved by HydPy-SW1D-Network).

The following “inlet update methods” are called in the given sequence at the beginning of each 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:
Users can hook submodels into the defined main model if they satisfy one of the following interfaces:
  • RoutingModel_V1 Interface for calculating the inflow into a channel.

  • RoutingModel_V2 Interface for calculating the discharge between two channel segments.

  • RoutingModel_V3 Interface for calculating the outflow of a channel.

  • StorageModel_V1 Interface for calculating the water amount stored in a single channel segment.

DOCNAME: DocName = ('SW1D-Channel', '"user model" for preparing single channels that will be combined and solved by HydPy-SW1D-Network')
storagemodels: modeltools.SubmodelsProperty[StorageModel_V1]

References to the storage submodels.

There must be one storage model for each channel segment.

add_storagemodel_v1

Initialise the given storage model that follows the StorageModel_V1 interface and build the necessary connections to its already available side models.

We construct a channel consisting of two segments:

>>> from hydpy.models.sw1d_channel import *
>>> parameterstep()
>>> nmbsegments(2)

We first add a routing model between both segments and then add the two required storage models:

>>> with model.add_routingmodel_v2("sw1d_lias", position=1, update=False):
...     pass
>>> with model.add_storagemodel_v1("sw1d_storage", position=0):
...     length(2.0)
>>> with model.add_storagemodel_v1("sw1d_storage", position=1):
...     length(3.0)

Each storage model is at the correct position:

>>> sm0, sm1 = model.storagemodels
>>> sm0.parameters.control.length
length(2.0)
>>> sm1.parameters.control.length
length(3.0)

All side model connections are adequately prepared:

>>> rm = model.routingmodels[1]
>>> assert sm0.routingmodelsupstream.number == 0
>>> assert sm0.routingmodelsdownstream.number == 1
>>> assert sm0.routingmodelsdownstream[0] is rm
>>> assert rm.storagemodelupstream is sm0
>>> assert rm.storagemodeldownstream is sm1
>>> assert sm1.routingmodelsupstream.number == 1
>>> assert sm1.routingmodelsupstream[0] is rm
>>> assert sm1.routingmodelsdownstream.number == 0
routingmodels: modeltools.SubmodelsProperty[RoutingModel_V1 | RoutingModel_V2 | RoutingModel_V3]

References to the routing submodels.

There must be one routing model for each interface between two channel segments. Additionally, one routing model can be at the first position for simulating “longitudinal inflow” into the channel. And there can be one routing model at the last position for calculating “longitudinal outflow”.

Note that “inflow” and “outflow” here only refer to the location but not necessarily to water increases or decreases in the channel’s water amount, as both might be positive or negative, depending on the selected submodel.

If neither the upstream channel model defines a routing model for its last position nor the corresponding downstream channel model for its first position, both channels cannot become connected. If both channel models define routing models for the mentioned positions, it is unclear which is relevant. We suggest the following convention. Of course, add routing submodels that handle “external longitudinal inflow” (e.g. sw1d_q_in) at the first position and routing submodels that handle “external longitudinal outflow” (e.g. sw1d_weir_out) at the last position. Add routing submodels that handle “internal longitudinal flow” (e.g. sw1d_lias) at the last position for places with confluences or without tributaries. For branches, add them to the first position.

add_routingmodel_v1

Initialise the given routing model that follows the RoutingModel_V1 interface and build the necessary connections to its already available side models.

We construct a channel consisting of two segments:

>>> from hydpy.models.sw1d_channel import *
>>> parameterstep()
>>> nmbsegments(2)

Method add_routingmodel_v1() checks that the user tries to add the model at the first position, as it is the only one valid for “inflow models”:

>>> with model.add_routingmodel_v1("sw1d_q_in", position=1):
...     pass
Traceback (most recent call last):
...
ValueError: While trying to add a submodel to the main model `sw1d_channel`, the following error occurred: Submodels of type `sw1d_q_in` can only be added to the first channel position 0, but `1` is given.
>>> assert model.routingmodels[1] is None

We first add one storage model to each channel segment and one routing model between both segments:

>>> from hydpy.models import sw1d_storage, sw1d_lias, sw1d_q_in
>>> with model.add_storagemodel_v1(sw1d_storage, position=0) as sm0:
...     pass
>>> with model.add_routingmodel_v2(sw1d_lias, position=1, update=False) as rm1:
...     timestepfactor(0.6)
>>> with model.add_storagemodel_v1(sw1d_storage, position=1) as sm1:
...     pass

Now, we add a routing model to the first position:

>>> with model.add_routingmodel_v1(sw1d_q_in, position=0, update=False) as rm0:
...     timestepfactor(0.7)

The “inflow model” is at the correct position:

>>> model.routingmodels[0].parameters.control.timestepfactor
timestepfactor(0.7)

All side model connections are adequately prepared:

>>> assert rm0.routingmodelsdownstream.number == 1
>>> assert rm1 in rm0.routingmodelsdownstream
>>> assert rm0.storagemodeldownstream is sm0
>>> assert sm0.routingmodelsupstream.number == 1
>>> assert rm0 in sm0.routingmodelsupstream
>>> assert sm0.routingmodelsdownstream.number == 1
>>> assert rm0 not in sm0.routingmodelsdownstream
>>> assert rm1.routingmodelsupstream.number == 1
>>> assert rm0 in rm1.routingmodelsupstream
>>> assert rm1.routingmodelsdownstream.number == 0
>>> assert sm1.routingmodelsupstream.number == 1
>>> assert rm0 not in sm1.routingmodelsupstream
>>> assert sm1.routingmodelsdownstream.number == 0
add_routingmodel_v2

Initialise the given routing model that follows the RoutingModel_V2 interface and build the necessary connections to its already available side models.

We construct a channel consisting of two segments:

>>> from hydpy.models.sw1d_channel import *
>>> parameterstep()
>>> nmbsegments(2)

We first one storage model to each channel segment and one routing model above the first and below the second segment:

>>> from hydpy.models import sw1d_storage, sw1d_lias
>>> with model.add_routingmodel_v2(sw1d_lias, position=0, update=False) as rm0:
...     timestepfactor(0.7)
>>> with model.add_storagemodel_v1(sw1d_storage, position=0) as sm0:
...     pass
>>> with model.add_storagemodel_v1(sw1d_storage, position=1) as sm1:
...     pass
>>> with model.add_routingmodel_v2(sw1d_lias, position=2, update=False) as rm2:
...     timestepfactor(0.8)

Now, we add a routing model to the central location:

>>> with model.add_routingmodel_v2(sw1d_lias, position=1, update=False) as rm1:
...     timestepfactor(0.6)

The central routing model is actually at the correct position:

>>> model.routingmodels[1].parameters.control.timestepfactor
timestepfactor(0.6)
>>> assert rm0.routingmodelsupstream.number == 0
>>> assert rm0.routingmodelsdownstream.number == 1
>>> assert rm1 in rm0.routingmodelsdownstream
>>> assert sm0.routingmodelsupstream.number == 1
>>> assert rm1 not in rm0.routingmodelsupstream
>>> assert sm0.routingmodelsdownstream.number == 1
>>> assert rm1 in rm0.routingmodelsdownstream
>>> assert rm1.routingmodelsupstream.number == 1
>>> assert rm0 in rm1.routingmodelsupstream
>>> assert sm0 is rm1.storagemodelupstream
>>> assert sm1 is rm1.storagemodeldownstream
>>> assert rm1.routingmodelsdownstream.number == 1
>>> assert rm2 in rm1.routingmodelsdownstream
>>> assert sm1.routingmodelsupstream.number == 1
>>> assert rm1 in rm2.routingmodelsupstream
>>> assert sm1.routingmodelsdownstream.number == 1
>>> assert rm1 not in rm2.routingmodelsdownstream
>>> assert rm2.routingmodelsupstream.number == 1
>>> assert rm1 in rm2.routingmodelsupstream
>>> assert rm2.routingmodelsdownstream.number == 0
add_routingmodel_v3

“Initialise the given routing model that follows the RoutingModel_V3 interface and build the necessary connections to its already available side models.

We construct a channel consisting of two segments:

>>> from hydpy.models.sw1d_channel import *
>>> parameterstep()
>>> nmbsegments(2)

Method add_routingmodel_v3() checks that the user tries to add the model at the last position, as it is the only one valid for “outflow models”:

>>> with model.add_routingmodel_v3("sw1d_weir_out", position=1):
...     pass
Traceback (most recent call last):
...
ValueError: While trying to add a submodel to the main model `sw1d_channel`, the following error occurred: Submodels of type `sw1d_weir_out` can only be added to the last channel position (2), but `1` is given.
>>> assert model.routingmodels[1] is None

We first add one storage model to each channel segment and one routing model between both segments:

>>> from hydpy.models import sw1d_storage, sw1d_lias, sw1d_weir_out
>>> with model.add_storagemodel_v1(sw1d_storage, position=0) as sm0:
...     pass
>>> with model.add_routingmodel_v2(sw1d_lias, position=1, update=False) as rm1:
...     timestepfactor(0.6)
>>> with model.add_storagemodel_v1(sw1d_storage, position=1) as sm1:
...     pass

Now, we add a routing model to the last position:

>>> with model.add_routingmodel_v3(sw1d_weir_out, position=2, update=False) as rm2:
...     timestepfactor(0.7)

The “outflow model” is at the correct position:

>>> model.routingmodels.submodels[2].parameters.control.timestepfactor
timestepfactor(0.7)
>>> assert sm0.routingmodelsupstream.number == 0
>>> assert sm0.routingmodelsdownstream.number == 1
>>> assert rm2 not in sm0.routingmodelsdownstream
>>> assert rm1.routingmodelsupstream.number == 0
>>> assert rm1.routingmodelsdownstream.number == 1
>>> assert rm2 in rm1.routingmodelsdownstream
>>> assert sm1.routingmodelsupstream.number == 1
>>> assert rm2 not in sm1.routingmodelsupstream
>>> assert sm1.routingmodelsdownstream.number == 1
>>> assert rm2 in sm1.routingmodelsdownstream
>>> assert rm2.storagemodelupstream is sm1
>>> assert rm2.routingmodelsupstream.number == 1
>>> assert rm1 in rm2.routingmodelsupstream
property couple_models: ModelCoupler

The model coupler combine_channels, as defined by the composite model sw1d_network.

check_waterbalance(initial_conditions: dict[str, dict[str, dict[str, float | ndarray[Any, dtype[float64]]]]]) float[source]

Determine the water balance error of the previous simulation in m³.

Method check_waterbalance() calculates the balance error as follows:

\[\begin{split}Error = \Sigma In - \Sigma Out + \Sigma Lat - \Delta Vol \\ \\ \Sigma In = \sum_{t=t_0}^{t_1} DischargeVolume_t^1 \\ \Sigma Out = \sum_{t=t_0}^{t_1} DischargeVolume_t^{N+1} \\ \Sigma Lat = Seconds \cdot \sum_{t=t_0}^{t_1} LateralFlow_t^1 \\ \Delta Vol = 1000 \cdot \sum_{i=1}^{N} WaterVolume_{t1}^i - WaterVolume_{t0}^i \\ \\ N = NmbSegments\end{split}\]

The returned error should always be in scale with numerical precision so that it does not affect the simulation results in any relevant manner.

Pick the required initial conditions before starting the simulation via property conditions. See the application model sw1d_channel integration tests for some examples.

ToDo: So far, check_waterbalance() works only with application model

sw1d_storage instances . We need to implement a more general solution as soon as we implement further storage models.

REUSABLE_METHODS: ClassVar[tuple[type[ReusableMethod], ...]] = ()
cymodel: CySubstepModelProtocol | None
parameters: parametertools.Parameters
sequences: sequencetools.Sequences
masks: masktools.Masks
class hydpy.models.sw1d_channel.ControlParameters(master: Parameters, cls_fastaccess: type[FastAccessParameter] | None = None, cymodel: CyModelProtocol | None = None)

Bases: SubParameters

Control parameters of model sw1d_channel.

The following classes are selected:
class hydpy.models.sw1d_channel.DerivedParameters(master: Parameters, cls_fastaccess: type[FastAccessParameter] | None = None, cymodel: CyModelProtocol | None = None)

Bases: SubParameters

Derived parameters of model sw1d_channel.

The following classes are selected:
  • Seconds() The length of the actual simulation step size in seconds [s].

class hydpy.models.sw1d_channel.FactorSequences(master: Sequences, cls_fastaccess: type[TypeFastAccess_co] | None = None, cymodel: CyModelProtocol | None = None)

Bases: FactorSequences

Factor sequences of model sw1d_channel.

The following classes are selected:
  • TimeStep() The actual computation step according to global stability considerations [s].

  • WaterLevels() The water level within all segments of a channel [m].

class hydpy.models.sw1d_channel.FluxSequences(master: Sequences, cls_fastaccess: type[TypeFastAccess_co] | None = None, cymodel: CyModelProtocol | None = None)

Bases: FluxSequences

Flux sequences of model sw1d_channel.

The following classes are selected:
  • Discharges() The discharges between all channel segments, including the flow into the first and out of the last one [m³/s].