diff --git a/core/src/read.jl b/core/src/read.jl index b9bf0c8ec..a0aec5fda 100644 --- a/core/src/read.jl +++ b/core/src/read.jl @@ -546,17 +546,26 @@ function DiscreteControl(db::DB, config::Config)::DiscreteControl compound_variable = load_structvector(db, config, DiscreteControlVariableV1) condition = load_structvector(db, config, DiscreteControlConditionV1) + node_id = NodeID[] listen_node_id = Vector{NodeID}[] variable = Vector{String}[] weight = Vector{Float64}[] look_ahead = Vector{Float64}[] for id in unique(condition.node_id) - group = filter(row -> row.node_id == id, compound_variable) - push!(listen_node_id, NodeID.(group.listen_node_type, group.listen_node_id)) - push!(variable, group.variable) - push!(weight, coalesce.(group.weight, 1.0)) - push!(look_ahead, coalesce.(group.look_ahead, 0.0)) + group_id = filter(row -> row.node_id == id, compound_variable) + for group_variable in + StructVector.(IterTools.groupby(row -> row.compound_variable_id, group_id)) + first_row = first(group_variable) + push!(node_id, NodeID(NodeType.DiscreteControl, first_row.node_id)) + push!( + listen_node_id, + NodeID.(group_variable.listen_node_type, group_variable.listen_node_id), + ) + push!(variable, group_variable.variable) + push!(weight, coalesce.(group_variable.weight, 1.0)) + push!(look_ahead, coalesce.(group_variable.look_ahead, 0.0)) + end end condition_value = fill(false, length(condition.node_id)) @@ -589,7 +598,7 @@ function DiscreteControl(db::DB, config::Config)::DiscreteControl ) return DiscreteControl( - NodeID.(NodeType.DiscreteControl, condition.node_id), # Not unique + node_id, # Not unique listen_node_id, variable, weight, diff --git a/core/src/schema.jl b/core/src/schema.jl index 60147c38a..60eb87ac8 100644 --- a/core/src/schema.jl +++ b/core/src/schema.jl @@ -186,6 +186,7 @@ end @version DiscreteControlVariableV1 begin node_id::Int32 + compound_variable_id::Int listen_node_type::String listen_node_id::Int variable::String @@ -195,6 +196,7 @@ end @version DiscreteControlConditionV1 begin node_id::Int32 + compound_variable_id::Int greater_than::Float64 end diff --git a/core/src/validation.jl b/core/src/validation.jl index 846cca5b4..580b4d5ba 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -118,7 +118,6 @@ sort_by_function(table::StructVector{UserDemandTimeV1}) = sort_by_priority_time sort_by_function(table::StructVector{BasinSubgridV1}) = sort_by_subgrid_level sort_by_function(table::StructVector{DiscreteControlVariableV1}) = sort_by_variable sort_by_function(table::StructVector{DiscreteControlConditionV1}) = sort_by_id_greater_than -sort_by_function(table::StructVector{DiscreteControlLogicV1}) = sort_by_truth_state const TimeSchemas = Union{ BasinTimeV1, diff --git a/docs/python/examples.ipynb b/docs/python/examples.ipynb index 5a76e3ae7..f78357420 100644 --- a/docs/python/examples.ipynb +++ b/docs/python/examples.ipynb @@ -492,10 +492,10 @@ "model.discrete_control.add(\n", " Node(7, Point(1.0, 0.0)),\n", " [\n", + " discrete_control.Variable(\n", + " listen_node_id=[1], listen_node_type=\"Basin\", variable=\"level\"\n", + " ),\n", " discrete_control.Condition(\n", - " listen_node_id=[1, 1, 1],\n", - " listen_node_type=[\"Basin\", \"Basin\", \"Basin\"],\n", - " variable=[\"level\", \"level\", \"level\"],\n", " greater_than=[5.0, 10.0, 15.0],\n", " ),\n", " discrete_control.Logic(\n", @@ -691,7 +691,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now run the model with `level_setpoint_with_minmax/ribasim.toml`.\n", + "Now run the model with `ribasim level_setpoint_with_minmax/ribasim.toml`.\n", "After running the model, read back the results:\n" ] }, diff --git a/python/ribasim/ribasim/config.py b/python/ribasim/ribasim/config.py index dd7fbbf0d..d837fc8fc 100644 --- a/python/ribasim/ribasim/config.py +++ b/python/ribasim/ribasim/config.py @@ -310,7 +310,7 @@ class DiscreteControl(MultiNodeModel): ) logic: TableModel[DiscreteControlLogicSchema] = Field( default_factory=TableModel[DiscreteControlLogicSchema], - json_schema_extra={"sort_keys": ["node_id", "truth_state"]}, + json_schema_extra={"sort_keys": ["node_id"]}, ) diff --git a/python/ribasim/ribasim/model.py b/python/ribasim/ribasim/model.py index 8bbb1666a..966a65888 100644 --- a/python/ribasim/ribasim/model.py +++ b/python/ribasim/ribasim/model.py @@ -321,14 +321,9 @@ def plot_control_listen(self, ax): df_listen_edge = pd.concat([df_listen_edge, to_add]) # Listen edges from DiscreteControl - for table in ( - self.discrete_control.condition.df, - self.discrete_control.variable.df, - ): - if table is None: - continue - - to_add = table[ + df_variable = self.discrete_control.variable.df + if df_variable is not None: + to_add = df_variable[ ["node_id", "listen_node_id", "listen_node_type"] ].drop_duplicates() to_add = to_add[to_add["listen_node_type"] != "compound"] diff --git a/python/ribasim/ribasim/schemas.py b/python/ribasim/ribasim/schemas.py index 260831282..2a8b5eab5 100644 --- a/python/ribasim/ribasim/schemas.py +++ b/python/ribasim/ribasim/schemas.py @@ -50,6 +50,7 @@ class BasinTimeSchema(_BaseSchema): class DiscreteControlConditionSchema(_BaseSchema): node_id: Series[Int32] = pa.Field(nullable=False, default=0) + compound_variable_id: Series[Int32] = pa.Field(nullable=False, default=0) greater_than: Series[float] = pa.Field(nullable=False) @@ -61,6 +62,7 @@ class DiscreteControlLogicSchema(_BaseSchema): class DiscreteControlVariableSchema(_BaseSchema): node_id: Series[Int32] = pa.Field(nullable=False, default=0) + compound_variable_id: Series[Int32] = pa.Field(nullable=False, default=0) listen_node_type: Series[str] = pa.Field(nullable=False) listen_node_id: Series[Int32] = pa.Field(nullable=False, default=0) variable: Series[str] = pa.Field(nullable=False) diff --git a/python/ribasim_testmodels/ribasim_testmodels/allocation.py b/python/ribasim_testmodels/ribasim_testmodels/allocation.py index 71353cf93..51980abc5 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/allocation.py +++ b/python/ribasim_testmodels/ribasim_testmodels/allocation.py @@ -410,9 +410,11 @@ def fractional_flow_subnetwork_model() -> Model: listen_node_type="FlowBoundary", listen_node_id=[1], variable="flow_rate", + compound_variable_id=1, ), discrete_control.Condition( greater_than=[3e-3], + compound_variable_id=1, ), discrete_control.Logic(truth_state=["F", "T"], control_state=["A", "B"]), ], @@ -506,9 +508,11 @@ def allocation_example_model() -> Model: listen_node_type="Basin", listen_node_id=[5], variable="level", + compound_variable_id=1, ), discrete_control.Condition( greater_than=[0.52], + compound_variable_id=1, ), discrete_control.Logic( truth_state=["T", "F"], control_state=["divert", "close"] @@ -687,9 +691,11 @@ def main_network_with_subnetworks_model() -> Model: listen_node_type="Basin", listen_node_id=[25], variable="level", + compound_variable_id=1, ), discrete_control.Condition( greater_than=[0.003], + compound_variable_id=1, ), discrete_control.Logic(truth_state=["F", "T"], control_state=["A", "B"]), ], diff --git a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py index 1a6037d8a..d8d911674 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py @@ -58,9 +58,11 @@ def pump_discrete_control_model() -> Model: listen_node_type="Basin", listen_node_id=[1, 3], variable="level", + compound_variable_id=[1, 2], ), discrete_control.Condition( greater_than=[0.8, 0.4], + compound_variable_id=[1, 2], ), discrete_control.Logic( truth_state=["FF", "TF", "FT", "TT"], @@ -75,9 +77,11 @@ def pump_discrete_control_model() -> Model: listen_node_type="Basin", listen_node_id=[3], variable="level", + compound_variable_id=1, ), discrete_control.Condition( greater_than=[0.45], + compound_variable_id=1, ), discrete_control.Logic( truth_state=["T", "F"], @@ -149,9 +153,11 @@ def flow_condition_model() -> Model: listen_node_id=[1], variable="flow_rate", look_ahead=60 * 86400, + compound_variable_id=1, ), discrete_control.Condition( greater_than=[20 / (86400)], + compound_variable_id=1, ), discrete_control.Logic(truth_state=["T", "F"], control_state=["off", "on"]), ], @@ -214,9 +220,11 @@ def level_boundary_condition_model() -> Model: listen_node_id=[1], variable="level", look_ahead=60 * 86400, + compound_variable_id=1, ), discrete_control.Condition( greater_than=[6.0], + compound_variable_id=1, ), discrete_control.Logic(truth_state=["T", "F"], control_state=["on", "off"]), ], @@ -287,6 +295,7 @@ def tabulated_rating_curve_control_model() -> Model: listen_node_type="Basin", listen_node_id=[1], variable="level", + compound_variable_id=1, ), discrete_control.Condition( greater_than=[0.5], @@ -356,10 +365,12 @@ def level_setpoint_with_minmax_model() -> Model: 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"], @@ -443,9 +454,11 @@ def compound_variable_condition_model() -> Model: 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"]), ], diff --git a/python/ribasim_testmodels/ribasim_testmodels/dutch_waterways.py b/python/ribasim_testmodels/ribasim_testmodels/dutch_waterways.py index b13bb4bcb..9fb315a7f 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/dutch_waterways.py +++ b/python/ribasim_testmodels/ribasim_testmodels/dutch_waterways.py @@ -125,9 +125,11 @@ def dutch_waterways_model() -> Model: listen_node_type="FlowBoundary", listen_node_id=[1], variable="flow_rate", + compound_variable_id=1, ), discrete_control.Condition( greater_than=[250, 275, 750, 800], + compound_variable_id=1, ), discrete_control.Logic( truth_state=["FFFF", "U***", "T**F", "***D", "TTTT"], diff --git a/python/ribasim_testmodels/ribasim_testmodels/invalid.py b/python/ribasim_testmodels/ribasim_testmodels/invalid.py index 29c9a8405..463603bf6 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/invalid.py +++ b/python/ribasim_testmodels/ribasim_testmodels/invalid.py @@ -156,9 +156,11 @@ def invalid_discrete_control_model() -> Model: # Invalid: this look_ahead will go past the provided timeseries during simulation. # Invalid: look_ahead must be non-negative. look_ahead=[100.0, 40 * 24 * 60 * 60, -10.0], + compound_variable_id=[1, 2, 3], ), discrete_control.Condition( greater_than=[0.5, 1.5, 1.5], + compound_variable_id=[1, 2, 3], ), # Invalid: DiscreteControl node #4 has 2 conditions so # truth states have to be of length 2 diff --git a/python/ribasim_testmodels/ribasim_testmodels/pid_control.py b/python/ribasim_testmodels/ribasim_testmodels/pid_control.py index 05ebc31b7..4a543acc3 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/pid_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/pid_control.py @@ -133,9 +133,11 @@ def discrete_control_of_pid_control_model() -> Model: listen_node_type="LevelBoundary", listen_node_id=[1], variable="level", + compound_variable_id=1, ), discrete_control.Condition( greater_than=[5.0], + compound_variable_id=1, ), discrete_control.Logic( truth_state=["T", "F"], control_state=["target_high", "target_low"]