From 1d8634f6f30fdedc9e9dd8e9323bf898b56da4d9 Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Thu, 25 Apr 2024 17:28:13 +0200 Subject: [PATCH 1/3] Make LevelDemand min_level and max_level optional --- core/src/read.jl | 5 +++-- core/src/schema.jl | 8 ++++---- core/src/util.jl | 9 +-------- core/test/allocation_test.jl | 10 ++++++++++ docs/core/usage.qmd | 11 +++++++---- python/ribasim/ribasim/schemas.py | 8 ++++---- .../ribasim_testmodels/allocation.py | 14 +++++++++++++- 7 files changed, 42 insertions(+), 23 deletions(-) diff --git a/core/src/read.jl b/core/src/read.jl index 9b84fed30..1a8351c6c 100644 --- a/core/src/read.jl +++ b/core/src/read.jl @@ -93,7 +93,7 @@ function parse_static_and_time( errors = false t_end = seconds_since(config.endtime, config.starttime) - trivial_timespan = [nextfloat(-Inf), prevfloat(Inf)] + trivial_timespan = [0.0, prevfloat(Inf)] for (node_idx, node_id) in enumerate(node_ids) if node_id in static_node_ids @@ -812,7 +812,7 @@ function UserDemand(db::DB, config::Config)::UserDemand realized_bmi = zeros(n_user) demand = zeros(n_user, n_priority) demand_reduced = zeros(n_user, n_priority) - trivial_timespan = [nextfloat(-Inf), prevfloat(Inf)] + trivial_timespan = [0.0, prevfloat(Inf)] demand_itp = [ [LinearInterpolation(zeros(2), trivial_timespan) for i in eachindex(priorities)] for j in eachindex(node_ids) ] @@ -877,6 +877,7 @@ function LevelDemand(db::DB, config::Config)::LevelDemand static, time, time_interpolatables = [:min_level, :max_level], + defaults = (; min_level = -Inf, max_level = Inf), ) if !valid diff --git a/core/src/schema.jl b/core/src/schema.jl index 0875e48c5..9ca14f9c6 100644 --- a/core/src/schema.jl +++ b/core/src/schema.jl @@ -250,16 +250,16 @@ end @version LevelDemandStaticV1 begin node_id::Int32 - min_level::Float64 - max_level::Float64 + min_level::Union{Missing, Float64} + max_level::Union{Missing, Float64} priority::Int32 end @version LevelDemandTimeV1 begin node_id::Int32 time::DateTime - min_level::Float64 - max_level::Float64 + min_level::Union{Missing, Float64} + max_level::Union{Missing, Float64} priority::Int32 end diff --git a/core/src/util.jl b/core/src/util.jl index b1f816d9c..9ec96f6d6 100644 --- a/core/src/util.jl +++ b/core/src/util.jl @@ -31,17 +31,10 @@ function get_storage_from_level(basin::Basin, state_idx::Int, level::Float64)::F storage_discrete = basin.storage[state_idx] area_discrete = basin.area[state_idx] level_discrete = basin.level[state_idx] - bottom = first(level_discrete) - - if level < bottom - node_id = basin.node_id.values[state_idx] - @error "The level $level of $node_id is lower than the bottom of this basin; $bottom." - return NaN - end level_lower_index = searchsortedlast(level_discrete, level) - # If the level is equal to the bottom then the storage is 0 + # If the level is at or below the bottom then the storage is 0 if level_lower_index == 0 return 0.0 end diff --git a/core/test/allocation_test.jl b/core/test/allocation_test.jl index d49f0512d..d10883135 100644 --- a/core/test/allocation_test.jl +++ b/core/test/allocation_test.jl @@ -313,6 +313,8 @@ end end @testitem "Allocation level control" begin + import JuMP + toml_path = normpath(@__DIR__, "../../generated_testmodels/level_demand/ribasim.toml") @test ispath(toml_path) model = Ribasim.run(toml_path) @@ -377,6 +379,14 @@ end stage_6_start_idx = findfirst(stage_6) u_stage_6(τ) = storage[stage_6_start_idx] @test storage[stage_6] ≈ u_stage_6.(t[stage_6]) rtol = 1e-4 + + # Isolated LevelDemand + Basin pair to test optional min_level + problem = allocation.allocation_models[2].problem + @test JuMP.value(only(problem[:F_basin_in])) == 0.0 + @test JuMP.value(only(problem[:F_basin_out])) == 0.0 + q = JuMP.normalized_rhs(only(problem[:basin_outflow])) + storage_surplus = 1000.0 # Basin #7 is 1000 m2 and 1 m above LevelDemand max_level + @test q ≈ storage_surplus / Δt_allocation end @testitem "Flow demand" begin diff --git a/docs/core/usage.qmd b/docs/core/usage.qmd index 874e7d57f..a788c7c5a 100644 --- a/docs/core/usage.qmd +++ b/docs/core/usage.qmd @@ -479,14 +479,17 @@ min_level | Float64 | $m$ | - A `LevelDemand` node associates a minimum and a maximum level with connected basins to be used by the allocation algorithm. Below the minimum level the basin has a demand of the supplied priority, -above the maximum level the basin acts as a source, used by all nodes with demands in order of priority. -The same `LevelDemand` node can be used for basins in different subnetworks. +above the maximum level the basin has a surplus and acts as a source, used by all nodes with demands in order of priority. +The same `LevelDemand` node can be used for Basins in different subnetworks. + +Both `min_level` and `max_level` are optional, to be able to handle only the demand or surplus side. +If both are missing `LevelDemand` won't have any effects on allocation. column | type | unit | restriction ------------- | ------- | ------------ | ----------- node_id | Int32 | - | sorted -min_level | Float64 | $m$ | - -max_level | Float64 | $m$ | - +min_level | Float64 | $m$ | (optional, default -Inf) +max_level | Float64 | $m$ | (optional, default Inf) priority | Int32 | - | positive ## LevelDemand / time diff --git a/python/ribasim/ribasim/schemas.py b/python/ribasim/ribasim/schemas.py index 2a8b5eab5..40f0e0a38 100644 --- a/python/ribasim/ribasim/schemas.py +++ b/python/ribasim/ribasim/schemas.py @@ -115,16 +115,16 @@ class LevelBoundaryTimeSchema(_BaseSchema): class LevelDemandStaticSchema(_BaseSchema): node_id: Series[Int32] = pa.Field(nullable=False, default=0) - min_level: Series[float] = pa.Field(nullable=False) - max_level: Series[float] = pa.Field(nullable=False) + min_level: Series[float] = pa.Field(nullable=True) + max_level: Series[float] = pa.Field(nullable=True) priority: Series[Int32] = pa.Field(nullable=False, default=0) class LevelDemandTimeSchema(_BaseSchema): node_id: Series[Int32] = pa.Field(nullable=False, default=0) time: Series[Timestamp] = pa.Field(nullable=False) - min_level: Series[float] = pa.Field(nullable=False) - max_level: Series[float] = pa.Field(nullable=False) + min_level: Series[float] = pa.Field(nullable=True) + max_level: Series[float] = pa.Field(nullable=True) priority: Series[Int32] = pa.Field(nullable=False, default=0) diff --git a/python/ribasim_testmodels/ribasim_testmodels/allocation.py b/python/ribasim_testmodels/ribasim_testmodels/allocation.py index 9651ddd87..746f8dd4b 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/allocation.py +++ b/python/ribasim_testmodels/ribasim_testmodels/allocation.py @@ -883,7 +883,7 @@ def subnetworks_with_sources_model() -> Model: def level_demand_model() -> Model: - """Small model with a LevelDemand.""" + """Small model with LevelDemand nodes.""" model = Model( starttime="2020-01-01", @@ -922,12 +922,24 @@ def level_demand_model() -> Model: [basin.Profile(area=1000.0, level=[0.0, 1.0]), basin.State(level=[0.5])], ) + # Isolated LevelDemand + Basin pair to test optional min_level + model.level_demand.add( + Node(6, Point(3, -1), subnetwork_id=3), + [level_demand.Static(max_level=[1.0], priority=1)], + ) + model.basin.add( + Node(7, Point(3, 0), subnetwork_id=3), + [basin.Profile(area=1000.0, level=[0.0, 1.0]), basin.State(level=[2.0])], + ) + model.edge.add(model.flow_boundary[1], model.basin[2], subnetwork_id=2) model.edge.add(model.basin[2], model.user_demand[3]) model.edge.add(model.level_demand[4], model.basin[2]) model.edge.add(model.user_demand[3], model.basin[5]) model.edge.add(model.level_demand[4], model.basin[5]) + model.edge.add(model.level_demand[6], model.basin[7]) + return model From 853ed7c34757b321e17348bffcaaa01b4985a4dd Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Fri, 26 Apr 2024 10:04:21 +0200 Subject: [PATCH 2/3] Update tests --- core/src/util.jl | 5 ++++- core/test/utils_test.jl | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/src/util.jl b/core/src/util.jl index 9ec96f6d6..699efa509 100644 --- a/core/src/util.jl +++ b/core/src/util.jl @@ -70,7 +70,10 @@ function get_storages_from_levels(basin::Basin, levels::Vector)::Vector{Float64} for (i, level) in enumerate(levels) storage = get_storage_from_level(basin, i, level) - if isnan(storage) + bottom = first(basin.level[i]) + node_id = basin.node_id.values[i] + if level < bottom + @error "The initial level ($level) of $node_id is below the bottom ($bottom)." errors = true end storages[i] = storage diff --git a/core/test/utils_test.jl b/core/test/utils_test.jl index 74203eb6a..e6dadb501 100644 --- a/core/test/utils_test.jl +++ b/core/test/utils_test.jl @@ -114,14 +114,17 @@ end @test length(logger.logs) == 1 @test logger.logs[1].level == Error @test logger.logs[1].message == - "The level -1.0 of Basin #1 is lower than the bottom of this basin; 0.0." + "The initial level (-1.0) of Basin #1 is below the bottom (0.0)." # Converting from storages to levels and back should return the same storages storages = range(0.0, 2 * storage[end], 50) levels = [Ribasim.get_area_and_level(basin, 1, s)[2] for s in storages] storages_ = [Ribasim.get_storage_from_level(basin, 1, l) for l in levels] - @test storages ≈ storages_ + + # At or below bottom the storage is 0 + @test Ribasim.get_storage_from_level(basin, 1, 0.0) == 0.0 + @test Ribasim.get_storage_from_level(basin, 1, -1.0) == 0.0 end @testitem "Expand logic_mapping" begin From 385a5165e68e3d2d3a9d511a14cd188ebf9dd5fb Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Mon, 29 Apr 2024 12:45:05 +0200 Subject: [PATCH 3/3] Update docs/core/usage.qmd Co-authored-by: Bart de Koning <74617371+SouthEndMusic@users.noreply.github.com> --- docs/core/usage.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/usage.qmd b/docs/core/usage.qmd index a788c7c5a..a54d48a6d 100644 --- a/docs/core/usage.qmd +++ b/docs/core/usage.qmd @@ -483,7 +483,7 @@ above the maximum level the basin has a surplus and acts as a source, used by al The same `LevelDemand` node can be used for Basins in different subnetworks. Both `min_level` and `max_level` are optional, to be able to handle only the demand or surplus side. -If both are missing `LevelDemand` won't have any effects on allocation. +If both are missing, `LevelDemand` won't have any effects on allocation. column | type | unit | restriction ------------- | ------- | ------------ | -----------