Welcome to RTC-Tools Hydraulic Structures’s documentation!¶
Contents:¶
Getting Started¶
Installation¶
This package requires RTC-Tools 2 to be installed, including the ChannelFlow library.
Installation of the RTC-Tools Hydraulic Structures library then consists of the following steps:
# 1. Download the source code
https://gitlab.com/deltares/rtc-tools-hydraulic-structures.git
# 2. Move into the downloaded directory
cd rtc-tools-hydraulic structures
# 3. Install the Python modules
python -m pip install .
The Modelica library is not installed automatically, and needs to be copied
manually. The location of RTC-Tools’s Modelica library root is typically
something like C:\RTCTools2\mo
on Windows. Copy the modelica/Deltares
folder to this location.
Running an example¶
To make sure that everything is set-up correctly, you can run one of the
example cases in examples/
:
cd /path/to/rtc-tools-hydraulic-structures/examples/simple-pumping-station/src
python example.py
You will see the progress of RTC-Tools in your shell. If all is well, you should see something like the following output.

Python API¶
Pumping Station Mixin¶
-
class
rtctools_hydraulic_structures.pumping_station_mixin.
Pump
(optimization_problem, symbol)[source]¶ Bases:
rtctools_hydraulic_structures.util._ObjectParameterWrapper
Python Pump object as an interface to the
Pump
object in the model.
-
class
rtctools_hydraulic_structures.pumping_station_mixin.
Resistance
(optimization_problem, symbol)[source]¶ Bases:
rtctools_hydraulic_structures.util._ObjectParameterWrapper
Python Resistance object as an interface to the
Resistance
object in the model.
-
class
rtctools_hydraulic_structures.pumping_station_mixin.
PumpingStation
(optimization_problem, symbol, pump_symbols=None, **kwargs)[source]¶ Bases:
rtctools_hydraulic_structures.util._ObjectParameterWrapper
Python PumpingStation object as an interface to the
PumpingStation
object in the model.-
__init__
(optimization_problem, symbol, pump_symbols=None, **kwargs)[source]¶ Initialize the pumping station object.
Parameters: - optimization_problem –
OptimizationProblem
instance. - symbol – Symbol name of the pumping station in the model.
- pump_symbols – Symbol names of the pumps in the pumping station.
- optimization_problem –
-
pumps
()[source]¶ Get a list of
Pump
objects that are part of this pumping station in the model.Returns: List of Pump
objects.
-
resistances
()[source]¶ Get a list of
Resistance
objects that are part of this pumping station in the model.Returns: List of Resistance
objects.
-
-
class
rtctools_hydraulic_structures.pumping_station_mixin.
PumpingStationMixin
(*args, **kwargs)[source]¶ Bases:
rtctools.optimization.optimization_problem.OptimizationProblem
Adds handling of PumpingStation objects in your model to your optimization problem.
Relevant parameters and variables are read from the model, and from this data a set of constraints and objectives are automatically generated to minimize cost.
-
pumping_stations
()[source]¶ User problem returns list of
PumpingStation
objects.Returns: A list of pumping stations.
-
Weir Mixin¶
-
class
rtctools_hydraulic_structures.weir_mixin.
Weir
(optimization_problem, name)[source]¶ Bases:
rtctools_hydraulic_structures.util._ObjectParameterWrapper
Python Weir object as an interface to the Weir object in the model.
In the optimization, the weir flow is implemented as constraints. It means that the optimization calculated a flow (not weir height!), that is forced by the constraints to be a physically possible weir flow.
-
class
rtctools_hydraulic_structures.weir_mixin.
WeirMixin
(*args, **kwargs)[source]¶ Bases:
rtctools.optimization.optimization_problem.OptimizationProblem
Adds handling of Weir objects in your model to your optimization problem.
Modelica API¶
Pumping Station¶
The Modelica library Deltares.HydraulicStructures.PumpingStation
is an
extension to the Deltares.ChannelFlow.Hydraulic
library, which is part of
the ChannelFlow library. It consists of the following components:
- Pump
- A pump model with a QHP (discharge, head, power) relationship, to be used
for optimization of e.g. costs. It extends
Deltares.ChannelFlow.Hydraulic.Structures.Pump
- Resistance
- Quadratic resistance.
- PumpingStation
- Encapsulating class for Pump and Resistance objects.
Note
Pump
and Resistance
objects should always be placed inside a
PumpingStation
object.
Pump¶
-
class
Pump
: Deltares::ChannelFlow::Hydraulic::Structures::Pump¶ Represents a single pump object. Because the power of the pump is seldom a linear function of Q and H, this class is wrapped by the Python API’s
Pump
which turns the power equation specified bypower_coefficients
into a set of inequality constraints:\[\begin{split}\begin{aligned} \ P &\ge C_{1,1} + C_{1,2} Q + \ldots \\[5pt] \lim_{Q \to 0} P &= 0 \ \end{aligned}\end{split}\]With minimization of pumping costs (i.e. power), the optimization results will satisfy the first inequality constraint as if it was an equality constraint.
-
Real
power_coefficients
[:, :]¶ The power coefficients describe the relationship between the discharge, head and power. For example, one can consider a fit of the pump power of the general form:
\[P = C_{1,1} + C_{1,2} Q + C_{2,1} H + C_{2,2} Q H + C_{1,3} Q^2 + \ldots\]The power coefficients matrix corresponds to the coefficients C in the equation above. To guarantee that optimization finds a good and stable solution, we require the coefficients of this polynomial to be chosen such that the polynomial is convex over the entire domain.
Note
Strictly speaking it would only have to be convex over the (automatically) extended working area domain, the size of which is not always known before run-time.
-
Real
working_area
[:, :, :]¶ The working area array describes the polynomials bounding the convex set of allowed Q-H coordinates. These polynomials typically arise from one of the following properties:
- Q-H curve at minimum pump speed
- Q-H curve at maximum pump speed
- Minimum required efficiency (e.g. 50%)
- Minimum and/or maximum input power constraint
- Cavitation constraints
- NPSH constraints
The first coordinate of the array is the polynomial number. For example,
working_area[1, :, :]
would describe the first working area polynomial. The order of Q and H coefficients is the same as inpower_coefficients
.
-
Real
working_area_direction
[:]¶ The polynomials in
working_area
describe the polynomials, but do not yet indicate what side of this polynomial the Q-H combination has to be on. So for each of the polynomials in the working area we have to specify whether the expression should evaluate to a positive expression (=1), or a negative expression (=-1).Note
It may become unnecessary to specify this in the future, if it is possible to figure out a way to determine this automatically based on the polynomials and their crossing points.
-
Integer
head_option
= 0¶ What head to use for the pump head. This can be one of the following three options:
- -1
- The upstream head
- 0
- The differential head (i.e. downstream head minus upstream head)
- 1
- The downstream head.
-
Modelica::SIunits::Duration
minimum_on
= 0.0¶ The minimum amount of time in seconds a pump needs to be on before allowed to switch off again. This applies to all pumps in this pumping station.
Note
Only multiples of the (equidistant) time step are allowed.
-
Modelica::SIunits::Duration
minimum_off
= 0.0¶ The minimum amount of time in seconds a pump needs to be off before allowed to switch on again. This applies to all pumps in this pumping station.
Note
Only multiples of the (equidistant) time step are allowed.
-
Modelica::SIunits::Energy
start_up_energy
= 0.0¶ The energy needed to start a pump. This will be multiplied with the energy price to calculate the costs.
-
Real
start_up_cost
= 0.0¶ Costs in e.g. EUR or kg CO2 associated with a pump start up. Many pump switches could for example mean the pump life is shortened, or that more maintenance is needed. These could then be expressed in monetary value, and associated with pump start up.
Important
Make sure that the units of this value are of the same units as
start_up_energy
times the energy price.
-
Modelica::SIunits::Energy
shut_down_energy
= 0.0¶ Energy needed to shut down a pump. See equivalent parameter for pump start
start_up_energy
for more information.
-
Real
shut_down_cost
= 0.0¶ Cost associated with a pump shutdown. See equivalent parameter for pump start
start_up_cost
for more information.
-
Real
Resistance¶
-
class
Resistance
: Deltares::ChannelFlow::Internal::HQTwoPort¶ Represents a single quadratic resistance object relating the head loss to the discharge:
\[dH = C \cdot Q^2\]Because a non-linear equality constraint is not allowed in convex optimization, this class is wrapped by the Python API’s
Resistance
which turns it into two inequality constraints:\[\begin{split}\begin{aligned} \ dH &\ge C \cdot Q^2 \\[5pt] \lim_{Q \to 0} dH &= 0 \ \end{aligned}\end{split}\]With minimization of pumping costs (i.e. power), the optimization results will satisfy the first inequality constraint as if it was an equality constraint, provided the power relationship of every pump is monotonically increasing with H.
Note
Only positive flow is allowed (read: enforced).
-
Real
C
= 0.0¶
-
Real
PumpingStation¶
-
class
PumpingStation
: Deltares::ChannelFlow::Internal::HQTwoPort¶ Represents a pumping station object, containing one or more
Pump
orResistance
objects.-
Integer
n_pumps
¶ The number of pumps contained in the pumping station. This is necessary to enforce the right size of e.g. the
pump_switching_matrix
.
-
Integer
pump_switching_matrix
[n_pumps, n_pumps] = -999¶ Together with
pump_switching_constraints
describes which pumps are allowed to be on at the same time. The default value of -999 will make Python fill it with the default matrix. This default matrix implies that the second pump can only be on when the first pump is on, that the third pump can only be on when the second pump is on, etc.In matrix multiplication form
\[b[:,1] \le A \cdot x \le b[:,2]\]with \(A\) the
pump_switching_matrix
, \(b\) thepump_switching_constraints
, and \(x\) the vector of pump statuses:\[\begin{split}x = \left[\begin{array}{cc}S_1 \\ S_2 \\ S_3 \\ \vdots \end{array}\right]\end{split}\]where \(S_1\) is the status of pump 1 (on = 1, off = 0).
So the default matrix, where a pump being on requires all lower numbered pumps to be on as well, can be expressed as follows:
\[\begin{split}A = \left[\begin{array}{ccc}0 & 0 & 0\\1 & -1 & 0\\1 & 1 & -2\end{array}\right]\end{split}\]with
pump_switching_constraints
equal to:\[\begin{split}b = \left[\begin{array}{cc}-\infty & \infty\\0 & \infty\\0 & \infty\end{array}\right]\end{split}\]To allow all pumps to switch independently from each other, it is sufficient to set the coefficient matrix to all zeros (e.g.
pump_switching_matrix = fill(0, n_pumps, n_pumps)
). For rows in the matrix not containing any non- zero values, the accompanying constraints are not applied.Note
Only square matrices allowed, i.e. a single constraint per pump.
-
Integer
pump_switching_constraints
[n_pumps, 2]¶ See discussion in
pump_switching_matrix
.
-
Integer
Weir¶
-
class
Weir
: Deltares::ChannelFlow::Internal::HQTwoPort¶ Represents a general movable-crest weir object described by the conventional weir equation (see e.g. Swamee, Prabhata K. “Generalized rectangular weir equations.” Journal of Hydraulic Engineering 114.8 (1988): 945-949.):
\[Q = \frac{2}{3} C B \sqrt{2g} \left( H - H_w \right)^{1.5}\]where Q is the discharge of the weir, C is the weir discharge coefficient (very well approximated by 0.61), B is the width of the weir, g is the acceleration of gravity, H is the water level, and \(H_w\) is the level of the movable weir crest. The equation assumes critical flow over the weir crest.
-
Modelica::SIunits::Length
width
¶ The physical width of the weir.
-
Modelica::SIunits::VolumeFlowRate
q_min
¶ The minimal possible discharge on this weir. It can be known from the physical characteristics of the system. The linear approximation works the best if this is set as tight as possible. It is allowed to set it to zero.
-
Modelica::SIunits::VolumeFlowRate
q_max
¶ The maximum physically possible flow over the weir. It should be set as tight as possible
-
Modelica::SIunits::Length
hw_min
¶ The minimal possible crest elevation.
-
Modelica::SIunits::Length
hw_max
¶ The maximum possible crest elevation.
-
Real
weir_coef
= 0.61¶ The discharge coefficient of the weir. Typically the default value of 0.61.
-
Modelica::SIunits::Length
Examples¶
Pumping Station¶
Basic Pumping Station¶

Note
This example focuses on how to implement optimization for pumping stations in RTC-Tools using the Hydraulic Structures library. It assumes basic exposure to RTC-Tools. If you are a first-time user of RTC-Tools, please refer to the RTC-Tools documentation.
The purpose of this example is to understand the technical setup of a model with the Hydraulic Structures Pumping Station object, how to run the model, and how to interpret the results.
The scenario is the following: A pumping station with a single pump is trying to keep an upstream polder in an allowable water level range. Downstream of the pumping station is a sea with a (large) tidal range, but the sea level never drops below the polder level. The price on the energy market fluctuates, and the goal of the operator is to keep the polder water level in the allowable range while minimizing the pumping costs.
The folder examples/pumping_station/basic
contains the complete RTC-Tools
optimization problem.
The Model¶
For this example, the model represents a typical setup for a polder pumping station in lowland areas. The inflow from precipitation and seepage is modeled as a discharge (left side), with the total surface area / volume of storage in the polder modeled as a linear storage. The downstream water level is assumed to not be (directly) influenced by the pumping station, and therefore modeled as a boundary condition.
Operating the pumps to discharge the water in the polder consumes power, which varies based on the head difference and total flow. In general, the lower the head difference or discharge, the lower the power needed to pump water.
The expected result is therefore that the model computes a control pattern that makes use of these tidal and energy fluctuations, pumping water when the sea water level is low and/or energy is cheap. It is also expected that as little water as necessary is pumped, i.e. the storage available in the polder is used to its fullest. Concretely speaking this means that the water level at the last time step will be very close (or equal) to the maximum water level.
The model can be viewed and edited using the OpenModelica Connection Editor
program. First load the Deltares library into OpenModelica Connection Editor,
and then load the example model, located at
examples/pumping_station/basic/model/Example.mo
. The model Example.mo
represents a simple water system with the following elements:
- the polder canals, modeled as storage element
Deltares.ChannelFlow.Hydraulic.Storage.Linear
, - a discharge boundary condition
Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Discharge
, - a water level boundary condition
Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Level
, - a pumping station
MyPumpingStation
extendingDeltares.HydraulicStructures.PumpingStation.PumpingStation

Note it is a nested model. In other words, we have defined our own MyPumpingStation
model, which is in itself part of the Example
model. You can add classes (e.g. models) to an existing model in the OpenModelica Editor by right clicking your current model (e.g. Example
) –> New Modelica Class
. Make sure to extend the Deltares.HydraulicStructures.PumpingStation.PumpingStation
class.
If we navigate into our nested MyPumpingStation model, we have the following elements:
- our single pump
Deltares.HydraulicStructures.PumpingStation.Pump
, - a resistance
Deltares.HydraulicStructures.PumpingStation.Resistance
,

In text mode, the Modelica model looks as follows (with annotation statements removed):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | model Example
model MyPumpingStation
extends Deltares.HydraulicStructures.PumpingStation.PumpingStation(
n_pumps=1
);
Deltares.HydraulicStructures.PumpingStation.Pump pump1(
power_coefficients = {{3522.8, -27946.3, 54484.8},
{1665.43, 5827.81, 0.0},
{208.251, 0.0, 0.0}},
working_area = {{{ -5.326999, 54.050758, 0.000000},
{ -1.0, 0.0, 0.0}},
{{ 0.000426, -0.001241, 2.564056},
{ -1.0, 0.0, 0.0}},
{{ 2.577975, -5.203480, 0.000000},
{ -1.0, 0.0, 0.0}},
{{ 13.219650, -3.097600, -7.551339},
{ -1.0, 0.0, 0.0}}},
working_area_direction = {1, -1, -1, 1},
minimum_on=3.0*3600
);
Deltares.HydraulicStructures.PumpingStation.Resistance resistance1(C=1.0);
equation
connect(HQUp, resistance1.HQUp);
connect(resistance1.HQDown, pump1.HQUp);
connect(pump1.HQDown, HQDown);
end MyPumpingStation;
// Elements in model flow chart
Deltares.ChannelFlow.Hydraulic.Storage.Linear storage(
A = 149000,
H_b = -1.0,
HQ.H(min = -0.7, max = 0.2),
V(nominal = 1E5)
);
Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Level sea;
Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Discharge inflow;
MyPumpingStation pumpingstation1;
// Input variables
input Modelica.SIunits.VolumeFlowRate Q_in(fixed = true);
input Modelica.SIunits.Position H_ext(fixed=true);
// Energy price is typically of units EUR/kWh (when optimizing for energy
// usage), but one can also choose for e.g. ton CO2/kWh to get the lowest
// CO2 output.
input Real energy_price(fixed=true);
// NOTE: Because we cannot flag each pump's .Q as "input", we need an extra
// variable to do this. Format is expected to be the fully specified name,
// with all dots replaced with underscores.
input Real pumpingstation1_pump1_Q;
// TODO: Move bounds to the mixin.
input Real pumpingstation1_resistance1_dH(min=0.0, max=10.0);
// Output variables
output Modelica.SIunits.Position storage_level;
output Modelica.SIunits.Position sea_level;
equation
connect(pumpingstation1.HQUp, storage.HQ);
connect(pumpingstation1.HQDown, sea.HQ);
connect(inflow.HQ, storage.HQ);
// Mapping of variables
inflow.Q = Q_in;
sea.H = H_ext;
pumpingstation1.pump1.Q = pumpingstation1_pump1_Q;
pumpingstation1.resistance1.dH = pumpingstation1_resistance1_dH;
storage_level = storage.HQ.H;
sea_level = H_ext;
end Example;
|
The attributes of pump1
are explained in detail in Pump
.
In addition to the elements, two input variables pumpingstation1_pump1_Q
and pumpingstation1_resistance1_dH
are also defined, with a set of
equations matching them to their dot-equivalent (e.g.
pumpingstation1.pump1.Q
).
Important
Because nested input
symbols cannot be detected, it is necessary for the
user to manually map this symbol to an equivalent one with dots replaced
with underscores.
The Optimization Problem¶
The python script consists of the following blocks:
- Import of packages
- Definition of water level goal
- Definition of the optimization problem class
- Constructor
- Passing a list of pumping stations
- Additional configuration of the solver
- A run statement
Importing Packages¶
For this example, the import block is as follows:
1 2 3 4 5 6 7 8 9 | import os
import sys
from rtctools.optimization.collocated_integrated_optimization_problem import CollocatedIntegratedOptimizationProblem
from rtctools.optimization.goal_programming_mixin import GoalProgrammingMixin, Goal, StateGoal
from rtctools.optimization.modelica_mixin import ModelicaMixin
from rtctools.optimization.pi_mixin import PIMixin
from rtctools.util import run_optimization_problem
from rtctools_hydraulic_structures.pumping_station_mixin import \
|
Note that we are importing both PumpingStationMixin
and PumpingStation
from rtctools_hydraulic_structures.pumping_station_mixin
.
Water Level Goal¶
Next we define our water level range goal. It reads the desired upper and
lower water levels from the optimization problem class. For more information
about how this goal maps to an objective and constraints, we refer to the
documentation of
StateGoal
.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class WaterLevelRangeGoal(StateGoal):
"""
Goal that tries to keep the water level minum and maximum water level,
the values of which are read from the optimization problem.
"""
state = 'storage.HQ.H'
priority = 1
def __init__(self, optimization_problem):
self.target_min = optimization_problem.wl_min
self.target_max = optimization_problem.wl_max
_range = self.target_max - self.target_min
self.function_range = (self.target_min - _range, self.target_max + _range)
super(WaterLevelRangeGoal, self).__init__(optimization_problem)
|
Optimization Problem¶
Then we construct the optimization problem class by declaring it and inheriting the desired parent classes.
33 34 | class Example(PumpingStationMixin, GoalProgrammingMixin, PIMixin, ModelicaMixin,
CollocatedIntegratedOptimizationProblem):
|
Now we define our pumping station objects, and store them in a local instance
variable. We refer to this instance variable from the abstract method
pumping_stations()
we have to override.
48 49 50 51 52 53 |
# Here we define a list of pumping stations, each consisting of a list
# of pumps. In our case, there is only one pumping station containing
# a single pump.
self.__pumping_stations = [PumpingStation(self, 'pumpingstation1',
pump_symbols=['pumpingstation1.pump1'])]
|
55 56 57 58 59 | def pumping_stations(self):
# This is the method that we must implement. It has to return a list of
# PumpingStation objects, which we already initialized in the __init__
# function. So here we just return that list.
return self.__pumping_stations
|
Then we append our water level range goal to the list of path goals from our parents classes:
61 62 63 64 | def path_goals(self):
goals = super(Example, self).path_goals()
goals.append(WaterLevelRangeGoal(self))
return goals
|
Note
The PumpingStationMixin
sets a minimization goal for the costs, with
priority equal to 999. There is no need to specify a minimization goal of
costs yourself.
Finally, we want to apply some additional configuration, reducing the amount of information the solver outputs:
66 67 68 69 | def solver_options(self):
options = super(Example, self).solver_options()
options['print_level'] = 2
return options
|
Run the Optimization Problem¶
To make our script run, at the bottom of our file we just have to call
the run_optimization_problem()
method we imported on the optimization
problem class we just created.
155 | run_optimization_problem(Example, base_folder='..')
|
The Whole Script¶
All together, the whole example script is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | import os
import sys
from rtctools.optimization.collocated_integrated_optimization_problem import CollocatedIntegratedOptimizationProblem
from rtctools.optimization.goal_programming_mixin import GoalProgrammingMixin, Goal, StateGoal
from rtctools.optimization.modelica_mixin import ModelicaMixin
from rtctools.optimization.pi_mixin import PIMixin
from rtctools.util import run_optimization_problem
from rtctools_hydraulic_structures.pumping_station_mixin import \
PumpingStationMixin, PumpingStation, plot_operating_points
class WaterLevelRangeGoal(StateGoal):
"""
Goal that tries to keep the water level minum and maximum water level,
the values of which are read from the optimization problem.
"""
state = 'storage.HQ.H'
priority = 1
def __init__(self, optimization_problem):
self.target_min = optimization_problem.wl_min
self.target_max = optimization_problem.wl_max
_range = self.target_max - self.target_min
self.function_range = (self.target_min - _range, self.target_max + _range)
super(WaterLevelRangeGoal, self).__init__(optimization_problem)
class Example(PumpingStationMixin, GoalProgrammingMixin, PIMixin, ModelicaMixin,
CollocatedIntegratedOptimizationProblem):
"""
An example showing the basic usage of the PumpingStationMixin. It consists of two goals:
1. Keep water level in the acceptable range.
2. Minimize power usage for doing so.
"""
# Set the target minimum and maximum water levels.
wl_min, wl_max = (-0.5, 0)
def __init__(self, *args, **kwargs):
super(Example, self).__init__(*args, **kwargs)
self.__output_folder = kwargs['output_folder'] # So we can write our pictures to it
# Here we define a list of pumping stations, each consisting of a list
# of pumps. In our case, there is only one pumping station containing
# a single pump.
self.__pumping_stations = [PumpingStation(self, 'pumpingstation1',
pump_symbols=['pumpingstation1.pump1'])]
def pumping_stations(self):
# This is the method that we must implement. It has to return a list of
# PumpingStation objects, which we already initialized in the __init__
# function. So here we just return that list.
return self.__pumping_stations
def path_goals(self):
goals = super(Example, self).path_goals()
goals.append(WaterLevelRangeGoal(self))
return goals
def solver_options(self):
options = super(Example, self).solver_options()
options['print_level'] = 2
return options
def post(self):
super(Example, self).post()
results = self.extract_results()
# TODO: Currently we use hardcoded references to pump1. It would be
# prettier if we could generalize this so we can handle an arbitrary
# number of pumps. It would also be prettier to replace hardcoded
# references to e.g. pumpingstation1.pump1__power with something like
# pumpingstation1.pump.power(), if at all possible.
# Calculate the total amount of energy used. Note that QHP fit was
# made to power in W, and that our timestep is 1 hour.
powers = results['pumpingstation1.pump1__power'][1:]
total_power = sum(powers)/1000
print("Total power = {} kWh".format(total_power))
# Make plots
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
plt.style.use('ggplot')
def unite_legends(axes):
h, l = [], []
for ax in axes:
tmp = ax.get_legend_handles_labels()
h.extend(tmp[0])
l.extend(tmp[1])
return h, l
# Plot #1: Data over time. X-axis is always time.
f, axarr = plt.subplots(4, sharex=True)
# TODO: Do not use private API of PIMixin
times = self._timeseries_import.times
axarr[0].set_ylabel('Water level\n[m]')
axarr[0].plot(times, results['storage_level'], label='Polder',
linewidth=2, color='b')
axarr[0].plot(times, self.wl_max * np.ones_like(times), label='Polder Max',
linewidth=2, color='r', linestyle='--')
axarr[0].plot(times, self.wl_min * np.ones_like(times), label='Polder Min',
linewidth=2, color='g', linestyle='--')
ymin, ymax = axarr[0].get_ylim()
axarr[0].set_ylim(ymin - 0.1, ymax + 0.1)
axarr[1].set_ylabel('Water level\n[m]')
axarr[1].plot(times, self.get_timeseries('H_ext', 0).values, label='Sea',
linewidth=2, color='b')
ymin, ymax = axarr[1].get_ylim()
axarr[1].set_ylim(ymin - 0.5, ymax + 0.5)
axarr[2].set_ylabel('Energy price\n[EUR/kWh]')
axarr[2].step(times, self.get_timeseries('energy_price', 0).values, label='Energy price',
linewidth=2, color='b')
ymin, ymax = axarr[2].get_ylim()
axarr[2].set_ylim(-0.1, ymax + 0.1)
axarr[3].set_ylabel('Discharge\n[$\mathdefault{m^3\!/s}$]')
axarr[3].step(times, results['pumpingstation1.pump1.Q'], label='Pump',
linewidth=2, color='b')
axarr[3].plot(times, self.get_timeseries('Q_in', 0).values, label='Inflow',
linewidth=2, color='g')
ymin, ymax = axarr[3].get_ylim()
axarr[3].set_ylim(-0.05 * (ymax - ymin), ymax * 1.1)
axarr[3].xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
f.autofmt_xdate()
# Shrink each axis by 20% and put a legend to the right of the axis
for i in range(len(axarr)):
box = axarr[i].get_position()
axarr[i].set_position([box.x0, box.y0, box.width * 0.8, box.height])
axarr[i].legend(loc='center left', bbox_to_anchor=(1, 0.5), frameon=False)
# Output Plot
f.set_size_inches(8, 9)
plt.savefig(os.path.join(self._output_folder, 'overall_results.png'), bbox_inches='tight', pad_inches=0.1)
# Plot the working area with the operating points of the pump.
plot_operating_points(self, self._output_folder)
# Run
run_optimization_problem(Example, base_folder='..')
|
Results¶
The results from the run are found in output/timeseries_export.xml
. Any
PI-XML-reading software can import it.
The post()
method in our Example
class also generates some pictures to
help understand what is going on.
First we have an overview of the relevant boundary conditions and control variables.

As expressed in the introduction of this example problem, we indeed see that the available buffer in the polder is used to its fullest. The water level at the final time step is (almost) equal to the maximum water level.
Furthermore, we see that the pump only discharges water when the water level is low. It is interesting to see that the optimal solution for costs means pumping at the lowest water level, even though the energy price is twice as high.
Two Pumps¶

Note
This example focuses on how to put multiple pumps in a hydraulic model, and
assumes basic exposure to RTC-Tools and the PumpingStationMixin
.
To start with basics of pump modeling, see Basic Pumping Station.
The purpose of this example is to understand the technical setup of a model with multiple pumps.
The scenario of this example is equal to that of Basic Pumping Station,
but with two pumps available instead of one. The folder
examples/pumping_station/two_pumps
contains the complete RTC- Tools
optimization problem. The discussion below will focus on the differences from
the Basic Pumping Station.
The Model¶
The pumping station object MyPumpingStation
looks as follows in its
diagram representation in OpenModelica:

When modeling multiple pumps of the same type, it makes sense to define a
model, which can then be instantiated into multiple objects. In the file
Example.mo
this can be seen in the submodel MyPump
of
MyPumpingStation
:
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | model MyPump
extends Deltares.HydraulicStructures.PumpingStation.Pump(
power_coefficients = {{3522.8, -27946.3, 54484.8},
{1665.43, 5827.81, 0.0},
{208.251, 0.0, 0.0}},
working_area = {{{ -5.326999, 54.050758, 0.000000},
{ -1.0, 0.0, 0.0}},
{{ 0.000426, -0.001241, 2.564056},
{ -1.0, 0.0, 0.0}},
{{ 2.577975, -5.203480, 0.000000},
{ -1.0, 0.0, 0.0}},
{{ 13.219650, -3.097600, -7.551339},
{ -1.0, 0.0, 0.0}}},
working_area_direction = {1, -1, -1, 1},
minimum_on=3.0*3600);
end MyPump;
|
The data of this pump is exactly equal to that used in basic-pumping-
station, but is not instantiated yet. To instantiate two pumps using this
data, we define two components pump1
and pump2
:
28 29 | MyPump pump1;
MyPump pump2;
|
Lastly, it is important not to forget to set the right number of pumps on the pumping station object:
3 4 5 6 | model MyPumpingStation
extends Deltares.HydraulicStructures.PumpingStation.PumpingStation(
n_pumps=2
);
|
The Optimization Problem¶
When using multiple pumps it is important to specify the right order of pumps.
This order should match the order of pumps in the
pump_switching_matrix
.
48 49 50 51 52 53 54 |
# Here we define a list of pumping stations, each consisting of a list
# of pumps. In our case, there is only one pumping station containing
# a single pump.
self.__pumping_stations = [PumpingStation(self, 'pumpingstation1',
pump_symbols=['pumpingstation1.pump1',
'pumpingstation1.pump2'])]
|
Weir¶
Basic Weir¶

Note
This example focuses on how to implement a controllable weir in RTC-Tools using the Hydraulic Structures library. It assumes basic exposure to RTC- Tools. If you are a first-time user of RTC-Tools, please refer to the RTC-Tools documentation.
The weir structure is valid for two flow conditions:
- Free (critical) flow
- No flow
Warning
Submerged flow is not supported.
Modeling¶
Building a model with a weir¶
In this example we are considering a system of two branches and a controllable weir in between. On the upstream side is a prescribed input flow, and on the downstream side is a prescribed output flow. The weir should move in such way that the water level in both branches is kept within the desired limits.
- To build this model, we need the following blocks:
- upstream and downstream discharge boundary conditions
- two branches
- a weir
By putting the blocks from the Modelica editor, the code is automatically generated (Note: this code snippet excludes the lines about the annotation and location):
6 7 8 9 10 11 | output Modelica.SIunits.Volume branch_2_water_level;
Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Discharge Upstream;
Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Discharge Downstream;
Deltares.ChannelFlow.Hydraulic.Reservoir.Linear Branch1(A = 50, H_b = 0, H(nominal=1, min=0, max=100));
Deltares.ChannelFlow.Hydraulic.Reservoir.Linear Branch2(A = 100, H_b = 0, H(nominal=1, min=0, max=100));
Deltares.HydraulicStructures.Weir.Weir weir1(hw_max = 3, hw_min = 1.7, q_max = 1, q_min = 0, width = 10);
|
For the weir block, the dimensions of the weir should be set. It can be done either by double
clicking to the block, or in the text editor.
A controllable weir is represented with a weir block. This block has discharge and water level as input,
and also as output. When a block is placed, the following parameters can be given:
- width
: the width of the crest in meters
- hw_min
: the minimum crest height
- hw_max
: the maximum crest height
- q_min
: the minimum expected discharge
- q_max
: the maximum expected discharge
The last two values should be estimated in such way that the discharge will not be able to go outside these bounds. However, for some linearization purposes, they should be as tight as possible. The values set by the text editor look like the line above.
The input variables are the upstream and downstream (known) discharges. The control variable - the variable that the algorithm changes until it achieves the desired results - is the discharge between the two branches. In practice, the weir height is the variable that we are interested in, but as it depends on the discharge between the two branches and the upstream water level, it will only be calculated in post processing. The input variables for the model are:
2 3 4 | input Modelica.SIunits.VolumeFlowRate upstream_q_ext(fixed = true);
input Modelica.SIunits.VolumeFlowRate downstream_q_ext(fixed = true);
input Modelica.SIunits.VolumeFlowRate WeirFlow1(fixed = false, nominal=1, min=0, max=2.5);
|
Important
The min, max and nominal the values should always be meaningful. For nominal, set the value that the variable most likely takes.
As output, we are interested in the water level in the two branches:
5 6 | output Modelica.SIunits.Volume branch_1_water_level;
output Modelica.SIunits.Volume branch_2_water_level;
|
Now we have to define the equations. We have to set the boundary conditions. First the discharge should be read from the external files:
21 22 | Upstream.Q = upstream_q_ext;
Downstream.Q = downstream_q_ext;
|
And then the water level should be defined equal to the water level in the branch:
17 18 | Branch1.HQDown.H=Branch1.H;
Branch2.HQDown.H=Branch2.H;
|
As we use reservoirs for branches, the variables we do not need should be zero:
19 20 | Branch1.Q_turbine=0;
Branch2.Q_turbine=0;
|
Finally the outputs are set:
24 25 | branch_1_water_level = Branch1.H;
branch_2_water_level = Branch2.H;
|
and the control variable as well:
23 | WeirFlow1 = weir1.Q;
|
The whole model file looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | model WeirExample
input Modelica.SIunits.VolumeFlowRate upstream_q_ext(fixed = true);
input Modelica.SIunits.VolumeFlowRate downstream_q_ext(fixed = true);
input Modelica.SIunits.VolumeFlowRate WeirFlow1(fixed = false, nominal=1, min=0, max=2.5);
output Modelica.SIunits.Volume branch_1_water_level;
output Modelica.SIunits.Volume branch_2_water_level;
Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Discharge Upstream;
Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Discharge Downstream;
Deltares.ChannelFlow.Hydraulic.Reservoir.Linear Branch1(A = 50, H_b = 0, H(nominal=1, min=0, max=100));
Deltares.ChannelFlow.Hydraulic.Reservoir.Linear Branch2(A = 100, H_b = 0, H(nominal=1, min=0, max=100));
Deltares.HydraulicStructures.Weir.Weir weir1(hw_max = 3, hw_min = 1.7, q_max = 1, q_min = 0, width = 10);
equation
connect(weir1.HQDown, Branch2.HQUp);
connect(Branch1.HQDown, weir1.HQUp);
connect(Branch2.HQDown, Downstream.HQ);
connect(Upstream.HQ, Branch1.HQUp);
Branch1.HQDown.H=Branch1.H;
Branch2.HQDown.H=Branch2.H;
Branch1.Q_turbine=0;
Branch2.Q_turbine=0;
Upstream.Q = upstream_q_ext;
Downstream.Q = downstream_q_ext;
WeirFlow1 = weir1.Q;
branch_1_water_level = Branch1.H;
branch_2_water_level = Branch2.H;
end WeirExample;
|
Optimization¶
In this example, we would like to achieve that the water levels in the branches stay in the prescribed
limits. The easiest way to achieve this objective is through goal programming. We will define two goals,
one goal for each branch. The goal is that the water level should be higher than the given minimum and
lower than the given maximum. Any solution satisfying these criteria is equally attractive for us.
In practice, in goal programming the goal violation value is taken to the order’th power in the objective function
(see: Goal Programming).
In our example, we use the file WeirExample.py
. We define a class, and apart from the usual classes
that we import for optimization problems, we also have to import the class WeirMixin
:
37 38 39 40 | class WeirExample(WeirMixin, GoalProgrammingMixin, CSVMixin, ModelicaMixin, CollocatedIntegratedOptimizationProblem):
def __init__(self, *args, **kwargs):
super(WeirExample, self).__init__(*args, **kwargs)
|
Now we have to define the weirs: in quotation marks should be the same name as used for the Modelica model. Now there is only one weir, and the definition looks like:
41 | self.__weirs = [Weir(self, 'weir1')]
|
In case of more weirs, the names can be separated with a comma, for example:
self._weirs = [Weir('weir1'), Weir('weir2')]
Lastly we have to override the abstract method the returns the list of weirs:
44 45 | def weirs(self):
return self.__weirs
|
Adding goals¶
In this example there are two branches connected with a weir. On the upstream side is a prescribed input flow, and on the downstream side is a prescribed output flow. The weir should move in such way, that the water level in both branches kept within the desired limits. We can add a water level goal for the upstream branch:
13 14 15 16 17 18 19 20 21 22 | class WLRangeGoal_01(StateGoal):
def __init__(self, optimization_problem):
self.state = 'Branch1.H'
self.priority = 1
self.target_min = optimization_problem.get_timeseries('h_min_branch1')
self.target_max = optimization_problem.get_timeseries('h_max_branch1')
super(WLRangeGoal_01, self).__init__(optimization_problem)
|
A similar goal can be added to the downstream branch.
Setting the solver¶
As it is a mixed integer problem, it is handy to set some options to control
the solver. In this example, we set the allowable_gap
to 0.005. It is used
to specify the value of absolute gap under which the algorithm stops. This is
bigger than the default. This gives lower expectations for the acceptable
solution, and in this way, the time of iteration is less. This value might be
different for every problem and might be adjusted a trial-and-error basis. For
more information, see the documentation of the BONMIN solver User’s Manual)
The solver setting is the following:
47 48 49 50 51 | def solver_options(self):
options = super(WeirExample, self).solver_options()
options['allowable_gap'] = 0.005
options['print_level'] = 2
return options
|
Input data¶
In order to run the optimization, we need to give the boundary conditions and the water level bounds.
This data is given as time-series in the file timeseries_import.csv
.
The whole python file¶
The optimization file looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | from rtctools.optimization.collocated_integrated_optimization_problem \
import CollocatedIntegratedOptimizationProblem
from rtctools.optimization.goal_programming_mixin import GoalProgrammingMixin, StateGoal
from rtctools.optimization.modelica_mixin import ModelicaMixin
from rtctools.optimization.csv_mixin import CSVMixin
from rtctools.util import run_optimization_problem
from rtctools_hydraulic_structures.weir_mixin import WeirMixin, Weir, plot_operating_points
# There are two water level targets, with different priority.
# The water level should stay in the required range during all the simulation
class WLRangeGoal_01(StateGoal):
def __init__(self, optimization_problem):
self.state = 'Branch1.H'
self.priority = 1
self.target_min = optimization_problem.get_timeseries('h_min_branch1')
self.target_max = optimization_problem.get_timeseries('h_max_branch1')
super(WLRangeGoal_01, self).__init__(optimization_problem)
class WLRangeGoal_02(StateGoal):
def __init__(self, optimization_problem):
self.state = 'Branch2.H'
self.priority = 2
self.target_min = optimization_problem.get_timeseries('h_min_branch2')
self.target_max = optimization_problem.get_timeseries('h_max_branch2')
super(WLRangeGoal_02, self).__init__(optimization_problem)
class WeirExample(WeirMixin, GoalProgrammingMixin, CSVMixin, ModelicaMixin, CollocatedIntegratedOptimizationProblem):
def __init__(self, *args, **kwargs):
super(WeirExample, self).__init__(*args, **kwargs)
self.__weirs = [Weir(self, 'weir1')]
self.__output_folder = kwargs['output_folder'] # So we can write our pictures to it
def weirs(self):
return self.__weirs
def solver_options(self):
options = super(WeirExample, self).solver_options()
options['allowable_gap'] = 0.005
options['print_level'] = 2
return options
def path_goals(self):
goals = super(WeirExample, self).path_goals()
goals.append(WLRangeGoal_01(self))
goals.append(WLRangeGoal_02(self))
return goals
def post(self):
super(WeirExample, self).post()
results = self.extract_results()
# Make plots
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import os
plt.style.use('ggplot')
def unite_legends(axes):
h, l = [], []
for ax in axes:
tmp = ax.get_legend_handles_labels()
h.extend(tmp[0])
l.extend(tmp[1])
return h, l
# Plot #1: Data over time. X-axis is always time.
f, axarr = plt.subplots(4, sharex=True)
# TODO: Do not use private API of CSVMixin
times = self._timeseries_times
weir = self.weirs()[0]
axarr[0].set_ylabel('Water level\n[m]')
axarr[0].plot(times, results['branch_1_water_level'], label='Upstream',
linewidth=2, color='b')
axarr[0].plot(times, self.get_timeseries('h_min_branch1').values, label='Upstream Max',
linewidth=2, color='r', linestyle='--')
axarr[0].plot(times, self.get_timeseries('h_max_branch1').values, label='Upstream Min',
linewidth=2, color='g', linestyle='--')
ymin, ymax = axarr[0].get_ylim()
axarr[0].set_ylim(ymin - 0.1, ymax + 0.1)
axarr[1].set_ylabel('Water level\n[m]')
axarr[1].plot(times, results['branch_2_water_level'], label='Downstream',
linewidth=2, color='b')
axarr[1].plot(times, self.get_timeseries('h_max_branch2').values, label='Downstream Max',
linewidth=2, color='r', linestyle='--')
axarr[1].plot(times, self.get_timeseries('h_min_branch2').values, label='Downstream Min',
linewidth=2, color='g', linestyle='--')
ymin, ymax = axarr[1].get_ylim()
axarr[1].set_ylim(ymin - 0.1, ymax + 0.1)
axarr[2].set_ylabel('Discharge\n[$\mathdefault{m^3\!/s}$]')
# We need the first point for plotting, but its value does not
# necessarily make sense as it is not included in the optimization.
weir_flow = results['WeirFlow1']
weir_height = results["weir1_height"]
minimum_water_level_above_weir = (weir_flow-weir.q_nom)/weir.slope + weir.h_nom
minimum_weir_height = minimum_water_level_above_weir - ((weir_flow/weir.c_weir)**(2.0/3.0))
minimum_weir_height[0] = minimum_weir_height[1]
weir_flow = results['WeirFlow1']
weir_flow[0] = weir_flow[1]
axarr[2].step(times, weir_flow, label='Weir',
linewidth=2, color='b')
axarr[2].step(times, self.get_timeseries('upstream_q_ext').values, label='Inflow',
linewidth=2, color='r', linestyle='--')
axarr[2].step(times, -1 * self.get_timeseries('downstream_q_ext').values, label='Outflow',
linewidth=2, color='g', linestyle='--')
ymin, ymax = axarr[2].get_ylim()
axarr[2].set_ylim(-0.1, ymax + 0.1)
weir_height = results["weir1_height"]
weir_height[0] = weir_height[1]
axarr[3].set_ylabel('Weir height\n[m]')
axarr[3].step(times, weir_height, label='Weir',
linewidth=2, color='b')
ymin, ymax = axarr[3].get_ylim()
ymargin = 0.1 * (ymax - ymin)
axarr[3].set_ylim(ymin - ymargin, ymax + ymargin)
axarr[3].xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
f.autofmt_xdate()
# Shrink each axis by 20% and put a legend to the right of the axis
for i in range(len(axarr)):
box = axarr[i].get_position()
axarr[i].set_position([box.x0, box.y0, box.width * 0.8, box.height])
axarr[i].legend(loc='center left', bbox_to_anchor=(1, 0.5), frameon=False)
# Output Plot
f.set_size_inches(8, 9)
plt.savefig(os.path.join(self.__output_folder, 'overall_results.png'),
bbox_inches='tight', pad_inches=0.1)
plot_operating_points(self, self._output_folder, results)
if __name__ == "__main__":
run_optimization_problem(WeirExample)
|
Results¶
After successful optimization the results are printed in the time series export file. After running this example, the following results are expected:

The file found in the example folder includes some visualization routines.
Interpretation of the results¶
The results of this simulation are summarized in the following figure:

In this example, the input flow increases after 6 minutes, while the downstream flow is kept constant. While the inflow drops after 6 minutes, the result is not seen in the upstream branch, because the weir moves up to compensate it. After the weir moved up, the water level drops in the downstream branch.