From c6627a172dbf1aec10ef207b71db1824782d2af5 Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Tue, 16 Apr 2024 15:07:07 +0200 Subject: [PATCH] VectorContinuousCallback -> FunctionCallingCallback and remove level_set_point_with_minmax model and associated tests --- core/src/callback.jl | 211 ++---------- core/src/model.jl | 2 - core/src/util.jl | 18 +- core/src/validation.jl | 1 - core/test/control_test.jl | 32 +- docs/python/examples.ipynb | 321 ------------------ python/ribasim/tests/conftest.py | 5 - python/ribasim/tests/test_io.py | 35 -- .../ribasim_testmodels/__init__.py | 2 - .../ribasim_testmodels/discrete_control.py | 93 ----- 10 files changed, 35 insertions(+), 685 deletions(-) diff --git a/core/src/callback.jl b/core/src/callback.jl index fb1918831..d5025340c 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 @@ -99,13 +67,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...) @@ -196,33 +158,49 @@ 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 + + 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 @@ -272,105 +250,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 @@ -386,56 +273,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 @@ -447,7 +302,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 241ba2ec6..0eca2bc73 100644 --- a/core/src/model.jl +++ b/core/src/model.jl @@ -163,8 +163,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 d84567632..636227673 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 @@ -709,19 +709,3 @@ function get_influx(basin::Basin, basin_idx::Int)::Float64 return precipitation[basin_idx] - evaporation[basin_idx] + drainage[basin_idx] - infiltration[basin_idx] end - -function get_discrete_control_indices(discrete_control::DiscreteControl, condition_idx::Int) - (; greater_than) = discrete_control - condition_idx_now = 1 - - for (compound_variable_idx, vec) in enumerate(greater_than) - l = length(vec) - - if condition_idx_now + l > condition_idx - greater_than_idx = condition_idx - condition_idx_now + 1 - return compound_variable_idx, greater_than_idx - end - - condition_idx_now += l - end -end diff --git a/core/src/validation.jl b/core/src/validation.jl index 9d6a621d2..6551690ad 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -552,7 +552,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 d225579d3..7f7703ce3 100644 --- a/docs/python/examples.ipynb +++ b/docs/python/examples.ipynb @@ -433,327 +433,6 @@ "ax.legend(bbox_to_anchor=(1.3, 1), title=\"Edge\")" ] }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "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" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the basins:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model = Model(starttime=\"2020-01-01\", endtime=\"2021-01-01\", crs=\"EPSG:4326\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.basin.add(\n", - " Node(1, Point(0.0, 0.0)),\n", - " [\n", - " basin.Profile(area=[1000.0, 1000.0], level=[0.0, 1.0]),\n", - " basin.State(level=[20.0]),\n", - " ],\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the discrete control:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.discrete_control.add(\n", - " 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", - " ),\n", - " discrete_control.Condition(\n", - " greater_than=[5.0, 10.0, 15.0], compound_variable_id=1\n", - " ),\n", - " discrete_control.Logic(\n", - " truth_state=[\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n", - " control_state=[\"in\", \"in\", \"none\", \"out\", \"out\"],\n", - " ),\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "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", - "- Otherwise activate the control state \"none\".\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the pump:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.pump.add(\n", - " Node(2, Point(1.0, 1.0)),\n", - " [pump.Static(control_state=[\"none\", \"in\", \"out\"], flow_rate=[0.0, 2e-3, 0.0])],\n", - ")\n", - "model.pump.add(\n", - " Node(3, Point(1.0, -1.0)),\n", - " [pump.Static(control_state=[\"none\", \"in\", \"out\"], flow_rate=[0.0, 0.0, 2e-3])],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The pump data defines the following:\n", - "\n", - "| Control state | Pump #2 flow rate (m/s) | Pump #3 flow rate (m/s) |\n", - "| ------------- | ----------------------- | ----------------------- |\n", - "| \"none\" | 0.0 | 0.0 |\n", - "| \"in\" | 2e-3 | 0.0 |\n", - "| \"out\" | 0.0 | 2e-3 |\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the level boundary:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.level_boundary.add(\n", - " Node(4, Point(2.0, 0.0)), [level_boundary.Static(level=[10.0])]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the rating curve:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "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", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the terminal:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.terminal.add(Node(6, Point(-2.0, 0.0)))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup edges:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.edge.add(model.basin[1], model.pump[3])\n", - "model.edge.add(model.pump[3], model.level_boundary[4])\n", - "model.edge.add(model.level_boundary[4], model.pump[2])\n", - "model.edge.add(model.pump[2], model.basin[1])\n", - "model.edge.add(model.basin[1], model.tabulated_rating_curve[5])\n", - "model.edge.add(model.tabulated_rating_curve[5], model.terminal[6])\n", - "model.edge.add(model.discrete_control[7], model.pump[2])\n", - "model.edge.add(model.discrete_control[7], model.pump[3])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let’s take a look at the model:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.plot()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Listen edges are plotted with a dashed line since they are not present in the \"Edge / static\" schema but only in the \"Control / condition\" schema.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "datadir = Path(\"data\")\n", - "model.write(datadir / \"level_setpoint_with_minmax/ribasim.toml\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# | include: false\n", - "from subprocess import run\n", - "\n", - "run(\n", - " [\n", - " \"julia\",\n", - " \"--project=../../core\",\n", - " \"--eval\",\n", - " f'using Ribasim; Ribasim.main(\"{datadir.as_posix()}/level_setpoint_with_minmax/ribasim.toml\")',\n", - " ],\n", - " check=True,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now run the model with `ribasim level_setpoint_with_minmax/ribasim.toml`.\n", - "After running the model, read back the results:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "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_wide = df_basin.pivot_table(\n", - " index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n", - ")\n", - "\n", - "ax = df_basin_wide[\"level\"].plot()\n", - "\n", - "greater_than = model.discrete_control.condition.df.greater_than\n", - "\n", - "ax.hlines(\n", - " greater_than,\n", - " df_basin.time[0],\n", - " df_basin.time.max(),\n", - " lw=1,\n", - " ls=\"--\",\n", - " 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_ylabel(\"level\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The highlighted regions show where a pump is active.\n" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/python/ribasim/tests/conftest.py b/python/ribasim/tests/conftest.py index 30939e521..4b4e1a7ba 100644 --- a/python/ribasim/tests/conftest.py +++ b/python/ribasim/tests/conftest.py @@ -39,11 +39,6 @@ def discrete_control_of_pid_control() -> ribasim.Model: return ribasim_testmodels.discrete_control_of_pid_control_model() -@pytest.fixture() -def level_setpoint_with_minmax() -> ribasim.Model: - return ribasim_testmodels.level_setpoint_with_minmax_model() - - @pytest.fixture() def trivial() -> ribasim.Model: return ribasim_testmodels.trivial_model() diff --git a/python/ribasim/tests/test_io.py b/python/ribasim/tests/test_io.py index e6507d77e..bd35fcc5c 100644 --- a/python/ribasim/tests/test_io.py +++ b/python/ribasim/tests/test_io.py @@ -92,41 +92,6 @@ 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 - table = model.discrete_control.condition - edge = model.edge - - # apply a wrong sort, then call the sort method to restore order - table.df.sort_values("greater_than", ascending=False, inplace=True) - assert table.df.iloc[0]["greater_than"] == 15.0 - assert table._sort_keys == [ - "node_id", - "compound_variable_id", - "greater_than", - ] - table.sort() - assert table.df.iloc[0]["greater_than"] == 5.0 - - # The edge table is not sorted - assert edge.df.iloc[1]["from_node_type"] == "Pump" - assert edge.df.iloc[1]["from_node_id"] == 3 - - # re-apply wrong sort, then check if it gets sorted on write - table.df.sort_values("greater_than", ascending=False, inplace=True) - model.write(tmp_path / "basic/ribasim.toml") - # write sorts the model in place - assert table.df.iloc[0]["greater_than"] == 5.0 - model_loaded = ribasim.Model.read(filepath=tmp_path / "basic/ribasim.toml") - table_loaded = model_loaded.discrete_control.condition - edge_loaded = model_loaded.edge - assert table_loaded.df.iloc[0]["greater_than"] == 5.0 - assert edge.df.iloc[1]["from_node_type"] == "Pump" - assert edge.df.iloc[1]["from_node_id"] == 3 - __assert_equal(table.df, table_loaded.df) - __assert_equal(edge.df, edge_loaded.df) - - def test_roundtrip(trivial, tmp_path): model1 = trivial # set custom Edge index diff --git a/python/ribasim_testmodels/ribasim_testmodels/__init__.py b/python/ribasim_testmodels/ribasim_testmodels/__init__.py index 4f9ce86c3..736cdeab3 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/__init__.py +++ b/python/ribasim_testmodels/ribasim_testmodels/__init__.py @@ -31,7 +31,6 @@ compound_variable_condition_model, flow_condition_model, level_boundary_condition_model, - level_setpoint_with_minmax_model, pump_discrete_control_model, tabulated_rating_curve_control_model, ) @@ -79,7 +78,6 @@ "leaky_bucket_model", "level_boundary_condition_model", "level_demand_model", - "level_setpoint_with_minmax_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..7111c0d4b 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py @@ -323,99 +323,6 @@ def tabulated_rating_curve_control_model() -> Model: return model -def level_setpoint_with_minmax_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. - This is done by bringing the level back to the setpoint once the level goes beyond this range. - """ - - model = Model( - starttime="2020-01-01", - endtime="2021-01-01", - crs="EPSG:28992", - ) - - model.basin.add( - Node(1, Point(0, 0)), - [ - basin.Profile(area=1000.0, level=[0.0, 1.0]), - basin.State(level=[20.0]), - ], - ) - model.pump.add( - Node(2, Point(1, 1)), - [pump.Static(control_state=["none", "in", "out"], flow_rate=[0.0, 2e-3, 0.0])], - ) - model.pump.add( - Node(3, Point(1, -1)), - [pump.Static(control_state=["none", "in", "out"], flow_rate=[0.0, 0.0, 2e-3])], - ) - model.level_boundary.add( - Node(4, Point(2, 0)), [level_boundary.Static(level=[10.0])] - ) - 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])], - ) - model.terminal.add(Node(6, Point(-2, 0))) - model.discrete_control.add( - Node(7, Point(1, 0)), - [ - discrete_control.Variable( - listen_node_type="Basin", - listen_node_id=[1], - variable="level", - compound_variable_id=1, - ), - discrete_control.Condition( - # min, setpoint, max - greater_than=[5.0, 10.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"], - ), - ], - ) - - model.edge.add( - model.basin[1], - model.pump[3], - ) - model.edge.add( - model.pump[3], - model.level_boundary[4], - ) - model.edge.add( - model.level_boundary[4], - model.pump[2], - ) - model.edge.add( - model.pump[2], - model.basin[1], - ) - model.edge.add( - model.basin[1], - model.tabulated_rating_curve[5], - ) - model.edge.add( - model.tabulated_rating_curve[5], - model.terminal[6], - ) - model.edge.add( - model.discrete_control[7], - model.pump[2], - ) - model.edge.add( - model.discrete_control[7], - model.pump[3], - ) - - return model - - def compound_variable_condition_model() -> Model: """ Set up a minimal model containing a condition on a compound variable