diff --git a/core/src/callback.jl b/core/src/callback.jl index 22c5fbb49..f9dbd438a 100644 --- a/core/src/callback.jl +++ b/core/src/callback.jl @@ -1,35 +1,3 @@ -""" -Set parameters of nodes that are controlled by DiscreteControl to the -values corresponding to the initial state of the model. -""" -function set_initial_discrete_controlled_parameters!( - integrator, - storage0::Vector{Float64}, -)::Nothing - (; p) = integrator - (; discrete_control) = p - - n_conditions = sum(length(vec) for vec in discrete_control.condition_value; init = 0) - condition_diffs = zeros(Float64, n_conditions) - discrete_control_condition(condition_diffs, storage0, integrator.t, integrator) - - # Set the discrete control value (bool) per compound variable - idx_start = 1 - for (compound_variable_idx, vec) in enumerate(discrete_control.condition_value) - l = length(vec) - idx_end = idx_start + l - 1 - discrete_control.condition_value[compound_variable_idx] .= - (condition_diffs[idx_start:idx_end] .> 0) - idx_start += l - end - - # For every discrete_control node find a condition_idx it listens to - for discrete_control_node_id in unique(discrete_control.node_id) - condition_idx = - searchsortedfirst(discrete_control.node_id, discrete_control_node_id) - discrete_control_affect!(integrator, condition_idx, missing) - end -end """ Create the different callbacks that are used to store results @@ -89,13 +57,7 @@ function create_callbacks( n_conditions = sum(length(vec) for vec in discrete_control.greater_than; init = 0) if n_conditions > 0 - discrete_control_cb = VectorContinuousCallback( - discrete_control_condition, - discrete_control_affect_upcrossing!, - discrete_control_affect_downcrossing!, - n_conditions; - save_positions = (false, false), - ) + discrete_control_cb = FunctionCallingCallback(apply_discrete_control!) push!(callbacks, discrete_control_cb) end callback = CallbackSet(callbacks...) @@ -186,33 +148,50 @@ function save_vertical_flux(u, t, integrator) return vertical_flux_mean end +function apply_discrete_control!(u, t, integrator)::Nothing + (; p) = integrator + (; discrete_control) = p + condition_idx = 0 + + discrete_control_condition!(u, t, integrator) + + # For every compound variable see whether it changes a control state + for compound_variable_idx in eachindex(discrete_control.node_id) + discrete_control_affect!(integrator, compound_variable_idx) + end +end + """ -Listens for changes in condition truths. +Update discrete control condition truths. """ -function discrete_control_condition(out, u, t, integrator) +function discrete_control_condition!(u, t, integrator) (; p) = integrator (; discrete_control) = p - condition_idx = 0 # Loop over compound variables - for (listen_node_ids, variables, weights, greater_thans, look_aheads) in zip( + for ( + listen_node_ids, + variables, + weights, + greater_thans, + look_aheads, + condition_values, + ) in zip( discrete_control.listen_node_id, discrete_control.variable, discrete_control.weight, discrete_control.greater_than, discrete_control.look_ahead, + discrete_control.condition_value, ) value = 0.0 for (listen_node_id, variable, weight, look_ahead) in zip(listen_node_ids, variables, weights, look_aheads) value += weight * get_value(p, listen_node_id, variable, look_ahead, u, t) end - # Loop over greater_than values for this compound_variable - for greater_than in greater_thans - condition_idx += 1 - diff = value - greater_than - out[condition_idx] = diff - end + + condition_values .= false + condition_values[1:searchsortedlast(greater_thans, value)] .= true end end @@ -262,105 +241,14 @@ function get_value( return value end -""" -An upcrossing means that a condition (always greater than) becomes true. -""" -function discrete_control_affect_upcrossing!(integrator, condition_idx) - (; p, u, t) = integrator - (; discrete_control, basin) = p - (; variable, condition_value, listen_node_id) = discrete_control - - compound_variable_idx, greater_than_idx = - get_discrete_control_indices(discrete_control, condition_idx) - condition_value[compound_variable_idx][greater_than_idx] = true - - control_state_change = discrete_control_affect!(integrator, condition_idx, true) - - # Check whether the control state change changed the direction of the crossing - # NOTE: This works for level conditions, but not for flow conditions on an - # arbitrary edge. That is because parameter changes do not change the instantaneous level, - # only possibly the du. Parameter changes can change the flow on an edge discontinuously, - # giving the possibility of logical paradoxes where certain parameter changes immediately - # undo the truth state that caused that parameter change. - listen_node_ids = discrete_control.listen_node_id[compound_variable_idx] - is_basin = - length(listen_node_ids) == 1 ? id_index(basin.node_id, only(listen_node_ids))[1] : - false - - # NOTE: The above no longer works when listen feature ids can be something other than node ids - # I think the more durable option is to give all possible condition types a different variable string, - # e.g. basin.level and level_boundary.level - if variable[compound_variable_idx][1] == "level" && control_state_change && is_basin - # Calling water_balance is expensive, but it is a sure way of getting - # du for the basin of this level condition - du = zero(u) - water_balance!(du, u, p, t) - _, condition_basin_idx = - id_index(basin.node_id, listen_node_id[compound_variable_idx][1]) - - if du[condition_basin_idx] < 0.0 - condition_value[compound_variable_idx][greater_than_idx] = false - discrete_control_affect!(integrator, condition_idx, false) - end - end -end - -""" -An downcrossing means that a condition (always greater than) becomes false. -""" -function discrete_control_affect_downcrossing!(integrator, condition_idx) - (; p, u, t) = integrator - (; discrete_control, basin) = p - (; variable, condition_value, listen_node_id) = discrete_control - - compound_variable_idx, greater_than_idx = - get_discrete_control_indices(discrete_control, condition_idx) - condition_value[compound_variable_idx][greater_than_idx] = false - - control_state_change = discrete_control_affect!(integrator, condition_idx, false) - - # Check whether the control state change changed the direction of the crossing - # NOTE: This works for level conditions, but not for flow conditions on an - # arbitrary edge. That is because parameter changes do not change the instantaneous level, - # only possibly the du. Parameter changes can change the flow on an edge discontinuously, - # giving the possibility of logical paradoxes where certain parameter changes immediately - # undo the truth state that caused that parameter change. - compound_variable_idx, greater_than_idx = - get_discrete_control_indices(discrete_control, condition_idx) - listen_node_ids = discrete_control.listen_node_id[compound_variable_idx] - is_basin = - length(listen_node_ids) == 1 ? id_index(basin.node_id, only(listen_node_ids))[1] : - false - - if variable[compound_variable_idx][1] == "level" && control_state_change && is_basin - # Calling water_balance is expensive, but it is a sure way of getting - # du for the basin of this level condition - du = zero(u) - water_balance!(du, u, p, t) - has_index, condition_basin_idx = - id_index(basin.node_id, listen_node_id[compound_variable_idx][1]) - - if has_index && du[condition_basin_idx] > 0.0 - condition_value[compound_variable_idx][greater_than_idx] = true - discrete_control_affect!(integrator, condition_idx, true) - end - end -end - """ Change parameters based on the control logic. """ -function discrete_control_affect!( - integrator, - condition_idx::Int, - upcrossing::Union{Bool, Missing}, -)::Bool +function discrete_control_affect!(integrator, compound_variable_idx) p = integrator.p (; discrete_control, graph) = p - # Get the discrete_control node that listens to this condition - - compound_variable_idx, _ = get_discrete_control_indices(discrete_control, condition_idx) + # Get the discrete_control node to which this compound variable belongs discrete_control_node_id = discrete_control.node_id[compound_variable_idx] # Get the indices of all conditions that this control node listens to @@ -376,56 +264,24 @@ function discrete_control_affect!( ) truth_state = join(truth_values, "") - # Get the truth specific about the latest crossing - if !ismissing(upcrossing) - truth_value_idx = - condition_idx - sum( - length(vec) for - vec in discrete_control.condition_value[1:(where_node_id.start - 1)]; - init = 0, - ) - truth_values[truth_value_idx] = upcrossing ? "U" : "D" - end - truth_state_crossing_specific = join(truth_values, "") - # What the local control state should be control_state_new = - if haskey( - discrete_control.logic_mapping, - (discrete_control_node_id, truth_state_crossing_specific), - ) - truth_state_used = truth_state_crossing_specific - discrete_control.logic_mapping[( - discrete_control_node_id, - truth_state_crossing_specific, - )] - elseif haskey( - discrete_control.logic_mapping, - (discrete_control_node_id, truth_state), - ) - truth_state_used = truth_state + if haskey(discrete_control.logic_mapping, (discrete_control_node_id, truth_state)) discrete_control.logic_mapping[(discrete_control_node_id, truth_state)] else error( - "Control state specified for neither $truth_state_crossing_specific nor $truth_state for $discrete_control_node_id.", + "No control state specified for $discrete_control_node_id for truth state $truth_state.", ) end - # What the local control state is - # TODO: Check time elapsed since control change control_state_now, _ = discrete_control.control_state[discrete_control_node_id] - - control_state_change = false - if control_state_now != control_state_new - control_state_change = true - # Store control action in record record = discrete_control.record push!(record.time, integrator.t) push!(record.control_node_id, Int32(discrete_control_node_id)) - push!(record.truth_state, truth_state_used) + push!(record.truth_state, truth_state) push!(record.control_state, control_state_new) # Loop over nodes which are under control of this control node @@ -437,7 +293,7 @@ function discrete_control_affect!( discrete_control.control_state[discrete_control_node_id] = (control_state_new, integrator.t) end - return control_state_change + return nothing end function get_allocation_model(p::Parameters, allocation_network_id::Int32)::AllocationModel diff --git a/core/src/model.jl b/core/src/model.jl index 7ac16aea1..6c20d31d6 100644 --- a/core/src/model.jl +++ b/core/src/model.jl @@ -162,8 +162,6 @@ function Model(config::Config)::Model @show Ribasim.to end - set_initial_discrete_controlled_parameters!(integrator, storage) - return Model(integrator, config, saved) end diff --git a/core/src/util.jl b/core/src/util.jl index 57482cca1..f8ebeabe3 100644 --- a/core/src/util.jl +++ b/core/src/util.jl @@ -421,7 +421,7 @@ function expand_logic_mapping( logic_mapping_expanded = Dict{Tuple{NodeID, String}, String}() for (node_id, truth_state) in keys(logic_mapping) - pattern = r"^[TFUD\*]+$" + pattern = r"^[TF\*]+$" if !occursin(pattern, truth_state) error("Truth state \'$truth_state\' contains illegal characters or is empty.") end diff --git a/core/src/validation.jl b/core/src/validation.jl index ae039a561..dc39244fc 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -563,7 +563,6 @@ function valid_discrete_control(p::Parameters, config::Config)::Bool if !isempty(undefined_control_states) undefined_list = collect(undefined_control_states) - node_type = typeof(node).name.name @error "These control states from $id are not defined for controlled $id_outneighbor: $undefined_list." errors = true end diff --git a/core/test/control_test.jl b/core/test/control_test.jl index 21bcc653c..1bee012a9 100644 --- a/core/test/control_test.jl +++ b/core/test/control_test.jl @@ -145,41 +145,11 @@ end @test discrete_control.record.control_state == ["high", "low"] @test discrete_control.record.time[1] == 0.0 t = Ribasim.datetime_since(discrete_control.record.time[2], model.config.starttime) - @test Date(t) == Date("2020-03-15") + @test Date(t) == Date("2020-03-16") # then the rating curve is updated to the "low" control_state @test only(p.tabulated_rating_curve.tables).t[2] == 1.2 end -@testitem "Setpoint with bounds control" begin - toml_path = normpath( - @__DIR__, - "../../generated_testmodels/level_setpoint_with_minmax/ribasim.toml", - ) - @test ispath(toml_path) - model = Ribasim.run(toml_path) - p = model.integrator.p - (; discrete_control) = p - (; record, greater_than) = discrete_control - level = Ribasim.get_storages_and_levels(model).level[1, :] - t = Ribasim.tsaves(model) - - t_none_1 = discrete_control.record.time[2] - t_in = discrete_control.record.time[3] - t_none_2 = discrete_control.record.time[4] - - level_min = greater_than[1][1] - setpoint = greater_than[1][2] - - t_1_none_index = findfirst(>=(t_none_1), t) - t_in_index = findfirst(>=(t_in), t) - t_2_none_index = findfirst(>=(t_none_2), t) - - @test record.control_state == ["out", "none", "in", "none"] - @test level[t_1_none_index] <= setpoint - @test level[t_in_index] >= level_min - @test level[t_2_none_index] <= setpoint -end - @testitem "Set PID target with DiscreteControl" begin using Ribasim: NodeID diff --git a/docs/python/examples.ipynb b/docs/python/examples.ipynb index 55e8b3244..5e1eb45ce 100644 --- a/docs/python/examples.ipynb +++ b/docs/python/examples.ipynb @@ -440,7 +440,7 @@ "source": [ "# Model with discrete control\n", "\n", - "The model constructed below consists of a single basin which slowly drains trough a `TabulatedRatingCurve`, but is held within a range around a target level (setpoint) by two connected pumps. These two pumps behave like a reversible pump. When pumping can be done in only one direction, and the other direction is only possible under gravity, use an Outlet for that direction.\n" + "The model constructed below consists of a single basin which slowly drains trough a `TabulatedRatingCurve`, but is held within a range by two connected pumps. These two pumps together behave like a reversible pump. When pumping can be done in only one direction, and the other direction is only possible under gravity, use an Outlet for that direction.\n" ] }, { @@ -471,6 +471,7 @@ " [\n", " basin.Profile(area=[1000.0, 1000.0], level=[0.0, 1.0]),\n", " basin.State(level=[20.0]),\n", + " basin.Time(time=[\"2020-01-01\", \"2020-07-01\"], precipitation=[0.0, 3e-6]),\n", " ],\n", ")" ] @@ -493,17 +494,19 @@ " Node(7, Point(1.0, 0.0)),\n", " [\n", " discrete_control.Variable(\n", - " listen_node_id=[1],\n", - " listen_node_type=\"Basin\",\n", - " variable=\"level\",\n", " compound_variable_id=1,\n", + " listen_node_id=1,\n", + " listen_node_type=[\"Basin\"],\n", + " variable=[\"level\"],\n", " ),\n", " discrete_control.Condition(\n", - " greater_than=[5.0, 10.0, 15.0], compound_variable_id=1\n", + " compound_variable_id=1,\n", + " # min, max\n", + " greater_than=[5.0, 15.0],\n", " ),\n", " discrete_control.Logic(\n", - " truth_state=[\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n", - " control_state=[\"in\", \"in\", \"none\", \"out\", \"out\"],\n", + " truth_state=[\"FF\", \"TF\", \"TT\"],\n", + " control_state=[\"in\", \"none\", \"out\"],\n", " ),\n", " ],\n", ")" @@ -515,8 +518,8 @@ "source": [ "The above control logic can be summarized as follows:\n", "\n", - "- If the level gets above the maximum, activate the control state \"out\" until the setpoint is reached;\n", - "- If the level gets below the minimum, active the control state \"in\" until the setpoint is reached;\n", + "- If the level is above the maximum, activate the control state \"out\";\n", + "- If the level is below the minimum, active the control state \"in\";\n", "- Otherwise activate the control state \"none\".\n" ] }, @@ -590,7 +593,7 @@ "source": [ "model.tabulated_rating_curve.add(\n", " Node(5, Point(-1.0, 0.0)),\n", - " [tabulated_rating_curve.Static(level=[2.0, 15.0], flow_rate=[0.0, 1e-3])],\n", + " [tabulated_rating_curve.Static(level=[2.0, 15.0], flow_rate=[0.0, 2e-3])],\n", ")" ] }, @@ -667,7 +670,7 @@ "outputs": [], "source": [ "datadir = Path(\"data\")\n", - "model.write(datadir / \"level_setpoint_with_minmax/ribasim.toml\")" + "model.write(datadir / \"level_range/ribasim.toml\")" ] }, { @@ -684,7 +687,7 @@ " \"julia\",\n", " \"--project=../../core\",\n", " \"--eval\",\n", - " f'using Ribasim; Ribasim.main(\"{datadir.as_posix()}/level_setpoint_with_minmax/ribasim.toml\")',\n", + " f'using Ribasim; Ribasim.main(\"{datadir.as_posix()}/level_range/ribasim.toml\")',\n", " ],\n", " check=True,\n", ")" @@ -694,7 +697,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now run the model with `ribasim level_setpoint_with_minmax/ribasim.toml`.\n", + "Now run the model with `ribasim level_range/ribasim.toml`.\n", "After running the model, read back the results:\n" ] }, @@ -704,9 +707,7 @@ "metadata": {}, "outputs": [], "source": [ - "from matplotlib.dates import date2num\n", - "\n", - "df_basin = pd.read_feather(datadir / \"level_setpoint_with_minmax/results/basin.arrow\")\n", + "df_basin = pd.read_feather(datadir / \"level_range/results/basin.arrow\")\n", "df_basin_wide = df_basin.pivot_table(\n", " index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n", ")\n", @@ -724,25 +725,7 @@ " color=\"k\",\n", ")\n", "\n", - "df_control = pd.read_feather(\n", - " datadir / \"level_setpoint_with_minmax/results/control.arrow\"\n", - ")\n", - "\n", - "y_min, y_max = ax.get_ybound()\n", - "ax.fill_between(\n", - " df_control.time[:2].to_numpy(), 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\"\n", - ")\n", - "ax.fill_between(\n", - " df_control.time[2:4].to_numpy(), 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\"\n", - ")\n", - "\n", - "ax.set_xticks(\n", - " date2num(df_control.time).tolist(),\n", - " df_control.control_state.tolist(),\n", - " rotation=50,\n", - ")\n", - "\n", - "ax.set_yticks(greater_than, [\"min\", \"setpoint\", \"max\"])\n", + "ax.set_yticks(greater_than, [\"min\", \"max\"])\n", "ax.set_ylabel(\"level\")\n", "plt.show()" ] @@ -751,7 +734,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The highlighted regions show where a pump is active.\n" + "We see that in Januari the level of the basin is too high and thus water is pumped out until the maximum level of the desired range is reached. Then until May water flows out of the basin freely through the tabulated rating curve until the minimum level is reached. From \n", + "this point until the start of July water is pumped into the basin in short bursts to stay within the desired range. At the start of July rain starts falling on the basin, which causes the basin level to rise until the maximum level. From this point onward water is pumped out of the basin in short bursts to stay within the desired range." ] }, { diff --git a/python/ribasim/tests/conftest.py b/python/ribasim/tests/conftest.py index 30939e521..d77d287e1 100644 --- a/python/ribasim/tests/conftest.py +++ b/python/ribasim/tests/conftest.py @@ -40,8 +40,8 @@ def discrete_control_of_pid_control() -> ribasim.Model: @pytest.fixture() -def level_setpoint_with_minmax() -> ribasim.Model: - return ribasim_testmodels.level_setpoint_with_minmax_model() +def level_range() -> ribasim.Model: + return ribasim_testmodels.level_range_model() @pytest.fixture() diff --git a/python/ribasim/tests/test_io.py b/python/ribasim/tests/test_io.py index e6507d77e..2fb6e50de 100644 --- a/python/ribasim/tests/test_io.py +++ b/python/ribasim/tests/test_io.py @@ -92,8 +92,8 @@ def test_extra_columns(): terminal.Static(meta_id=[-1, -2, -3], extra=[-1, -2, -3]) -def test_sort(level_setpoint_with_minmax, tmp_path): - model = level_setpoint_with_minmax +def test_sort(level_range, tmp_path): + model = level_range table = model.discrete_control.condition edge = model.edge diff --git a/python/ribasim_testmodels/ribasim_testmodels/__init__.py b/python/ribasim_testmodels/ribasim_testmodels/__init__.py index 4f9ce86c3..bae49d032 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/__init__.py +++ b/python/ribasim_testmodels/ribasim_testmodels/__init__.py @@ -31,7 +31,7 @@ compound_variable_condition_model, flow_condition_model, level_boundary_condition_model, - level_setpoint_with_minmax_model, + level_range_model, pump_discrete_control_model, tabulated_rating_curve_control_model, ) @@ -79,7 +79,7 @@ "leaky_bucket_model", "level_boundary_condition_model", "level_demand_model", - "level_setpoint_with_minmax_model", + "level_range_model", "linear_resistance_demand_model", "linear_resistance_model", "local_pidcontrolled_cascade_model", diff --git a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py index 650e73876..fcdd5b501 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py @@ -323,7 +323,65 @@ def tabulated_rating_curve_control_model() -> Model: return model -def level_setpoint_with_minmax_model() -> Model: +def compound_variable_condition_model() -> Model: + """ + Set up a minimal model containing a condition on a compound variable + for discrete control. + """ + + model = Model( + starttime="2020-01-01", + endtime="2021-01-01", + crs="EPSG:28992", + ) + + model.basin.add( + Node(1, Point(1, 0)), + [ + basin.Profile(area=1000.0, level=[0.0, 1.0]), + basin.State(level=[1.0]), + ], + ) + model.flow_boundary.add( + Node(2, Point(0, 0)), [flow_boundary.Static(flow_rate=[0.0])] + ) + model.flow_boundary.add( + Node(3, Point(0, 1)), + [flow_boundary.Time(time=["2020-01-01", "2021-01-01"], flow_rate=[0.0, 2.0])], + ) + model.pump.add( + Node(4, Point(2, 0)), + [pump.Static(control_state=["Off", "On"], flow_rate=[0.0, 1.0])], + ) + model.terminal.add(Node(5, Point(3, 0))) + model.discrete_control.add( + Node(6, Point(1, 1)), + [ + discrete_control.Variable( + listen_node_type="FlowBoundary", + listen_node_id=[2, 3], + variable="flow_rate", + weight=0.5, + compound_variable_id=1, + ), + discrete_control.Condition( + greater_than=[0.5], + compound_variable_id=1, + ), + discrete_control.Logic(truth_state=["T", "F"], control_state=["On", "Off"]), + ], + ) + + model.edge.add(model.flow_boundary[2], model.basin[1]) + model.edge.add(model.flow_boundary[3], model.basin[1]) + model.edge.add(model.basin[1], model.pump[4]) + model.edge.add(model.pump[4], model.terminal[5]) + model.edge.add(model.discrete_control[6], model.pump[4]) + + return model + + +def level_range_model() -> Model: """ Set up a minimal model in which the level of a basin is kept within an acceptable range around a setpoint while being affected by time-varying forcing. @@ -341,6 +399,7 @@ def level_setpoint_with_minmax_model() -> Model: [ basin.Profile(area=1000.0, level=[0.0, 1.0]), basin.State(level=[20.0]), + basin.Time(time=["2020-01-01", "2020-07-01"], precipitation=[0.0, 3e-6]), ], ) model.pump.add( @@ -356,7 +415,7 @@ def level_setpoint_with_minmax_model() -> Model: ) model.tabulated_rating_curve.add( Node(5, Point(-1, 0)), - [tabulated_rating_curve.Static(level=[2.0, 15.0], flow_rate=[0.0, 1e-3])], + [tabulated_rating_curve.Static(level=[2.0, 15.0], flow_rate=[0.0, 2e-3])], ) model.terminal.add(Node(6, Point(-2, 0))) model.discrete_control.add( @@ -369,13 +428,13 @@ def level_setpoint_with_minmax_model() -> Model: compound_variable_id=1, ), discrete_control.Condition( - # min, setpoint, max - greater_than=[5.0, 10.0, 15.0], + # min, max + greater_than=[5.0, 15.0], compound_variable_id=1, ), discrete_control.Logic( - truth_state=["FFF", "U**", "T*F", "**D", "TTT"], - control_state=["in", "in", "none", "out", "out"], + truth_state=["FF", "TF", "TT"], + control_state=["in", "none", "out"], ), ], ) @@ -414,61 +473,3 @@ def level_setpoint_with_minmax_model() -> Model: ) return model - - -def compound_variable_condition_model() -> Model: - """ - Set up a minimal model containing a condition on a compound variable - for discrete control. - """ - - model = Model( - starttime="2020-01-01", - endtime="2021-01-01", - crs="EPSG:28992", - ) - - model.basin.add( - Node(1, Point(1, 0)), - [ - basin.Profile(area=1000.0, level=[0.0, 1.0]), - basin.State(level=[1.0]), - ], - ) - model.flow_boundary.add( - Node(2, Point(0, 0)), [flow_boundary.Static(flow_rate=[0.0])] - ) - model.flow_boundary.add( - Node(3, Point(0, 1)), - [flow_boundary.Time(time=["2020-01-01", "2021-01-01"], flow_rate=[0.0, 2.0])], - ) - model.pump.add( - Node(4, Point(2, 0)), - [pump.Static(control_state=["Off", "On"], flow_rate=[0.0, 1.0])], - ) - model.terminal.add(Node(5, Point(3, 0))) - model.discrete_control.add( - Node(6, Point(1, 1)), - [ - discrete_control.Variable( - listen_node_type="FlowBoundary", - listen_node_id=[2, 3], - variable="flow_rate", - weight=0.5, - compound_variable_id=1, - ), - discrete_control.Condition( - greater_than=[0.5], - compound_variable_id=1, - ), - discrete_control.Logic(truth_state=["T", "F"], control_state=["On", "Off"]), - ], - ) - - model.edge.add(model.flow_boundary[2], model.basin[1]) - model.edge.add(model.flow_boundary[3], model.basin[1]) - model.edge.add(model.basin[1], model.pump[4]) - model.edge.add(model.pump[4], model.terminal[5]) - model.edge.add(model.discrete_control[6], model.pump[4]) - - return model