From 25153610fd0d3797106b642812f85ac87d7b4ecd Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Wed, 12 Jul 2023 15:49:29 +0200 Subject: [PATCH 1/6] Add node activity input and handling in core --- core/src/create.jl | 26 ++++-- core/src/solve.jl | 89 +++++++++++++++---- core/src/validation.jl | 9 ++ docs/schema/FlowBoundaryStatic.schema.json | 7 ++ docs/schema/FractionalFlowStatic.schema.json | 7 ++ docs/schema/LevelBoundaryStatic.schema.json | 7 ++ .../schema/LinearResistanceStatic.schema.json | 7 ++ .../ManningResistanceStatic.schema.json | 7 ++ docs/schema/PIDControlStatic.schema.json | 7 ++ docs/schema/PumpStatic.schema.json | 7 ++ .../TabulatedRatingCurveStatic.schema.json | 7 ++ docs/schema/TerminalStatic.schema.json | 7 ++ python/ribasim/ribasim/models.py | 11 ++- 13 files changed, 172 insertions(+), 26 deletions(-) diff --git a/core/src/create.jl b/core/src/create.jl index f1f0038f3..89d40d5f1 100644 --- a/core/src/create.jl +++ b/core/src/create.jl @@ -19,11 +19,14 @@ end function LinearResistance(db::DB, config::Config)::LinearResistance static = load_structvector(db, config, LinearResistanceStaticV1) - return LinearResistance(static.node_id, static.resistance) + active = coalesce.(static.active, true) + + return LinearResistance(static.node_id, active, static.resistance) end function TabulatedRatingCurve(db::DB, config::Config)::TabulatedRatingCurve static = load_structvector(db, config, TabulatedRatingCurveStaticV1) + active = coalesce.(static.active, true) time = load_structvector(db, config, TabulatedRatingCurveTimeV1) static_node_ids = Set(static.node_id) @@ -48,13 +51,15 @@ function TabulatedRatingCurve(db::DB, config::Config)::TabulatedRatingCurve end push!(interpolations, interpolation) end - return TabulatedRatingCurve(node_ids, interpolations, time) + return TabulatedRatingCurve(node_ids, active, interpolations, time) end function ManningResistance(db::DB, config::Config)::ManningResistance static = load_structvector(db, config, ManningResistanceStaticV1) + active = coalesce.(static.active, true) return ManningResistance( static.node_id, + active, static.length, static.manning_n, static.profile_width, @@ -64,21 +69,25 @@ end function FractionalFlow(db::DB, config::Config)::FractionalFlow static = load_structvector(db, config, FractionalFlowStaticV1) - return FractionalFlow(static.node_id, static.fraction) + active = coalesce.(static.active, true) + return FractionalFlow(static.node_id, active, static.fraction) end function LevelBoundary(db::DB, config::Config)::LevelBoundary static = load_structvector(db, config, LevelBoundaryStaticV1) - return LevelBoundary(static.node_id, static.level) + active = coalesce.(static.active, true) + return LevelBoundary(static.node_id, active, static.level) end function FlowBoundary(db::DB, config::Config)::FlowBoundary static = load_structvector(db, config, FlowBoundaryStaticV1) - return FlowBoundary(static.node_id, static.flow_rate) + active = coalesce.(static.active, true) + return FlowBoundary(static.node_id, active, static.flow_rate) end function Pump(db::DB, config::Config)::Pump static = load_structvector(db, config, PumpStaticV1) + active = coalesce.(static.active, true) control_mapping = Dict{Tuple{Int, String}, NamedTuple}() @@ -104,12 +113,13 @@ function Pump(db::DB, config::Config)::Pump min_flow_rate = coalesce.(static.min_flow_rate, 0.0) max_flow_rate = coalesce.(static.max_flow_rate, NaN) - return Pump(node_ids, flow_rates, min_flow_rate, max_flow_rate, control_mapping) + return Pump(node_ids, active, flow_rates, min_flow_rate, max_flow_rate, control_mapping) end function Terminal(db::DB, config::Config)::Terminal static = load_structvector(db, config, TerminalStaticV1) - return Terminal(static.node_id) + active = coalesce.(static.active, true) + return Terminal(static.node_id, active) end function Basin(db::DB, config::Config)::Basin @@ -199,6 +209,7 @@ end function PidControl(db::DB, config::Config)::PidControl static = load_structvector(db, config, PidControlStaticV1) + active = coalesce.(static.active, true) proportional = coalesce.(static.proportional, NaN) integral = coalesce.(static.integral, NaN) @@ -207,6 +218,7 @@ function PidControl(db::DB, config::Config)::PidControl return PidControl( static.node_id, + active, static.listen_node_id, proportional, integral, diff --git a/core/src/solve.jl b/core/src/solve.jl index 85466f2eb..0e281f96e 100644 --- a/core/src/solve.jl +++ b/core/src/solve.jl @@ -123,6 +123,7 @@ of Vectors or Arrow Primitives, and is added to avoid type instabilities. """ struct TabulatedRatingCurve{C} <: AbstractParameterNode node_id::Vector{Int} + active::BitVector tables::Vector{Interpolation} time::StructVector{TabulatedRatingCurveTimeV1, C, Int} end @@ -135,6 +136,7 @@ Requirements: """ struct LinearResistance <: AbstractParameterNode node_id::Vector{Int} + active::BitVector resistance::Vector{Float64} end @@ -173,6 +175,7 @@ Requirements: """ struct ManningResistance <: AbstractParameterNode node_id::Vector{Int} + active::BitVector length::Vector{Float64} manning_n::Vector{Float64} profile_width::Vector{Float64} @@ -188,6 +191,7 @@ Requirements: """ struct FractionalFlow <: AbstractParameterNode node_id::Vector{Int} + active::BitVector fraction::Vector{Float64} end @@ -198,6 +202,7 @@ The node_id are Indices to support fast lookup of level using ID. """ struct LevelBoundary <: AbstractParameterNode node_id::Vector{Int} + active::BitVector level::Vector{Float64} end @@ -207,6 +212,7 @@ flow_rate: target flow rate """ struct FlowBoundary <: AbstractParameterNode node_id::Vector{Int} + active::BitVector flow_rate::Vector{Float64} end @@ -217,6 +223,7 @@ control_mapping: dictionary from (node_id, control_state) to target flow rate """ struct Pump <: AbstractParameterNode node_id::Vector{Int} + active::BitVector flow_rate::Vector{Float64} min_flow_rate::Vector{Float64} max_flow_rate::Vector{Float64} @@ -228,6 +235,7 @@ node_id: node ID of the Terminal node """ struct Terminal <: AbstractParameterNode node_id::Vector{Int} + active::BitVector end """ @@ -256,6 +264,7 @@ end struct PidControl <: AbstractParameterNode node_id::Vector{Int} + active::BitVector listen_node_id::Vector{Int} proportional::Vector{Float64} integral::Vector{Float64} @@ -336,13 +345,24 @@ function continuous_control!( (; min_flow_rate, max_flow_rate) = pump (; dstorage) = basin (; graph_control) = connectivity - (; node_id, proportional, integral, derivative, listen_node_id, error) = pid_control + (; node_id, active, proportional, integral, derivative, listen_node_id, error) = + pid_control get_error!(pid_control, p) for (i, id) in enumerate(node_id) + # Should this integration continue when the PID node is inactive? du.integral[i] = error[i] + controlled_node_id = only(outneighbors(graph_control, id)) + # TODO: support the use of id_index + controlled_node_idx = findfirst(pump.node_id .== controlled_node_id) + + if !active[i] + pump.flow_rate[controlled_node_idx] = 0.0 + return + end + listened_node_id = listen_node_id[i] _, listened_node_idx = id_index(basin.node_id, listened_node_id) @@ -372,9 +392,6 @@ function continuous_control!( flow_rate = min(flow_rate, max_flow_rate[i]) end - controlled_node_id = only(outneighbors(graph_control, id)) - # TODO: support the use of id_index - controlled_node_idx = findfirst(pump.node_id .== controlled_node_id) pump.flow_rate[controlled_node_idx] = flow_rate end return nothing @@ -386,13 +403,19 @@ Directed graph: outflow is positive! function formulate!(linear_resistance::LinearResistance, p::Parameters)::Nothing (; connectivity) = p (; graph_flow, flow) = connectivity - (; node_id, resistance) = linear_resistance + (; node_id, active, resistance) = linear_resistance for (i, id) in enumerate(node_id) basin_a_id = only(inneighbors(graph_flow, id)) basin_b_id = only(outneighbors(graph_flow, id)) - q = (get_level(p, basin_a_id) - get_level(p, basin_b_id)) / resistance[i] - flow[basin_a_id, id] = q - flow[id, basin_b_id] = q + + if active[i] + q = (get_level(p, basin_a_id) - get_level(p, basin_b_id)) / resistance[i] + flow[basin_a_id, id] = q + flow[id, basin_b_id] = q + else + flow[basin_a_id, id] = 0.0 + flow[id, basin_b_id] = 0.0 + end end return nothing end @@ -403,11 +426,17 @@ Directed graph: outflow is positive! function formulate!(tabulated_rating_curve::TabulatedRatingCurve, p::Parameters)::Nothing (; connectivity) = p (; graph_flow, flow) = connectivity - (; node_id, tables) = tabulated_rating_curve + (; node_id, active, tables) = tabulated_rating_curve for (i, id) in enumerate(node_id) upstream_basin_id = only(inneighbors(graph_flow, id)) downstream_ids = outneighbors(graph_flow, id) - q = tables[i](get_level(p, upstream_basin_id)) + + if active[i] + q = tables[i](get_level(p, upstream_basin_id)) + else + q = 0.0 + end + flow[upstream_basin_id, id] = q for downstream_id in downstream_ids flow[id, downstream_id] = q @@ -458,11 +487,18 @@ dry. function formulate!(manning_resistance::ManningResistance, p::Parameters)::Nothing (; basin, connectivity) = p (; graph_flow, flow) = connectivity - (; node_id, length, manning_n, profile_width, profile_slope) = manning_resistance + (; node_id, active, length, manning_n, profile_width, profile_slope) = + manning_resistance for (i, id) in enumerate(node_id) basin_a_id = only(inneighbors(graph_flow, id)) basin_b_id = only(outneighbors(graph_flow, id)) + if !active[i] + flow[basin_a_id, id] = 0.0 + flow[id, basin_b_id] = 0.0 + continue + end + h_a = get_level(p, basin_a_id) h_b = get_level(p, basin_b_id) bottom_a, bottom_b = basin_bottoms(basin, basin_a_id, basin_b_id, id) @@ -501,11 +537,16 @@ end function formulate!(fractional_flow::FractionalFlow, p::Parameters)::Nothing (; connectivity) = p (; graph_flow, flow) = connectivity - (; node_id, fraction) = fractional_flow + (; node_id, active, fraction) = fractional_flow for (i, id) in enumerate(node_id) - upstream_id = only(inneighbors(graph_flow, id)) downstream_id = only(outneighbors(graph_flow, id)) - flow[id, downstream_id] = flow[upstream_id, id] * fraction[i] + + if active[i] + upstream_id = only(inneighbors(graph_flow, id)) + flow[id, downstream_id] = flow[upstream_id, id] * fraction[i] + else + flow[id, downstream_id] = 0.0 + end end return nothing end @@ -517,11 +558,16 @@ function formulate!( )::Nothing (; connectivity, basin) = p (; graph_flow, flow) = connectivity - (; node_id, flow_rate) = flow_boundary + (; node_id, active, flow_rate) = flow_boundary - for (id, rate) in zip(node_id, flow_rate) + for (id, isactive, rate) in zip(node_id, active, flow_rate) # Requirement: edge points away from the flow boundary for dst_id in outneighbors(graph_flow, id) + if !isactive + flow[id, dst_id] = 0.0 + continue + end + # Adding water is always possible if rate >= 0 flow[id, dst_id] = rate @@ -541,10 +587,17 @@ end function formulate!(pump::Pump, p::Parameters, storage::SubArray{Float64})::Nothing (; connectivity, basin, level_boundary) = p (; graph_flow, flow) = connectivity - (; node_id, flow_rate) = pump - for (id, rate) in zip(node_id, flow_rate) + (; node_id, active, flow_rate) = pump + for (id, isactive, rate) in zip(node_id, active, flow_rate) src_id = only(inneighbors(graph_flow, id)) dst_id = only(outneighbors(graph_flow, id)) + + if !isactive + flow[src_id, id] = 0.0 + flow[id, dst_id] = 0.0 + continue + end + # negative flow_rate means pumping against edge direction intake_id = rate >= 0 ? src_id : dst_id diff --git a/core/src/validation.jl b/core/src/validation.jl index f7d65a4ac..640456d0c 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -77,6 +77,7 @@ end @version PumpStaticV1 begin node_id::Int + active::Union{Missing, Bool} flow_rate::Float64 min_flow_rate::Union{Missing, Float64} max_flow_rate::Union{Missing, Float64} @@ -116,30 +117,35 @@ end @version FractionalFlowStaticV1 begin node_id::Int + active::Union{Missing, Bool} fraction::Float64 control_state::Union{Missing, String} end @version LevelBoundaryStaticV1 begin node_id::Int + active::Union{Missing, Bool} level::Float64 control_state::Union{Missing, String} end @version FlowBoundaryStaticV1 begin node_id::Int + active::Union{Missing, Bool} flow_rate::Float64 control_state::Union{Missing, String} end @version LinearResistanceStaticV1 begin node_id::Int + active::Union{Missing, Bool} resistance::Float64 control_state::Union{Missing, String} end @version ManningResistanceStaticV1 begin node_id::Int + active::Union{Missing, Bool} length::Float64 manning_n::Float64 profile_width::Float64 @@ -149,6 +155,7 @@ end @version TabulatedRatingCurveStaticV1 begin node_id::Int + active::Union{Missing, Bool} level::Float64 discharge::Float64 end @@ -162,6 +169,7 @@ end @version TerminalStaticV1 begin node_id::Int + active::Union{Missing, Bool} end @version DiscreteControlConditionV1 begin @@ -179,6 +187,7 @@ end @version PidControlStaticV1 begin node_id::Int + active::Union{Missing, Bool} listen_node_id::Int proportional::Float64 integral::Union{Missing, Float64} diff --git a/docs/schema/FlowBoundaryStatic.schema.json b/docs/schema/FlowBoundaryStatic.schema.json index cafbf84ae..e47cc5450 100644 --- a/docs/schema/FlowBoundaryStatic.schema.json +++ b/docs/schema/FlowBoundaryStatic.schema.json @@ -7,6 +7,13 @@ "description": "a hack for pandera", "type": "string" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "flow_rate": { "format": "double", "description": "flow_rate", diff --git a/docs/schema/FractionalFlowStatic.schema.json b/docs/schema/FractionalFlowStatic.schema.json index f98567832..d1e80f695 100644 --- a/docs/schema/FractionalFlowStatic.schema.json +++ b/docs/schema/FractionalFlowStatic.schema.json @@ -7,6 +7,13 @@ "description": "a hack for pandera", "type": "string" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "node_id": { "format": "default", "description": "node_id", diff --git a/docs/schema/LevelBoundaryStatic.schema.json b/docs/schema/LevelBoundaryStatic.schema.json index cb5d9842b..4e59e3df2 100644 --- a/docs/schema/LevelBoundaryStatic.schema.json +++ b/docs/schema/LevelBoundaryStatic.schema.json @@ -7,6 +7,13 @@ "description": "a hack for pandera", "type": "string" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "node_id": { "format": "default", "description": "node_id", diff --git a/docs/schema/LinearResistanceStatic.schema.json b/docs/schema/LinearResistanceStatic.schema.json index 796f31191..a5d6efdb6 100644 --- a/docs/schema/LinearResistanceStatic.schema.json +++ b/docs/schema/LinearResistanceStatic.schema.json @@ -7,6 +7,13 @@ "description": "a hack for pandera", "type": "string" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "node_id": { "format": "default", "description": "node_id", diff --git a/docs/schema/ManningResistanceStatic.schema.json b/docs/schema/ManningResistanceStatic.schema.json index b40ae316a..5094f68df 100644 --- a/docs/schema/ManningResistanceStatic.schema.json +++ b/docs/schema/ManningResistanceStatic.schema.json @@ -17,6 +17,13 @@ "description": "a hack for pandera", "type": "string" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "profile_width": { "format": "double", "description": "profile_width", diff --git a/docs/schema/PIDControlStatic.schema.json b/docs/schema/PIDControlStatic.schema.json index f3a307072..034ee2d7d 100644 --- a/docs/schema/PIDControlStatic.schema.json +++ b/docs/schema/PIDControlStatic.schema.json @@ -19,6 +19,13 @@ "description": "listen_node_id", "type": "integer" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "proportional": { "format": "double", "description": "proportional", diff --git a/docs/schema/PumpStatic.schema.json b/docs/schema/PumpStatic.schema.json index 23203fc8a..fe4a95864 100644 --- a/docs/schema/PumpStatic.schema.json +++ b/docs/schema/PumpStatic.schema.json @@ -14,6 +14,13 @@ "description": "a hack for pandera", "type": "string" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "flow_rate": { "format": "double", "description": "flow_rate", diff --git a/docs/schema/TabulatedRatingCurveStatic.schema.json b/docs/schema/TabulatedRatingCurveStatic.schema.json index be3f8d1d1..4c94a4597 100644 --- a/docs/schema/TabulatedRatingCurveStatic.schema.json +++ b/docs/schema/TabulatedRatingCurveStatic.schema.json @@ -7,6 +7,13 @@ "description": "a hack for pandera", "type": "string" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "node_id": { "format": "default", "description": "node_id", diff --git a/docs/schema/TerminalStatic.schema.json b/docs/schema/TerminalStatic.schema.json index 4d4091a68..9791de678 100644 --- a/docs/schema/TerminalStatic.schema.json +++ b/docs/schema/TerminalStatic.schema.json @@ -7,6 +7,13 @@ "description": "a hack for pandera", "type": "string" }, + "active": { + "format": "default", + "description": "active", + "type": [ + "boolean" + ] + }, "node_id": { "format": "default", "description": "node_id", diff --git a/python/ribasim/ribasim/models.py b/python/ribasim/ribasim/models.py index 1c7336973..eebc204fb 100644 --- a/python/ribasim/ribasim/models.py +++ b/python/ribasim/ribasim/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: root.schema.json -# timestamp: 2023-07-07T14:02:25+00:00 +# timestamp: 2023-07-12T12:38:51+00:00 from __future__ import annotations @@ -28,6 +28,7 @@ class Edge(BaseModel): class PumpStatic(BaseModel): max_flow_rate: Optional[float] = Field(None, description="max_flow_rate") remarks: Optional[str] = Field("", description="a hack for pandera") + active: Optional[bool] = Field(None, description="active") flow_rate: float = Field(..., description="flow_rate") node_id: int = Field(..., description="node_id") control_state: Optional[str] = Field(None, description="control_state") @@ -36,6 +37,7 @@ class PumpStatic(BaseModel): class LevelBoundaryStatic(BaseModel): remarks: Optional[str] = Field("", description="a hack for pandera") + active: Optional[bool] = Field(None, description="active") node_id: int = Field(..., description="node_id") level: float = Field(..., description="level") control_state: Optional[str] = Field(None, description="control_state") @@ -62,6 +64,7 @@ class BasinForcing(BaseModel): class FractionalFlowStatic(BaseModel): remarks: Optional[str] = Field("", description="a hack for pandera") + active: Optional[bool] = Field(None, description="active") node_id: int = Field(..., description="node_id") fraction: float = Field(..., description="fraction") control_state: Optional[str] = Field(None, description="control_state") @@ -69,6 +72,7 @@ class FractionalFlowStatic(BaseModel): class LinearResistanceStatic(BaseModel): remarks: Optional[str] = Field("", description="a hack for pandera") + active: Optional[bool] = Field(None, description="active") node_id: int = Field(..., description="node_id") resistance: float = Field(..., description="resistance") control_state: Optional[str] = Field(None, description="control_state") @@ -78,6 +82,7 @@ class PidControlStatic(BaseModel): integral: Optional[float] = Field(None, description="integral") remarks: Optional[str] = Field("", description="a hack for pandera") listen_node_id: int = Field(..., description="listen_node_id") + active: Optional[bool] = Field(None, description="active") proportional: float = Field(..., description="proportional") node_id: int = Field(..., description="node_id") derivative: Optional[float] = Field(None, description="derivative") @@ -87,6 +92,7 @@ class ManningResistanceStatic(BaseModel): length: float = Field(..., description="length") manning_n: float = Field(..., description="manning_n") remarks: Optional[str] = Field("", description="a hack for pandera") + active: Optional[bool] = Field(None, description="active") profile_width: float = Field(..., description="profile_width") node_id: int = Field(..., description="node_id") profile_slope: float = Field(..., description="profile_slope") @@ -95,6 +101,7 @@ class ManningResistanceStatic(BaseModel): class FlowBoundaryStatic(BaseModel): remarks: Optional[str] = Field("", description="a hack for pandera") + active: Optional[bool] = Field(None, description="active") flow_rate: float = Field(..., description="flow_rate") node_id: int = Field(..., description="node_id") control_state: Optional[str] = Field(None, description="control_state") @@ -116,6 +123,7 @@ class TabulatedRatingCurveTime(BaseModel): class TabulatedRatingCurveStatic(BaseModel): remarks: Optional[str] = Field("", description="a hack for pandera") + active: Optional[bool] = Field(None, description="active") node_id: int = Field(..., description="node_id") discharge: float = Field(..., description="discharge") level: float = Field(..., description="level") @@ -136,6 +144,7 @@ class BasinProfile(BaseModel): class TerminalStatic(BaseModel): remarks: Optional[str] = Field("", description="a hack for pandera") + active: Optional[bool] = Field(None, description="active") node_id: int = Field(..., description="node_id") From e381fc7483909230b54e2a05eecd913e170a9140 Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Fri, 14 Jul 2023 12:19:43 +0200 Subject: [PATCH 2/6] Bugfixes --- core/src/create.jl | 3 +-- core/src/solve.jl | 1 - core/src/validation.jl | 1 - docs/schema/TerminalStatic.schema.json | 7 ------- python/ribasim/ribasim/models.py | 3 +-- python/ribasim/tests/test_io.py | 2 +- python/ribasim/tests/test_model.py | 5 +++-- 7 files changed, 6 insertions(+), 16 deletions(-) diff --git a/core/src/create.jl b/core/src/create.jl index bae853eee..49f0bc2f1 100644 --- a/core/src/create.jl +++ b/core/src/create.jl @@ -121,8 +121,7 @@ end function Terminal(db::DB, config::Config)::Terminal static = load_structvector(db, config, TerminalStaticV1) - active = coalesce.(static.active, true) - return Terminal(static.node_id, active) + return Terminal(static.node_id) end function Basin(db::DB, config::Config)::Basin diff --git a/core/src/solve.jl b/core/src/solve.jl index 5b8b907f9..17d28afc2 100644 --- a/core/src/solve.jl +++ b/core/src/solve.jl @@ -238,7 +238,6 @@ node_id: node ID of the Terminal node """ struct Terminal <: AbstractParameterNode node_id::Vector{Int} - active::BitVector end """ diff --git a/core/src/validation.jl b/core/src/validation.jl index 0fd6e48e9..e8cb8da27 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -169,7 +169,6 @@ end @version TerminalStaticV1 begin node_id::Int - active::Union{Missing, Bool} end @version DiscreteControlConditionV1 begin diff --git a/docs/schema/TerminalStatic.schema.json b/docs/schema/TerminalStatic.schema.json index 9791de678..4d4091a68 100644 --- a/docs/schema/TerminalStatic.schema.json +++ b/docs/schema/TerminalStatic.schema.json @@ -7,13 +7,6 @@ "description": "a hack for pandera", "type": "string" }, - "active": { - "format": "default", - "description": "active", - "type": [ - "boolean" - ] - }, "node_id": { "format": "default", "description": "node_id", diff --git a/python/ribasim/ribasim/models.py b/python/ribasim/ribasim/models.py index 6f5d402fc..5ce01a747 100644 --- a/python/ribasim/ribasim/models.py +++ b/python/ribasim/ribasim/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: root.schema.json -# timestamp: 2023-07-12T12:38:51+00:00 +# timestamp: 2023-07-14T09:57:55+00:00 from __future__ import annotations @@ -144,7 +144,6 @@ class BasinProfile(BaseModel): class TerminalStatic(BaseModel): remarks: Optional[str] = Field("", description="a hack for pandera") - active: Optional[bool] = Field(None, description="active") node_id: int = Field(..., description="node_id") diff --git a/python/ribasim/tests/test_io.py b/python/ribasim/tests/test_io.py index b24a7b91b..35a7a0422 100644 --- a/python/ribasim/tests/test_io.py +++ b/python/ribasim/tests/test_io.py @@ -68,5 +68,5 @@ def test_repr(): assert ( repr(pump_1) - == "\n static: DataFrame(rows=3)\n (max_flow_rate, remarks, flow_rate, node_id,\n control_state, min_flow_rate)" + == "\n static: DataFrame(rows=3)\n (max_flow_rate, remarks, active, flow_rate,\n node_id, control_state, min_flow_rate)" ) diff --git a/python/ribasim/tests/test_model.py b/python/ribasim/tests/test_model.py index 9f5225ad6..9124f6268 100644 --- a/python/ribasim/tests/test_model.py +++ b/python/ribasim/tests/test_model.py @@ -46,7 +46,8 @@ def test_invalid_node_id(basic): # Add entry with invalid node ID model.pump.static = model.pump.static._append( - {"flow_rate": 1, "node_id": -1, "remarks": ""}, ignore_index=True + {"flow_rate": 1, "node_id": -1, "remarks": "", "active": True}, + ignore_index=True, ) with pytest.raises( @@ -61,7 +62,7 @@ def test_node_id_duplicate(basic): # Add duplicate node ID model.pump.static = model.pump.static._append( - {"flow_rate": 1, "node_id": 1, "remarks": ""}, ignore_index=True + {"flow_rate": 1, "node_id": 1, "remarks": "", "active": True}, ignore_index=True ) with pytest.raises( From 9028332387c9dbd6e5c2f93ad8e661e5c5ee2aa8 Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Fri, 14 Jul 2023 14:03:28 +0200 Subject: [PATCH 3/6] Bugfixes --- core/src/create.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/src/create.jl b/core/src/create.jl index 11fde8db6..c06238670 100644 --- a/core/src/create.jl +++ b/core/src/create.jl @@ -48,6 +48,8 @@ function parse_static( # Index in the output vectors for this node ID node_idx = 1 + is_controllable = hasfield(static_type, :control_state) + for row in static if node_id != row.node_id node_idx += 1 @@ -55,7 +57,7 @@ function parse_static( end # If this row is a control state, add it to the control mapping - if !ismissing(row.control_state) + if is_controllable && !ismissing(row.control_state) control_values = NamedTuple{columnnames_variables}(values(row)[mask]) control_mapping[(row.node_id, row.control_state)] = control_values end @@ -169,7 +171,7 @@ end function LevelBoundary(db::DB, config::Config)::LevelBoundary static = load_structvector(db, config, LevelBoundaryStaticV1) defaults = (; active = true) - static_parsed = parse_static(static, db, "Levelboundary", defaults) + static_parsed = parse_static(static, db, "LevelBoundary", defaults) return LevelBoundary(static_parsed.node_id, static_parsed.active, static_parsed.level) end @@ -177,7 +179,11 @@ function FlowBoundary(db::DB, config::Config)::FlowBoundary static = load_structvector(db, config, FlowBoundaryStaticV1) defaults = (; active = true) static_parsed = parse_static(static, db, "FlowBoundary", defaults) - return FlowBoundary(static_parsed.node_id, static_parse.active, static_parsed.flow_rate) + return FlowBoundary( + static_parsed.node_id, + static_parsed.active, + static_parsed.flow_rate, + ) end function Pump(db::DB, config::Config)::Pump From 85e1c971fe92a55e9d6f4025491a6e4c6f3b8c9e Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Fri, 14 Jul 2023 14:37:53 +0200 Subject: [PATCH 4/6] Update usage docs --- core/src/solve.jl | 7 +++---- docs/core/usage.qmd | 29 +++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/src/solve.jl b/core/src/solve.jl index b74196785..52bf25bad 100644 --- a/core/src/solve.jl +++ b/core/src/solve.jl @@ -356,18 +356,17 @@ function continuous_control!( get_error!(pid_control, p) for (i, id) in enumerate(node_id) - # Should this integration continue when the PID node is inactive? - du.integral[i] = error[i] - controlled_node_id = only(outneighbors(graph_control, id)) # TODO: support the use of id_index controlled_node_idx = findfirst(pump.node_id .== controlled_node_id) if !active[i] - pump.flow_rate[controlled_node_idx] = 0.0 + du.integral[i] = 0.0 return end + du.integral[i] = error[i] + listened_node_id = listen_node_id[i] _, listened_node_idx = id_index(basin.node_id, listened_node_id) diff --git a/docs/core/usage.qmd b/docs/core/usage.qmd index f6c43d673..af62c80fd 100644 --- a/docs/core/usage.qmd +++ b/docs/core/usage.qmd @@ -122,6 +122,8 @@ name it must have in the GeoPackage if it is stored there. - DiscreteControl: Set parameters of other nodes based on model state conditions (e.g. basin level) - `DisceteControl / condition`: Conditions of the form 'the level in the basin with node id `n` is bigger than 2.0 m' - `DisceteControl / logic`: Translates the truth value of a set of conditions to parameter values for a controlled node +- PidControl: Controls the level in a basin by continuously controlling the flow rate of a connected pump or weir. See also [PID controller](https://en.wikipedia.org/wiki/PID_controller). + - `PidControl / static`: The proportional, integral and derivative parameters and which basin should be controlled. Adding a geometry to the node table can be helpful to examine models in [QGIS](https://qgis.org/en/site/), as it will show the location of the nodes on the map. The @@ -229,6 +231,7 @@ Lets a fraction (in [0,1]) of the incoming flow trough. column | type | unit | restriction ------------- | ------- | ------------ | ----------- node_id | Int | - | sorted +active | Bool | - | (optional, default true) fraction | Float64 | - | in the interval [0,1] control_state | String | - | (optional) @@ -241,6 +244,7 @@ relation between the storage of a connected Basin (via the outlet level) and its column | type | unit | restriction --------- | ------- | ------------ | ----------- node_id | Int | - | sorted +active | Bool | - | (optional, default true) level | Float64 | $m$ | - discharge | Float64 | $m^3 s^{-1}$ | non-negative @@ -279,8 +283,9 @@ Note that the intake must always be a Basin. column | type | unit | restriction --------- | ------- | ------------ | ----------- node_id | Int | - | sorted +active | Bool | - | (optional, default true) flow_rate | Float64 | $m^3 s^{-1}$ | - -min_flow_rate | Float64 | $m^3 s^{-1}$ | (optional, defaults to 0.0) +min_flow_rate | Float64 | $m^3 s^{-1}$ | (optional, default 0.0) max_flow_rate | Float64 | $m^3 s^{-1}$ | (optional) control_state | String | - | (optional) @@ -294,8 +299,8 @@ exchange water with the basin based on the difference in water level between the column | type | unit | restriction ------------- | ------- | ------------ | ----------- node_id | Int | - | sorted +active | Bool | - | (optional, default true) level | Float64 | $m^3$ | non-negative -control_state | String | - | (optional) @@ -311,6 +316,7 @@ Note that the connected node must always be a Basin. column | type | unit | restriction ------------- | ------- | ------------ | ----------- node_id | Int | - | sorted +active | Bool | - | (optional, default true) flow_rate | Float64 | $m^3 s^{-1}$ | - @@ -321,6 +327,7 @@ Flow proportional to the level difference between the connected basins. column | type | unit | restriction ------------- | ------- | ------------ | ----------- node_id | Int | - | sorted +active | Bool | - | (optional, default true) resistance | Float64 | $sm^{-2}$ | - control_state | String | - | (optional) @@ -332,6 +339,7 @@ Flow through this connection is estimated by conservation of energy and the Mann column | type | unit | restriction ------------- | ------- | ------------ | ----------- node_id | Int | - | sorted +active | Bool | - | (optional, default true) length | Float64 | $m$ | positive manning_n | Float64 | $s m^{-\frac{1}{3}}$ | positive profile_with | Float64 | $m$ | positive @@ -428,6 +436,23 @@ truth_state | String control_state | String + +### PidControl + +The PidControl node controls the level in a basin by continuously controlling the flow rate of a connected pump or weir. See also [PID controller](https://en.wikipedia.org/wiki/PID_controller). When A PidControl node is made inactive, the node under its control retains the last flow rate vale. + +column | type | unit | restriction +-------------- | -------- | -------- | ----------- +node_id | Int | - | sorted +active | Bool | - | (optional, default true) +listen_node_id | Int | - | - +proportional | Float64 | $s^{-1}$ | (optional, default 0.0) +integral | Float64 | $s^{-2}$ | (optional, default 0.0) +derivative | Floar64 | - | (optional, default 0.0) +control_state | String | - | (optional) + + + ## Example input files From [this link](https://github.com/visr/ribasim-artifacts/releases) you can download an From 4e62f475d35ad55cc77eb0085f4852692e64518b Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Fri, 14 Jul 2023 14:47:00 +0200 Subject: [PATCH 5/6] Set PID integral state to 0 when inactive. --- core/src/solve.jl | 2 ++ docs/core/usage.qmd | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/solve.jl b/core/src/solve.jl index 52bf25bad..30909fbbe 100644 --- a/core/src/solve.jl +++ b/core/src/solve.jl @@ -339,6 +339,7 @@ function get_error!(pid_control::PidControl, p::Parameters) end function continuous_control!( + u::ComponentVector{Float64}, du::ComponentVector{Float64}, pid_control::PidControl, p::Parameters, @@ -362,6 +363,7 @@ function continuous_control!( if !active[i] du.integral[i] = 0.0 + u.integral[i] = 0.0 return end diff --git a/docs/core/usage.qmd b/docs/core/usage.qmd index af62c80fd..b8a997d95 100644 --- a/docs/core/usage.qmd +++ b/docs/core/usage.qmd @@ -439,7 +439,7 @@ control_state | String ### PidControl -The PidControl node controls the level in a basin by continuously controlling the flow rate of a connected pump or weir. See also [PID controller](https://en.wikipedia.org/wiki/PID_controller). When A PidControl node is made inactive, the node under its control retains the last flow rate vale. +The PidControl node controls the level in a basin by continuously controlling the flow rate of a connected pump or weir. See also [PID controller](https://en.wikipedia.org/wiki/PID_controller). When A PidControl node is made inactive, the node under its control retains the last flow rate value, and the error integral is reset to 0. column | type | unit | restriction -------------- | -------- | -------- | ----------- From f9bec186ccc7c70c6bb07c76ace0e1f22b689f0d Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Fri, 14 Jul 2023 14:58:47 +0200 Subject: [PATCH 6/6] Bugfix --- core/src/solve.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/solve.jl b/core/src/solve.jl index 30909fbbe..2885afd93 100644 --- a/core/src/solve.jl +++ b/core/src/solve.jl @@ -686,7 +686,7 @@ function water_balance!( formulate!(du, basin, storage, t) # PID control (does not set flows) - continuous_control!(du, pid_control, p, integral) + continuous_control!(u, du, pid_control, p, integral) # First formulate intermediate flows formulate_flows!(p, storage)