Cascading Channels: Modeling Channel Hydraulics
Note
This is a more advanced example that implements multi-objective optimization in RTC-Tools. It also capitalizes on the homotopy techniques available in RTC-Tools. If you are a first-time user of RTC-Tools, see Filling a Reservoir.
Goal programming is a way to satisfy (sometimes conflicting) goals by ranking the goals by priority. In this example, we specify two goals. The higher priority goal will be to maintain the water levels in the channels within a desired band. The lower priority goal will be to extract water to meet a forecasted drinking water demand.
The Model
For this example, water is flowing through a multilevel channel system. The model has three channel sections. There is an extraction pump at the downstream end of the middle channel. The algorithm will first attempt to maintain water levels in the channels within the desired water level band. Using the remaining flexibility in the model, the algorithm will attempt to meet the diurnal demand pattern as best as it can with the extraction pump.
In OpenModelica Connection Editor, the model looks like this:
In text mode, the Modelica model looks as follows (with annotation statements removed):
1model Example
2 // Model Elements
3 Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Discharge Inflow;
4 Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Level DrinkingWaterPlant(H = 10.);
5 Deltares.ChannelFlow.Hydraulic.BoundaryConditions.Level Level;
6 Deltares.ChannelFlow.Hydraulic.Branches.HomotopicLinear LowerChannel(
7 H(each max = 1.0),
8 H_b_down = -2.0,
9 H_b_up = -1.5,
10 friction_coefficient = 10.,
11 length = 2000.,
12 theta = theta,
13 uniform_nominal_depth = 1.75,
14 width_down = 10.,
15 width_up = 10.,
16 semi_implicit_step_size = step_size,
17 Q_nominal = 1.0
18 );
19 Deltares.ChannelFlow.Hydraulic.Branches.HomotopicLinear MiddleChannel(
20 H(each max = 1.5),
21 H_b_down = -1.5,
22 H_b_up = -1.0,
23 friction_coefficient = 10.,
24 length = 2000.,
25 theta = theta,
26 uniform_nominal_depth = 1.75,
27 width_down = 10.,
28 width_up = 10.,
29 semi_implicit_step_size = step_size,
30 Q_nominal = 1.0
31 );
32 Deltares.ChannelFlow.Hydraulic.Branches.HomotopicLinear UpperChannel(
33 H(each max = 2.0),
34 H_b_down = -1.0,
35 H_b_up = -0.5,
36 friction_coefficient = 10.,
37 length = 2000.,
38 theta = theta,
39 uniform_nominal_depth = 1.75,
40 width_down = 10.,
41 width_up = 10.,
42 semi_implicit_step_size = step_size,
43 Q_nominal = 1.0
44 );
45 Deltares.ChannelFlow.Hydraulic.Structures.Pump DrinkingWaterExtractionPump;
46 Deltares.ChannelFlow.Hydraulic.Structures.Pump LowerControlStructure;
47 Deltares.ChannelFlow.Hydraulic.Structures.Pump UpperControlStructure;
48 // Parameters
49 parameter Real theta;
50 parameter Real step_size;
51 // Inputs
52 input Real Inflow_Q(fixed = true) = Inflow.Q;
53 input Real UpperControlStructure_Q(fixed = false, min = 0., max = 4.) = UpperControlStructure.Q;
54 input Real LowerControlStructure_Q(fixed = false, min = 0., max = 4.) = LowerControlStructure.Q;
55 input Real DrinkingWaterExtractionPump_Q(fixed = false, min = 0., max = 2.) = DrinkingWaterExtractionPump.Q;
56initial equation
57 // Steady state initialization
58 der(LowerChannel.Q) = 0;
59 der(MiddleChannel.Q) = 0;
60 der(UpperChannel.Q) = 0;
61equation
62 connect(DrinkingWaterExtractionPump.HQDown, DrinkingWaterPlant.HQ);
63 connect(Inflow.HQ, UpperChannel.HQUp);
64 connect(LowerChannel.HQDown, Level.HQ);
65 connect(LowerControlStructure.HQDown, LowerChannel.HQUp);
66 connect(MiddleChannel.HQDown, DrinkingWaterExtractionPump.HQUp);
67 connect(MiddleChannel.HQDown, LowerControlStructure.HQUp);
68 connect(UpperChannel.HQDown, UpperControlStructure.HQUp);
69 connect(UpperControlStructure.HQDown, MiddleChannel.HQUp);
70end Example;
Important
Modellers should take care to set proper values for the initial derivatives, in order to avoid spurious waves at the start of the optimization run. In this example we assume a steady state initial condition for all states except those acted upon by the PID controllers.
The Optimization Problem
The python script consists of the following blocks:
Import of packages
Declaration of Goals
Declaration of the optimization problem class
Constructor
Implementation of
pre()
methodImplementation of
parameters()
methodImplementation of
path_goals()
method
A run statement
Goals
In this model, we define two generic StateGoal subclasses:
13class RangeGoal(StateGoal):
14 def __init__(self, opt_prob, state, priority):
15 self.state = state
16 self.target_min = opt_prob.get_timeseries(state + "_min")
17 self.target_max = opt_prob.get_timeseries(state + "_max")
18 self.violation_timeseries_id = state + "_target_violation"
19 self.function_value_timeseries_id = state
20 self.priority = priority
21 super().__init__(opt_prob)
24class TargetGoal(StateGoal):
25 def __init__(self, opt_prob, state, priority):
26 self.state = state
27 self.target_min = opt_prob.get_timeseries(state + "_target")
28 self.target_max = self.target_min
29 self.violation_timeseries_id = state + "_target_violation"
30 self.function_value_timeseries_id = state
31 self.priority = priority
32 super().__init__(opt_prob)
These goals are actually really similar. The only difference is that the
TargetGoal
uses the same timeseries for its target_max
and
target_min
attributes. This goal will try to minimize the difference between
the target and the goal’s state. This is in contrast to the RangeGoal
, which
has a separate min and max that define an acceptable range of values.
You can read more about the components of goals in the documentation: Multi-objective optimization.
Optimization Problem
We construct the class by declaring it and inheriting the desired parent classes.
35class Example(
36 HomotopyMixin,
37 GoalProgrammingMixin,
38 CSVMixin,
39 ModelicaMixin,
40 CollocatedIntegratedOptimizationProblem,
41):
In our new class, we implement the pre()
method. This method is a good place
to do some preprocessing of the data to make sure it is all there when the model
runs.
45 def pre(self):
46 super().pre()
47 # Generate handy tuples to iterate over
48 self.channel_node_indices = tuple(range(1, self.channel_n_level_nodes + 1))
49 self.channel_level_nodes = tuple(
50 "{}.H[{}]".format(c, n)
51 for c, n in itertools.product(self.channels, self.channel_node_indices)
52 )
53 # Expand channel water level goals to all nodes
54 for channel in self.channels:
55 channel_max = self.get_timeseries(channel + "_max")
56 channel_min = self.get_timeseries(channel + "_min")
57 for i in self.channel_node_indices:
58 self.set_timeseries("{}.H[{}]_max".format(channel, i), channel_max)
59 self.set_timeseries("{}.H[{}]_min".format(channel, i), channel_min)
60 # Make input series appear in output csv
61 self.set_timeseries("Inflow_Q", self.get_timeseries("Inflow_Q"))
62 self.set_timeseries(
63 "DrinkingWaterExtractionPump_Q_target",
64 self.get_timeseries("DrinkingWaterExtractionPump_Q_target"),
65 )
Next, we implement the parameters()
method. This method passes parameter values
down to the model. The model uses the step size parameter to perform a semi-implicit
discretization of the hydraulic equations. We set the step_size
parameter value to
match the time step size in the input time series.
67 def parameters(self, ensemble_member):
68 p = super().parameters(ensemble_member)
69 times = self.times()
70 p["step_size"] = times[1] - times[0]
71 return p
Finally, we instantiate the goals. The highest priority goal in this example will be to keep the water levels within a desired range. We apply this goal iteratively over all the water level states, and give them a priority of 1. The second goal is to track a target extraction flow rate with the extraction pump. We give this goal a priority of 2.
73 def path_goals(self):
74 g = super().path_goals()
75
76 # Add RangeGoal on water level states with a priority of 1
77 for node in self.channel_level_nodes:
78 g.append(RangeGoal(self, node, 1))
79
80 # Add TargetGoal on Extraction Pump with a priority of 2
81 g.append(TargetGoal(self, "DrinkingWaterExtractionPump_Q", 2))
82
83 return g
We want to apply these goals to every timestep, so we use the path_goals()
method. This is a method that returns a list of the path goals we defined above.
Note that with path goals, each timestep is implemented as an independent goal—
if we cannot satisfy our min/max on time step A, it will not affect our desire
to satisfy the goal at time step B. Goals that inherit StateGoal
are always
path goals.
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.
90run_optimization_problem(Example)
The Whole Script
All together, the whole example script is as follows:
1import itertools
2
3from rtctools.optimization.collocated_integrated_optimization_problem import (
4 CollocatedIntegratedOptimizationProblem,
5)
6from rtctools.optimization.csv_mixin import CSVMixin
7from rtctools.optimization.goal_programming_mixin import GoalProgrammingMixin, StateGoal
8from rtctools.optimization.homotopy_mixin import HomotopyMixin
9from rtctools.optimization.modelica_mixin import ModelicaMixin
10from rtctools.util import run_optimization_problem
11
12
13class RangeGoal(StateGoal):
14 def __init__(self, opt_prob, state, priority):
15 self.state = state
16 self.target_min = opt_prob.get_timeseries(state + "_min")
17 self.target_max = opt_prob.get_timeseries(state + "_max")
18 self.violation_timeseries_id = state + "_target_violation"
19 self.function_value_timeseries_id = state
20 self.priority = priority
21 super().__init__(opt_prob)
22
23
24class TargetGoal(StateGoal):
25 def __init__(self, opt_prob, state, priority):
26 self.state = state
27 self.target_min = opt_prob.get_timeseries(state + "_target")
28 self.target_max = self.target_min
29 self.violation_timeseries_id = state + "_target_violation"
30 self.function_value_timeseries_id = state
31 self.priority = priority
32 super().__init__(opt_prob)
33
34
35class Example(
36 HomotopyMixin,
37 GoalProgrammingMixin,
38 CSVMixin,
39 ModelicaMixin,
40 CollocatedIntegratedOptimizationProblem,
41):
42 channels = "LowerChannel", "MiddleChannel", "UpperChannel"
43 channel_n_level_nodes = 2
44
45 def pre(self):
46 super().pre()
47 # Generate handy tuples to iterate over
48 self.channel_node_indices = tuple(range(1, self.channel_n_level_nodes + 1))
49 self.channel_level_nodes = tuple(
50 "{}.H[{}]".format(c, n)
51 for c, n in itertools.product(self.channels, self.channel_node_indices)
52 )
53 # Expand channel water level goals to all nodes
54 for channel in self.channels:
55 channel_max = self.get_timeseries(channel + "_max")
56 channel_min = self.get_timeseries(channel + "_min")
57 for i in self.channel_node_indices:
58 self.set_timeseries("{}.H[{}]_max".format(channel, i), channel_max)
59 self.set_timeseries("{}.H[{}]_min".format(channel, i), channel_min)
60 # Make input series appear in output csv
61 self.set_timeseries("Inflow_Q", self.get_timeseries("Inflow_Q"))
62 self.set_timeseries(
63 "DrinkingWaterExtractionPump_Q_target",
64 self.get_timeseries("DrinkingWaterExtractionPump_Q_target"),
65 )
66
67 def parameters(self, ensemble_member):
68 p = super().parameters(ensemble_member)
69 times = self.times()
70 p["step_size"] = times[1] - times[0]
71 return p
72
73 def path_goals(self):
74 g = super().path_goals()
75
76 # Add RangeGoal on water level states with a priority of 1
77 for node in self.channel_level_nodes:
78 g.append(RangeGoal(self, node, 1))
79
80 # Add TargetGoal on Extraction Pump with a priority of 2
81 g.append(TargetGoal(self, "DrinkingWaterExtractionPump_Q", 2))
82
83 return g
84
85 def post(self):
86 super().post()
87
88
89# Run
90run_optimization_problem(Example)
Extracting Results
The results from the run are found in output/timeseries_export.csv
. Any
CSV-reading software can import it, but this is how results can be plotted using
the python library matplotlib:
(Source code
, svg
, png
)