diff --git a/core/src/Ribasim.jl b/core/src/Ribasim.jl index b8027f058..bc256bc70 100644 --- a/core/src/Ribasim.jl +++ b/core/src/Ribasim.jl @@ -64,9 +64,9 @@ TimerOutputs.complement!() include("validation.jl") include("solve.jl") -include("allocation.jl") include("config.jl") using .config +include("allocation.jl") include("utils.jl") include("lib.jl") include("io.jl") diff --git a/core/src/allocation.jl b/core/src/allocation.jl index 36a6f53c0..c3e6b1658 100644 --- a/core/src/allocation.jl +++ b/core/src/allocation.jl @@ -409,34 +409,20 @@ function add_variables_flow!( end """ -Add the basin allocation variables A_basin to the allocation problem. -The variable indices are the allocation graph basin node IDs. -Non-negativivity constraints are also immediately added to the basin allocation variables. +Certain allocation distribution types use absolute values in the objective function. +Since most optimization packages do not support the absolute value function directly, +New variables are introduced that act as the absolute value of an expression by +posing the appropriate constraints. """ -function add_variables_allocation_basin!( - problem::JuMP.Model, - node_id_mapping::Dict{NodeID, Tuple{Int, Symbol}}, - allocgraph_node_ids_basin::Vector{Int}, -)::Nothing - JuMP.@variable(problem, A_basin[i = allocgraph_node_ids_basin] >= 0.0) - return nothing -end - -""" -Add the user allocation constraints to the allocation problem: -The flow to a user is bounded from above by the demand of the user. -""" -function add_constraints_user_allocation!( +function add_variables_absolute_value!( problem::JuMP.Model, allocgraph_edge_ids_user_demand::Dict{Int, Int}, + config::Config, )::Nothing - F = problem[:F] - # Allocation flows are bounded from above by demands - problem[:demand_user] = JuMP.@constraint( - problem, - [i = values(allocgraph_edge_ids_user_demand)], - F[i] <= 0.0 - ) + if startswith(config.allocation.objective_type, "linear") + problem[:F_abs] = + JuMP.@variable(problem, F_abs[values(allocgraph_edge_ids_user_demand)]) + end return nothing end @@ -571,20 +557,55 @@ function add_constraints_user_returnflow!( end """ -Add the objective function to be maximized to the allocation problem. -Objective function: Sum of flows to the users. +Minimizing |expr| can be achieved by introducing a new variable expr_abs +and posing the following constraints: +expr_abs >= expr +expr_abs >= -expr """ -function add_objective_function!( +function add_constraints_absolute_value!( problem::JuMP.Model, allocgraph_edge_ids_user_demand::Dict{Int, Int}, + config::Config, )::Nothing - F = problem[:F] - A_basin = problem[:A_basin] - JuMP.@objective( - problem, - Max, - sum(A_basin) + sum([F[i] for i in values(allocgraph_edge_ids_user_demand)]) - ) + objective_type = config.allocation.objective_type + if startswith(objective_type, "linear") + allocgraph_edge_ids_user_demand = collect(values(allocgraph_edge_ids_user_demand)) + F = problem[:F] + F_abs = problem[:F_abs] + d = 2.0 + + if config.allocation.objective_type == "linear_absolute" + # These constraints together make sure that F_abs acts as the absolute + # value F_abs = |x| where x = F-d (here for example d = 2) + problem[:abs_positive] = JuMP.@constraint( + problem, + [i = allocgraph_edge_ids_user_demand], + F_abs[i] >= (F[i] - d), + base_name = "abs_positive" + ) + problem[:abs_negative] = JuMP.@constraint( + problem, + [i = allocgraph_edge_ids_user_demand], + F_abs[i] >= -(F[i] - d), + base_name = "abs_negative" + ) + elseif config.allocation.objective_type == "linear_relative" + # These constraints together make sure that F_abs acts as the absolute + # value F_abs = |x| where x = 1-F/d (here for example d = 2) + problem[:abs_positive] = JuMP.@constraint( + problem, + [i = allocgraph_edge_ids_user_demand], + F_abs[i] >= (1 - F[i] / d), + base_name = "abs_positive" + ) + problem[:abs_negative] = JuMP.@constraint( + problem, + [i = allocgraph_edge_ids_user_demand], + F_abs[i] >= -(1 - F[i] / d), + base_name = "abs_negative" + ) + end + end return nothing end @@ -592,6 +613,7 @@ end Construct the allocation problem for the current subnetwork as a JuMP.jl model. """ function allocation_problem( + config::Config, node_id_mapping::Dict{NodeID, Tuple{Int, Symbol}}, allocgraph_node_ids_user_with_returnflow::Vector{Int}, allocgraph_edges::Vector{Edge{Int}}, @@ -613,11 +635,10 @@ function allocation_problem( # Add variables to problem add_variables_flow!(problem, allocgraph_edges) - add_variables_allocation_basin!(problem, node_id_mapping, allocgraph_node_ids_basin) + add_variables_absolute_value!(problem, allocgraph_edge_ids_user_demand, config) + # TODO: Add variables for allocation to basins # Add constraints to problem - add_constraints_user_allocation!(problem, allocgraph_edge_ids_user_demand) - add_constraints_basin_allocation!(problem, allocgraph_node_ids_basin) add_constraints_capacity!(problem, capacity, allocgraph_edges) add_constraints_source!( problem, @@ -632,11 +653,9 @@ function allocation_problem( allocgraph_node_outedge_ids, ) add_constraints_user_returnflow!(problem, allocgraph_node_ids_user_with_returnflow) + add_constraints_absolute_value!(problem, allocgraph_edge_ids_user_demand, config) # TODO: The fractional flow constraints - # Add objective to problem - add_objective_function!(problem, allocgraph_edge_ids_user_demand) - return problem end @@ -662,6 +681,7 @@ An AllocationModel object. """ function AllocationModel( + config::Config, allocation_network_id::Int, p::Parameters, subnetwork_node_ids::Vector{NodeID}, @@ -680,6 +700,7 @@ function AllocationModel( # The JuMP.jl allocation problem problem = allocation_problem( + config, node_id_mapping, allocgraph_node_ids_user_with_returnflow, allocgraph_edges, @@ -690,6 +711,7 @@ function AllocationModel( ) return AllocationModel( + Symbol(config.allocation.objective_type), allocation_network_id, subnetwork_node_ids, node_id_mapping, @@ -704,27 +726,69 @@ function AllocationModel( end """ -Set the demands of the users of the current time and priority -in the allocation problem. +Set the objective for the given priority. +For an objective with absolute values this also involves adjusting constraints. """ -function set_demands_priority!( +function set_objective_priority!( allocation_model::AllocationModel, user::User, - priority_idx::Int, t::Float64, + priority_idx::Int, )::Nothing - (; problem, allocgraph_edge_ids_user_demand, node_id_mapping_inverse) = allocation_model + (; objective_type, problem, allocgraph_edge_ids_user_demand, node_id_mapping_inverse) = + allocation_model (; demand, node_id) = user - constraints_demand = problem[:demand_user] + F = problem[:F] + if objective_type in [:quadratic_absolute, :quadratic_relative] + ex = JuMP.QuadExpr() + elseif objective_type in [:linear_absolute, :linear_relative] + ex = sum(problem[:F_abs]) + end for (allocgraph_node_id, allocgraph_edge_id) in allocgraph_edge_ids_user_demand - model_user_id = node_id_mapping_inverse[allocgraph_node_id][1] - user_idx = findsorted(node_id, model_user_id) - JuMP.set_normalized_rhs( - constraints_demand[allocgraph_edge_id], - demand[user_idx][priority_idx](t), - ) + user_idx = findsorted(node_id, node_id_mapping_inverse[allocgraph_node_id][1]) + d = demand[user_idx][priority_idx](t) + + if objective_type == :quadratic_absolute + # Objective function ∑ (F - d)^2 + F_ij = F[allocgraph_edge_id] + JuMP.add_to_expression!(ex, 1, F_ij, F_ij) + JuMP.add_to_expression!(ex, -2 * d, F_ij) + JuMP.add_to_expression!(ex, d^2) + + elseif objective_type == :quadratic_relative + # Objective function ∑ (1 - F/d)^2S + if d ≈ 0 + continue + end + F_ij = F[allocgraph_edge_id] + JuMP.add_to_expression!(ex, 1.0 / d^2, F_ij, F_ij) + JuMP.add_to_expression!(ex, -2.0 / d, F_ij) + JuMP.add_to_expression!(ex, 1.0) + + elseif objective_type == :linear_absolute + # Objective function ∑ |F - d| + JuMP.set_normalized_rhs(problem[:abs_positive][allocgraph_edge_id], -d) + JuMP.set_normalized_rhs(problem[:abs_negative][allocgraph_edge_id], d) + + elseif objective_type == :linear_relative + # Objective function ∑ |1 - F/d| + JuMP.set_normalized_coefficient( + problem[:abs_positive][allocgraph_edge_id], + F[allocgraph_edge_id], + iszero(d) ? 0 : 1 / d, + ) + JuMP.set_normalized_coefficient( + problem[:abs_negative][allocgraph_edge_id], + F[allocgraph_edge_id], + iszero(d) ? 0 : -1 / d, + ) + else + error("Invalid allocation objective type $objective_type.") + end end + new_objective = JuMP.@expression(problem, ex) + JuMP.@objective(problem, Min, new_objective) return nothing end @@ -847,7 +911,12 @@ function allocate!(p::Parameters, allocation_model::AllocationModel, t::Float64) # or set edge capacities if priority_idx = 1 adjust_edge_capacities!(allocation_model, priority_idx) - set_demands_priority!(allocation_model, user, priority_idx, t) + # Set the objective depending on the demands + # A new objective function is set instead of modifying the coefficients + # of an existing objective function because this is not supported for + # quadratic terms: + # https://jump.dev/JuMP.jl/v1.16/manual/objective/#Modify-an-objective-coefficient + set_objective_priority!(allocation_model, user, t, priority_idx) # Solve the allocation problem for this priority JuMP.optimize!(problem) diff --git a/core/src/config.jl b/core/src/config.jl index 5fd8692c3..848091631 100644 --- a/core/src/config.jl +++ b/core/src/config.jl @@ -126,6 +126,7 @@ end @option struct Allocation <: TableOption timestep::Union{Float64, Nothing} = nothing use_allocation::Bool = false + objective_type::String = "quadratic_relative" end @option @addnodetypes struct Config <: TableOption diff --git a/core/src/create.jl b/core/src/create.jl index a622885dc..0346ddff6 100644 --- a/core/src/create.jl +++ b/core/src/create.jl @@ -273,6 +273,7 @@ function generate_allocation_models!(p::Parameters, db::DB, config::Config)::Not push!( connectivity.allocation_models, AllocationModel( + config, allocation_network_id, p, NodeID.(allocation_group_node.fid), @@ -796,6 +797,18 @@ function User(db::DB, config::Config)::User abstracted = Float64[], ) + record = ( + time = Vector{Float64}(), + allocation_network_id = Vector{Int}(), + user_node_id = Vector{Int}(), + priority = Vector{Int}(), + demand = Vector{Float64}(), + allocated = Vector{Float64}(), + abstracted = Vector{Float64}(), + ) + + allocation_optimized = BitVector(zeros(UInt8, length(node_ids))) + return User( NodeID.(node_ids), active, diff --git a/core/src/solve.jl b/core/src/solve.jl index 845b67754..0f9ba5177 100644 --- a/core/src/solve.jl +++ b/core/src/solve.jl @@ -7,6 +7,7 @@ const VectorInterpolation = """ Store information for a subnetwork used for allocation. +objective_type: The name of the type of objective used allocation_network_id: The ID of this allocation network node_id: All the IDs of the nodes that are in this subnetwork node_id_mapping: Mapping Dictionary; model_node_id => AG_node_id where such a correspondence exists @@ -20,6 +21,7 @@ problem: The JuMP.jl model for solving the allocation problem Δt_allocation: The time interval between consecutive allocation solves """ struct AllocationModel + objective_type::Symbol allocation_network_id::Int node_id::Vector{NodeID} node_id_mapping::Dict{NodeID, Tuple{Int, Symbol}} diff --git a/core/test/allocation_test.jl b/core/test/allocation_test.jl index 925a7fcc4..f84fbaac9 100644 --- a/core/test/allocation_test.jl +++ b/core/test/allocation_test.jl @@ -18,7 +18,7 @@ Ribasim.allocate!(p, allocation_model, 0.0) F = JuMP.value.(allocation_model.problem[:F]) - @test F ≈ [0.0, 4.0, 0.0, 0.0, 0.0, 4.5] + @test F ≈ [0.0, 4.0, 0.0, 0.0, 0.0, 4.0] allocated = p.user.allocated @test allocated[1] ≈ [0.0, 4.0] @@ -26,61 +26,53 @@ @test allocated[3] ≈ [0.0, 0.0] end -@testitem "Simulation with allocation" begin +@testitem "Allocation objective types" begin using DataFrames: DataFrame + using SciMLBase: successful_retcode import JuMP toml_path = normpath(@__DIR__, "../../generated_testmodels/minimal_subnetwork/ribasim.toml") @test ispath(toml_path) - model = Ribasim.run(toml_path) - record = DataFrame(model.integrator.p.user.record) - where_5 = (record.user_node_id .== 5) - n_datapoints = sum(where_5) - record_5 = record[where_5, :] - record_6 = record[.!where_5, :] + config = Ribasim.Config(toml_path; allocation_objective_type = "quadratic_absolute") + model = Ribasim.run(config) + @test successful_retcode(model) + problem = model.integrator.p.connectivity.allocation_models[1].problem + objective = JuMP.objective_function(problem) + @test objective isa JuMP.QuadExpr # Quadratic expression + F = problem[:F] + @test JuMP.UnorderedPair{JuMP.VariableRef}(F[2], F[2]) in keys(objective.terms) # F[2]^2 term + @test JuMP.UnorderedPair{JuMP.VariableRef}(F[3], F[3]) in keys(objective.terms) # F[3]^2 term - @test all(record_5.demand .== 1.0e-3) - @test all( - isapprox( - record_5.allocated, - collect(range(1.0e-3, 0.0, n_datapoints)); - rtol = 0.01, - ), - ) - @test all( - isapprox( - record_5.abstracted[2:end], - collect(range(1.0e-3, 0.0, n_datapoints))[2:end]; - rtol = 0.01, - ), - ) - @test all( - isapprox( - record_6.demand, - collect(range(1.0e-3, 2.0e-3, n_datapoints)); - rtol = 0.01, - ), - ) - @test all( - isapprox( - record_6.allocated, - collect(range(1.0e-3, 2.0e-3, n_datapoints)); - rtol = 0.01, - ), - ) - @test all( - isapprox( - record_6.abstracted[2:end], - collect(range(1.0e-3, 2.0e-3, n_datapoints))[2:end]; - rtol = 0.01, - ), - ) + config = Ribasim.Config(toml_path; allocation_objective_type = "quadratic_relative") + model = Ribasim.run(config) + @test successful_retcode(model) + problem = model.integrator.p.connectivity.allocation_models[1].problem + objective = JuMP.objective_function(problem) + @test objective isa JuMP.QuadExpr # Quadratic expression + @test objective.aff.constant == 2.0 + F = problem[:F] + @test JuMP.UnorderedPair{JuMP.VariableRef}(F[2], F[2]) in keys(objective.terms) # F[2]^2 term + @test JuMP.UnorderedPair{JuMP.VariableRef}(F[3], F[3]) in keys(objective.terms) # F[3]^2 term - allocation_output_path = normpath( - @__DIR__, - "../../generated_testmodels/minimal_subnetwork/results/allocation.arrow", - ) - @test isfile(allocation_output_path) + config = Ribasim.Config(toml_path; allocation_objective_type = "linear_absolute") + model = Ribasim.run(config) + @test successful_retcode(model) + problem = model.integrator.p.connectivity.allocation_models[1].problem + objective = JuMP.objective_function(problem) + @test objective isa JuMP.AffExpr # Affine expression + @test :F_abs in keys(problem.obj_dict) + F_abs = problem[:F_abs] + @test objective == F_abs[3] + F_abs[2] + + config = Ribasim.Config(toml_path; allocation_objective_type = "linear_relative") + model = Ribasim.run(config) + @test successful_retcode(model) + problem = model.integrator.p.connectivity.allocation_models[1].problem + objective = JuMP.objective_function(problem) + @test objective isa JuMP.AffExpr # Affine expression + @test :F_abs in keys(problem.obj_dict) + F_abs = problem[:F_abs] + @test objective == F_abs[3] + F_abs[2] end diff --git a/core/test/docs.toml b/core/test/docs.toml index 42d38f7c1..727609a1b 100644 --- a/core/test/docs.toml +++ b/core/test/docs.toml @@ -17,6 +17,7 @@ time = "basin/time.arrow" [allocation] timestep = 86400 use_allocation = true +objective_type = "quadratic_relative" [solver] algorithm = "QNDF" # optional, default "QNDF" diff --git a/docs/contribute/core.qmd b/docs/contribute/core.qmd index cd611748f..7817dcb6f 100644 --- a/docs/contribute/core.qmd +++ b/docs/contribute/core.qmd @@ -2,6 +2,73 @@ title: "Julia core development" --- +# Julia core overview + +This section is about the Julia core in `Ribasim.jl`. See the component overview [here](index.qmd) for the context of this computational core. + +`Ribasim.jl` can be divided into 3 parts: + +- Model initialization +- Running the simulation loop +- Writing the output files + +The figure below gives a more detailed description of the simulation loop in the form of a [sequence diagram](https://en.wikipedia.org/wiki/Sequence_diagram). From top to bottom, it contains the following blocks: + +- Allocation optimization; activated when the allocation timestep has been passed; +- Control actions; activated when some discrete control callback is triggered; +- Water balance; computing the flows over flow edges happens each timestep; +- Time integration step; done by the integrator from `OrdinaryDiffEq.jl`. + +```{mermaid, style="margin-top: 0"} +sequenceDiagram + autonumber + participant Int as Process: Integrator + participant Optim as Process: Allocation optimization + participant Param as Data: Parameters + participant State as Data: State + participant Sim as Process: Water balance + loop Simulation loop (OrdinaryDiffEq.jl) + activate Int + %% Allocation + rect rgb(200, 200, 200) + opt Allocation optimization, per allocation network (JuMP.jl, HiGHS) + activate Optim + Int->>Optim: Callback: allocation timestep has passed + Param-->>Optim: Input + State-->>Optim: Input + Optim->>Optim: Optimize Basin allocations if below target level + Optim->>Optim: Optimize User allocation, per priority + Optim-->>Param: Set allocated flow rates + deactivate Optim + end + end + %% Control + rect rgb(200, 200, 200) + opt Control actions + Int->>Int: DiscreteControl callback + Int-->>Param: Parameter updates by control + end + end + %% water_balance! + rect rgb(200, 200, 200) + activate Sim + State-->>Sim: Input + Param-->>Sim: Input + Sim->>Sim: Compute flows over edges per node type + Sim-->>Param: Set flows + deactivate Sim + end + %% Time integration + rect rgb(200, 200, 200) + State-->>Int: Input + Param-->>Int: Input + Int->>Int: Time integration step + Int-->>State: Update state + end + deactivate Int + end +``` + # Set up the developer environment ## Install Julia via Juliaup diff --git a/docs/core/allocation.qmd b/docs/core/allocation.qmd index 35f09279a..462e31e58 100644 --- a/docs/core/allocation.qmd +++ b/docs/core/allocation.qmd @@ -94,7 +94,7 @@ There are 2 types of variables whose value has to be determined to solve the all $$ A^\text{basin}_{\hat{i}} \ge 0, \quad \hat{B}_S, $$ -where $\hat{B} = m_S(B_S) \subset \hat{V}_S$ is the set of basin node ids in the allocation graph. +where $\hat{B}_S = m_S(B_S) \subset \hat{V}_S$ is the set of basin node ids in the allocation graph. :::{.callout-note} Currently the basin allocations are not taken into account in the implementation. @@ -102,17 +102,37 @@ Currently the basin allocations are not taken into account in the implementation ### The optimization objective -The goal of allocation is to maximize the flow to the users. Therefore, we use the following optimization objective: +The goal of allocation is to get the flow to the users as close as possible to their demand. To achieve this, the following objectives are supported: + +- `quadratic_absolute`: +$$ + \min \sum_{(\hat{i},\hat{j})\in \hat{E}_S\;:\; \hat{i}\in \hat{U}_S} \left( F_{\hat{i}\hat{j}} - d_j^p(t)\right)^2 +$$ +- `quadratic_relative`: +$$ + \min \sum_{(\hat{i},\hat{j})\in \hat{E}_S\;:\; \hat{i}\in \hat{U}_S} \left( 1 - \frac{F_{\hat{i}\hat{j}}}{d_j^p(t)}\right)^2 +$$ +- `linear_absolute`: +$$ + \min \sum_{(\hat{i},\hat{j})\in \hat{E}_S\;:\; \hat{i}\in \hat{U}_S} \left| F_{\hat{i}\hat{j}} - d_j^p(t)\right| +$$ +- `linear_relative`: $$ - \max \sum_{(\hat{i},\hat{j})\in \hat{E}_S\;:\; \hat{i}\in \hat{U}_S} F_{\hat{i}\hat{j}} -$$ {#eq-objective} -This is a linear combination of the allocations to the users, where all allocations currently get a weight of $1$. + \min \sum_{(\hat{i},\hat{j})\in \hat{E}_S\;:\; \hat{i}\in \hat{U}_S} \left|1 - \frac{F_{\hat{i}\hat{j}}}{d_j^p(t)}\right| +$$ + +To avoid division by $0$ errors, if a `*_relative` objective is used and a demand is $0$, the coefficient of the flow $F_{\hat{i}\hat{j}}$ is set to $0$. + +For `*_absolute` objectives the optimizer cares about the actual amount of water allocated to a user, for `*_relative` objectives it cares about the fraction of the demand allocated to the user. For `quadratic_*` objectives the optimizer cares about avoiding large shortages, for `linear_*` objectives it treats all deviations equally. :::{.callout-note} -This optimization objective will probably be modified in the future, to take things into account like: +These options for objectives for allocation to users have not been tested thoroughly, and might change in the future. +::: + +The absolute value applied here is not supported in a linear programming context directly; this requires introduction of new variables and constraints. For more details see [here](https://optimization.cbe.cornell.edu/index.php?title=Optimization_with_absolute_values). -- Fair distribution in the case of water scarcity; -- An allocation graph-wide preference ordering over sources. +:::{.callout-note} +In the future new optimization objectives will be introduced, for demands of basins and priorities over sources. These will be used in combination with the above, in the form of goal programming. ::: ### The optimization variable constraints @@ -175,7 +195,9 @@ $$ 5. Repeat steps 2-4 for the remaining priorities up to $p_{\max}$. :::{.callout-note} -In the future there will be another solve before the priority 1 solve, taking the demand/supply from basins into account. +In the future there will be 2 more optimization solves: +- One before optimizing for users, taking the demand/supply from basins into account; +- One after optimizing for users, taking preferences over sources into account. ::: ## Example diff --git a/docs/core/index.qmd b/docs/core/index.qmd index 167feb357..8bcfc7383 100644 --- a/docs/core/index.qmd +++ b/docs/core/index.qmd @@ -19,65 +19,21 @@ can be found in the [Ribasim repository](https://github.com/Deltares/Ribasim) un # The simulation loop -`Ribasim.jl` in the above figure can be divided into 3 parts: - -- Model initialization -- Running the simulation loop -- Writing the output files - -The figure below gives a more detailed description of the simulation loop in the form of a [sequence diagram](https://en.wikipedia.org/wiki/Sequence_diagram). From top to bottom, it contains the following blocks: - -- Allocation optimization; activated when the allocation timestep has been passed; -- Control actions; activated when some discrete control callback is triggered; -- Water balance; computing the flows over flow edges happens each timestep; -- Time integration step; Done by the selected integrator from `OrdinaryDiffEq.jl`. +The figure below shows a simple flowchart of the simulation in `Ribasim.jl`. ```{mermaid} -sequenceDiagram - autonumber - participant Int as Process: Integrator - participant Optim as Process: Allocation optimization - participant Param as Data: Parameters - participant State as Data: State - participant Sim as Process: Water balance - loop Simulation loop (OrdinaryDiffEq.jl) - activate Int - %% Allocation - rect rgb(200, 200, 200) - opt Allocation optimization, per allocation network (JuMP.jl, HiGHS) - activate Optim - Int->>Optim: Callback: allocation timestep has passed - Param-->>Optim: Input - State-->>Optim: Input - Optim->>Optim: Optimize Basin allocations if below target level - Optim->>Optim: Optimize User allocation, per priority - Optim-->>Param: Set allocated flow rates - deactivate Optim - end - end - %% Control - rect rgb(200, 200, 200) - opt Control actions - Int->>Int: DiscreteControl callback - Int-->>Param: Parameter updates by control - end - end - %% water_balance! - rect rgb(200, 200, 200) - activate Sim - State-->>Sim: Input - Param-->>Sim: Input - Sim->>Sim: Compute flows over edges per node type - Sim-->>Param: Set flows - deactivate Sim - end - %% Time integration - rect rgb(200, 200, 200) - State-->>Int: Input - Param-->>Int: Input - Int->>Int: Time integration step - Int-->>State: Update state - end - deactivate Int - end +flowchart LR +Start((Start)) +Init[Initialize model] +Con[Conditional: allocation, control] +Sim[Simulate flows over timestep] +Finished{End of simulation period?} +Done((Done)) + +Start --> Init +Init --> Con +Con --> Sim +Sim --> Finished +Finished -->|no| Con +Finished -->|yes| Done ``` diff --git a/docs/schema/Allocation.schema.json b/docs/schema/Allocation.schema.json index d81c4423d..94195d977 100644 --- a/docs/schema/Allocation.schema.json +++ b/docs/schema/Allocation.schema.json @@ -21,9 +21,15 @@ "format": "default", "type": "boolean", "default": false + }, + "objective_type": { + "format": "default", + "type": "string", + "default": "quadratic_relative" } }, "required": [ - "use_allocation" + "use_allocation", + "objective_type" ] } diff --git a/docs/schema/Config.schema.json b/docs/schema/Config.schema.json index 3fccb5fe7..c75a7f8ae 100644 --- a/docs/schema/Config.schema.json +++ b/docs/schema/Config.schema.json @@ -36,7 +36,8 @@ "$ref": "https://deltares.github.io/Ribasim/schema/Allocation.schema.json", "default": { "timestep": null, - "use_allocation": false + "use_allocation": false, + "objective_type": "quadratic_relative" } }, "solver": { diff --git a/python/ribasim/ribasim/config.py b/python/ribasim/ribasim/config.py index 6a4330699..2c8f0c436 100644 --- a/python/ribasim/ribasim/config.py +++ b/python/ribasim/ribasim/config.py @@ -35,6 +35,7 @@ class Allocation(BaseModel): timestep: float | None = None use_allocation: bool = False + objective_type: str = "quadratic_relative" class Compression(str, Enum):