diff --git a/core/src/allocation_init.jl b/core/src/allocation_init.jl index 39cfd100b..858e044fe 100644 --- a/core/src/allocation_init.jl +++ b/core/src/allocation_init.jl @@ -32,7 +32,7 @@ function allocation_graph_used_nodes!(p::Parameters, allocation_network_id::Int) use_node = false has_fractional_flow_outneighbors = get_fractional_flow_connected_basins(node_id, basin, fractional_flow, graph)[3] - if node_id.type in [NodeType.User, NodeType.Basin, NodeType.Terminal] + if node_id.type in [NodeType.UserDemand, NodeType.Basin, NodeType.Terminal] use_node = true elseif has_fractional_flow_outneighbors use_node = true @@ -148,7 +148,7 @@ function find_allocation_graph_edges!( end indicate_allocation_flow!(graph, [node_id, outneighbor_id]) push!(edge_ids, (node_id, outneighbor_id)) - # if subnetwork_outneighbor_id in user.node_id: Capacity depends on user demand at a given priority + # if subnetwork_outneighbor_id in user_demand.node_id: Capacity depends on user demand at a given priority # else: These direct connections cannot have capacity constraints capacity[node_id, outneighbor_id] = Inf end @@ -283,23 +283,24 @@ const allocation_source_nodetypes = Set{NodeType.T}([NodeType.LevelBoundary, NodeType.FlowBoundary]) """ -Remove allocation user return flow edges that are upstream of the user itself. +Remove allocation UserDemand return flow edges that are upstream of the UserDemand itself. """ function avoid_using_own_returnflow!(p::Parameters, allocation_network_id::Int)::Nothing (; graph) = p node_ids = graph[].node_ids[allocation_network_id] edge_ids = graph[].edge_ids[allocation_network_id] - node_ids_user = [node_id for node_id in node_ids if node_id.type == NodeType.User] - - for node_id_user in node_ids_user - node_id_return_flow = only(outflow_ids_allocation(graph, node_id_user)) - if allocation_path_exists_in_graph(graph, node_id_return_flow, node_id_user) - edge_metadata = graph[node_id_user, node_id_return_flow] - graph[node_id_user, node_id_return_flow] = + node_ids_user_demand = + [node_id for node_id in node_ids if node_id.type == NodeType.UserDemand] + + for node_id_user_demand in node_ids_user_demand + node_id_return_flow = only(outflow_ids_allocation(graph, node_id_user_demand)) + if allocation_path_exists_in_graph(graph, node_id_return_flow, node_id_user_demand) + edge_metadata = graph[node_id_user_demand, node_id_return_flow] + graph[node_id_user_demand, node_id_return_flow] = @set edge_metadata.allocation_flow = false empty!(edge_metadata.node_ids) - delete!(edge_ids, (node_id_user, node_id_return_flow)) - @debug "The outflow of user $node_id_user is upstream of the user itself and thus ignored in allocation solves." + delete!(edge_ids, (node_id_user_demand, node_id_return_flow)) + @debug "The outflow of $node_id_user_demand is upstream of the UserDemand itself and thus ignored in allocation solves." end end return nothing @@ -345,7 +346,7 @@ function allocation_graph( error("Errors in sources in allocation network.") end - # Discard user return flow in allocation if this leads to a closed loop of flow + # Discard UserDemand return flow in allocation if this leads to a closed loop of flow avoid_using_own_returnflow!(p, allocation_network_id) return capacity @@ -404,28 +405,29 @@ function add_variables_absolute_value!( (; main_network_connections) = allocation if startswith(config.allocation.objective_type, "linear") node_ids = graph[].node_ids[allocation_network_id] - node_ids_user = NodeID[] + node_ids_user_demand = NodeID[] node_ids_basin = NodeID[] for node_id in node_ids type = node_id.type - if type == NodeType.User - push!(node_ids_user, node_id) + if type == NodeType.UserDemand + push!(node_ids_user_demand, node_id) elseif type == NodeType.Basin push!(node_ids_basin, node_id) end end - # For the main network, connections to subnetworks are treated as users + # For the main network, connections to subnetworks are treated as UserDemands if is_main_network(allocation_network_id) for connections_subnetwork in main_network_connections for connection in connections_subnetwork - push!(node_ids_user, connection[2]) + push!(node_ids_user_demand, connection[2]) end end end - problem[:F_abs_user] = JuMP.@variable(problem, F_abs_user[node_id = node_ids_user]) + problem[:F_abs_user_demand] = + JuMP.@variable(problem, F_abs_user_demand[node_id = node_ids_user_demand]) problem[:F_abs_basin] = JuMP.@variable(problem, F_abs_basin[node_id = node_ids_basin]) end @@ -548,7 +550,7 @@ end """ Add the flow conservation constraints to the allocation problem. -The constraint indices are user node IDs. +The constraint indices are UserDemand node IDs. Constraint: sum(flows out of node node) == flows into node + flow from storage and vertical fluxes @@ -585,31 +587,36 @@ function add_constraints_flow_conservation!( end """ -Add the user returnflow constraints to the allocation problem. -The constraint indices are user node IDs. +Add the UserDemand returnflow constraints to the allocation problem. +The constraint indices are UserDemand node IDs. Constraint: -outflow from user <= return factor * inflow to user +outflow from user_demand <= return factor * inflow to user_demand """ -function add_constraints_user_returnflow!( +function add_constraints_user_demand_returnflow!( problem::JuMP.Model, p::Parameters, allocation_network_id::Int, )::Nothing - (; graph, user) = p + (; graph, user_demand) = p F = problem[:F] node_ids = graph[].node_ids[allocation_network_id] - node_ids_user_with_returnflow = [ - node_id for node_id in node_ids if node_id.type == NodeType.User && + node_ids_user_demand_with_returnflow = [ + node_id for node_id in node_ids if node_id.type == NodeType.UserDemand && !isempty(outflow_ids_allocation(graph, node_id)) ] problem[:return_flow] = JuMP.@constraint( problem, - [node_id_user = node_ids_user_with_returnflow], - F[(node_id_user, only(outflow_ids_allocation(graph, node_id_user)))] <= - user.return_factor[findsorted(user.node_id, node_id_user)] * - F[(only(inflow_ids_allocation(graph, node_id_user)), node_id_user)], + [node_id_user_demand = node_ids_user_demand_with_returnflow], + F[( + node_id_user_demand, + only(outflow_ids_allocation(graph, node_id_user_demand)), + )] <= + user_demand.return_factor[findsorted(user_demand.node_id, node_id_user_demand)] * F[( + only(inflow_ids_allocation(graph, node_id_user_demand)), + node_id_user_demand, + )], base_name = "return_flow", ) return nothing @@ -651,8 +658,8 @@ function add_constraints_absolute_value!( base_name = base_name ) elseif objective_type == "linear_relative" - # These constraints together make sure that F_abs_user acts as the absolute - # value F_abs_user = |x| where x = 1-F/d (here for example d = 2) + # These constraints together make sure that F_abs_user_demand acts as the absolute + # value F_abs_user_demand = |x| where x = 1-F/d (here for example d = 2) base_name = "abs_positive_$variable_type" problem[Symbol(base_name)] = JuMP.@constraint( problem, @@ -672,10 +679,10 @@ function add_constraints_absolute_value!( end """ -Add constraints so that variables F_abs_user act as the -absolute value of the expression comparing flow to an user to its demand. +Add constraints so that variables F_abs_user_demand act as the +absolute value of the expression comparing flow to a UserDemand to its demand. """ -function add_constraints_absolute_value_user!( +function add_constraints_absolute_value_user_demand!( problem::JuMP.Model, p::Parameters, config::Config, @@ -685,19 +692,19 @@ function add_constraints_absolute_value_user!( objective_type = config.allocation.objective_type if startswith(objective_type, "linear") F = problem[:F] - F_abs_user = problem[:F_abs_user] + F_abs_user_demand = problem[:F_abs_user_demand] flow_per_node = Dict( node_id => F[(only(inflow_ids_allocation(graph, node_id)), node_id)] for - node_id in only(F_abs_user.axes) + node_id in only(F_abs_user_demand.axes) ) add_constraints_absolute_value!( problem, flow_per_node, - F_abs_user, + F_abs_user_demand, objective_type, - "user", + "user_demand", ) end return nothing @@ -778,8 +785,8 @@ function add_constraints_fractional_flow!( end """ -Add the basin flow constraints to the allocation problem. -The constraint indices are the basin node IDs. +Add the Basin flow constraints to the allocation problem. +The constraint indices are the Basin node IDs. Constraint: flow out of basin <= basin capacity @@ -816,8 +823,8 @@ function allocation_problem( add_constraints_capacity!(problem, capacity, p, allocation_network_id) add_constraints_source!(problem, p, allocation_network_id) add_constraints_flow_conservation!(problem, p, allocation_network_id) - add_constraints_user_returnflow!(problem, p, allocation_network_id) - add_constraints_absolute_value_user!(problem, p, config) + add_constraints_user_demand_returnflow!(problem, p, allocation_network_id) + add_constraints_absolute_value_user_demand!(problem, p, config) add_constraints_absolute_value_basin!(problem, config) add_constraints_fractional_flow!(problem, p, allocation_network_id) add_constraints_basin_flow!(problem) diff --git a/core/src/allocation_optim.jl b/core/src/allocation_optim.jl index 8943e92fc..9593b68e4 100644 --- a/core/src/allocation_optim.jl +++ b/core/src/allocation_optim.jl @@ -50,9 +50,9 @@ end """ Add a term to the expression of the objective function corresponding to -the demand of a user. +the demand of a UserDemand. """ -function add_user_term!( +function add_user_demand_term!( ex::Union{JuMP.QuadExpr, JuMP.AffExpr}, edge::Tuple{NodeID, NodeID}, objective_type::Symbol, @@ -61,11 +61,11 @@ function add_user_term!( )::Nothing F = problem[:F] F_edge = F[edge] - node_id_user = edge[2] + node_id_user_demand = edge[2] if objective_type in [:linear_absolute, :linear_relative] - constraint_abs_positive = problem[:abs_positive_user][node_id_user] - constraint_abs_negative = problem[:abs_negative_user][node_id_user] + constraint_abs_positive = problem[:abs_positive_user_demand][node_id_user_demand] + constraint_abs_negative = problem[:abs_negative_user_demand][node_id_user_demand] else constraint_abs_positive = nothing constraint_abs_negative = nothing @@ -126,8 +126,8 @@ function set_objective_priority!( priority_idx::Int, )::Nothing (; objective_type, problem, allocation_network_id) = allocation_model - (; graph, user, allocation, basin) = p - (; demand_itp, demand_from_timeseries, node_id) = user + (; graph, user_demand, allocation, basin) = p + (; demand_itp, demand_from_timeseries, node_id) = user_demand (; main_network_connections, subnetwork_demands) = allocation edge_ids = graph[].edge_ids[allocation_network_id] @@ -135,39 +135,39 @@ function set_objective_priority!( ex = JuMP.QuadExpr() elseif objective_type in [:linear_absolute, :linear_relative] ex = JuMP.AffExpr() - ex += sum(problem[:F_abs_user]) + ex += sum(problem[:F_abs_user_demand]) ex += sum(problem[:F_abs_basin]) end demand_max = 0.0 - # Terms for subnetworks as users + # Terms for subnetworks as UserDemand if is_main_network(allocation_network_id) for connections_subnetwork in main_network_connections for connection in connections_subnetwork d = subnetwork_demands[connection][priority_idx] demand_max = max(demand_max, d) - add_user_term!(ex, connection, objective_type, d, problem) + add_user_demand_term!(ex, connection, objective_type, d, problem) end end end - # Terms for user nodes + # Terms for UserDemand nodes for edge_id in edge_ids - node_id_user = edge_id[2] - if node_id_user.type != NodeType.User + node_id_user_demand = edge_id[2] + if node_id_user_demand.type != NodeType.UserDemand continue end - user_idx = findsorted(node_id, node_id_user) - if demand_from_timeseries[user_idx] - d = demand_itp[user_idx][priority_idx](t) - set_user_demand!(p, node_id_user, priority_idx, d) + user_demand_idx = findsorted(node_id, node_id_user_demand) + if demand_from_timeseries[user_demand_idx] + d = demand_itp[user_demand_idx][priority_idx](t) + set_user_demand!(p, node_id_user_demand, priority_idx, d) else - d = get_user_demand(p, node_id_user, priority_idx) + d = get_user_demand(p, node_id_user_demand, priority_idx) end demand_max = max(demand_max, d) - add_user_term!(ex, edge_id, objective_type, d, problem) + add_user_demand_term!(ex, edge_id, objective_type, d, problem) end # Terms for basins @@ -188,7 +188,7 @@ function set_objective_priority!( end """ -Assign the allocations to the users as determined by the solution of the allocation problem. +Assign the allocations to the UserDemand as determined by the solution of the allocation problem. """ function assign_allocations!( allocation_model::AllocationModel, @@ -197,7 +197,7 @@ function assign_allocations!( collect_demands::Bool = false, )::Nothing (; problem, allocation_network_id) = allocation_model - (; graph, user, allocation) = p + (; graph, user_demand, allocation) = p (; subnetwork_demands, subnetwork_allocateds, @@ -217,12 +217,12 @@ function assign_allocations!( subnetwork_demands[edge_id][priority_idx] += allocated end - user_node_id = edge_id[2] + user_demand_node_id = edge_id[2] - if user_node_id.type == NodeType.User + if user_demand_node_id.type == NodeType.UserDemand allocated = JuMP.value(F[edge_id]) - user_idx = findsorted(user.node_id, user_node_id) - user.allocated[user_idx][priority_idx] = allocated + user_demand_idx = findsorted(user_demand.node_id, user_demand_node_id) + user_demand.allocated[user_demand_idx][priority_idx] = allocated end end @@ -459,7 +459,7 @@ function adjust_basin_capacities!( end """ -Save the demands and allocated flows for users and basins. +Save the demands and allocated flows for UserDemand and Basin. Note: Basin supply (negative demand) is only saved for the first priority. """ function save_demands_and_allocations!( @@ -468,7 +468,7 @@ function save_demands_and_allocations!( t::Float64, priority_idx::Int, )::Nothing - (; graph, allocation, user, basin) = p + (; graph, allocation, user_demand, basin) = p (; record_demand, priorities) = allocation (; allocation_network_id, problem) = allocation_model node_ids = graph[].node_ids[allocation_network_id] @@ -479,11 +479,11 @@ function save_demands_and_allocations!( for node_id in node_ids has_demand = false - if node_id.type == NodeType.User + if node_id.type == NodeType.UserDemand has_demand = true - user_idx = findsorted(user.node_id, node_id) - demand = user.demand[user_idx] - allocated = user.allocated[user_idx][priority_idx] + user_demand_idx = findsorted(user_demand.node_id, node_id) + demand = user_demand.demand[user_demand_idx] + allocated = user_demand.allocated[user_demand_idx][priority_idx] realized = get_flow(graph, inflow_id(graph, node_id), node_id, 0) elseif node_id.type == NodeType.Basin @@ -586,7 +586,7 @@ end """ Update the allocation optimization problem for the given subnetwork with the problem state -and flows, solve the allocation problem and assign the results to the users. +and flows, solve the allocation problem and assign the results to the UserDemand. """ function allocate!( p::Parameters, @@ -636,7 +636,7 @@ function allocate!( ) end - # Assign the allocations to the users for this priority + # Assign the allocations to the UserDemand for this priority assign_allocations!(allocation_model, p, priority_idx; collect_demands) # Save the demands and allocated flows for all nodes that have these diff --git a/core/src/callback.jl b/core/src/callback.jl index e5d8793a7..9403416aa 100644 --- a/core/src/callback.jl +++ b/core/src/callback.jl @@ -424,7 +424,7 @@ function update_basin(integrator)::Nothing return nothing end -"Solve the allocation problem for all users and assign allocated abstractions to user nodes." +"Solve the allocation problem for all demands and assign allocated abstractions." function update_allocation!(integrator)::Nothing (; p, t, u) = integrator (; allocation) = p diff --git a/core/src/model.jl b/core/src/model.jl index ba166c516..a7899bfd6 100644 --- a/core/src/model.jl +++ b/core/src/model.jl @@ -77,9 +77,9 @@ function Model(config::Config)::Model # TODO add all time tables here time_flow_boundary = load_structvector(db, config, FlowBoundaryTimeV1) tstops_flow_boundary = get_tstops(time_flow_boundary.time, config.starttime) - time_user = load_structvector(db, config, UserTimeV1) - tstops_user = get_tstops(time_user.time, config.starttime) - tstops = sort(unique(vcat(tstops_flow_boundary, tstops_user))) + time_user_demand = load_structvector(db, config, UserDemandTimeV1) + tstops_user_demand = get_tstops(time_user_demand.time, config.starttime) + tstops = sort(unique(vcat(tstops_flow_boundary, tstops_user_demand))) # use state state = load_structvector(db, config, BasinStateV1) diff --git a/core/src/parameter.jl b/core/src/parameter.jl index 7d2fb566e..fe36b0070 100644 --- a/core/src/parameter.jl +++ b/core/src/parameter.jl @@ -477,15 +477,15 @@ struct PidControl{T} <: AbstractParameterNode end """ -demand: water flux demand of user per priority over time. - Each user has a demand for all priorities, +demand: water flux demand of UserDemand per priority over time. + Each UserDemand has a demand for all priorities, which is 0.0 if it is not provided explicitly. active: whether this node is active and thus demands water -allocated: water flux currently allocated to user per priority +allocated: water flux currently allocated to UserDemand per priority return_factor: the factor in [0,1] of how much of the abstracted water is given back to the system -min_level: The level of the source basin below which the user does not abstract +min_level: The level of the source basin below which the UserDemand does not abstract """ -struct User <: AbstractParameterNode +struct UserDemand <: AbstractParameterNode node_id::Vector{NodeID} active::BitVector demand::Vector{Float64} @@ -495,7 +495,7 @@ struct User <: AbstractParameterNode return_factor::Vector{Float64} min_level::Vector{Float64} - function User( + function UserDemand( node_id, active, demand, @@ -577,7 +577,7 @@ struct Parameters{T, C1, C2} terminal::Terminal discrete_control::DiscreteControl pid_control::PidControl{T} - user::User + user_demand::UserDemand level_demand::LevelDemand subgrid::Subgrid end diff --git a/core/src/read.jl b/core/src/read.jl index bafdeb1a3..71abd2f38 100644 --- a/core/src/read.jl +++ b/core/src/read.jl @@ -198,7 +198,7 @@ function static_and_time_node_ids( end const nonconservative_nodetypes = - Set{String}(["Basin", "LevelBoundary", "FlowBoundary", "Terminal", "User"]) + Set{String}(["Basin", "LevelBoundary", "FlowBoundary", "Terminal", "UserDemand"]) function initialize_allocation!(p::Parameters, config::Config)::Nothing (; graph, allocation) = p @@ -640,17 +640,17 @@ function PidControl(db::DB, config::Config, chunk_sizes::Vector{Int})::PidContro ) end -function User(db::DB, config::Config)::User - static = load_structvector(db, config, UserStaticV1) - time = load_structvector(db, config, UserTimeV1) +function UserDemand(db::DB, config::Config)::UserDemand + static = load_structvector(db, config, UserDemandStaticV1) + time = load_structvector(db, config, UserDemandTimeV1) static_node_ids, time_node_ids, node_ids, _, valid = - static_and_time_node_ids(db, static, time, "User") + static_and_time_node_ids(db, static, time, "UserDemand") - time_node_id_vec = NodeID.(NodeType.User, time.node_id) + time_node_id_vec = NodeID.(NodeType.UserDemand, time.node_id) if !valid - error("Problems encountered when parsing User static and time node IDs.") + error("Problems encountered when parsing UserDemand static and time node IDs.") end # All priorities used in the model @@ -666,14 +666,14 @@ function User(db::DB, config::Config)::User t_end = seconds_since(config.endtime, config.starttime) # Create a dictionary priority => time data for that priority - time_priority_dict::Dict{Int, StructVector{UserTimeV1}} = Dict( + time_priority_dict::Dict{Int, StructVector{UserDemandTimeV1}} = Dict( first(group).priority => StructVector(group) for group in IterTools.groupby(row -> row.priority, time) ) demand = Float64[] - # Whether the demand of a user node is given by a timeseries + # Whether the demand of a UserDemand node is given by a timeseries demand_from_timeseries = BitVector() for node_id in node_ids @@ -682,7 +682,7 @@ function User(db::DB, config::Config)::User if node_id in static_node_ids push!(demand_from_timeseries, false) - rows = searchsorted(NodeID.(NodeType.User, static.node_id), node_id) + rows = searchsorted(NodeID.(NodeType.UserDemand, static.node_id), node_id) static_id = view(static, rows) for p in priorities idx = findsorted(static_id.priority, p) @@ -711,7 +711,7 @@ function User(db::DB, config::Config)::User if is_valid push!(demand_itp_node_id, demand_p_itp) else - @error "The demand(t) relationship for User #$node_id of priority $p from the time table has repeated timestamps, this can not be interpolated." + @error "The demand(t) relationship for UserDemand #$node_id of priority $p from the time table has repeated timestamps, this can not be interpolated." errors = true end else @@ -725,7 +725,7 @@ function User(db::DB, config::Config)::User first_row = time[first_row_idx] is_active = true else - @error "User node #$node_id data not in any table." + @error "UserDemand node #$node_id data not in any table." errors = true end @@ -739,12 +739,12 @@ function User(db::DB, config::Config)::User end if errors - error("Errors occurred when parsing User data.") + error("Errors occurred when parsing UserDemand data.") end allocated = [fill(Inf, length(priorities)) for id in node_ids] - return User( + return UserDemand( node_ids, active, demand, @@ -888,7 +888,7 @@ function Parameters(db::DB, config::Config)::Parameters terminal = Terminal(db, config) discrete_control = DiscreteControl(db, config) pid_control = PidControl(db, config, chunk_sizes) - user = User(db, config) + user_demand = UserDemand(db, config) level_demand = LevelDemand(db, config) basin = Basin(db, config, chunk_sizes) @@ -910,7 +910,7 @@ function Parameters(db::DB, config::Config)::Parameters terminal, discrete_control, pid_control, - user, + user_demand, level_demand, subgrid_level, ) diff --git a/core/src/schema.jl b/core/src/schema.jl index c7da2c14c..c6a6a8e6f 100644 --- a/core/src/schema.jl +++ b/core/src/schema.jl @@ -21,8 +21,8 @@ @schema "ribasim.tabulatedratingcurve.static" TabulatedRatingCurveStatic @schema "ribasim.tabulatedratingcurve.time" TabulatedRatingCurveTime @schema "ribasim.outlet.static" OutletStatic -@schema "ribasim.user.static" UserStatic -@schema "ribasim.user.time" UserTime +@schema "ribasim.userdemand.static" UserDemandStatic +@schema "ribasim.userdemand.time" UserDemandTime @schema "ribasim.leveldemand.static" LevelDemandStatic @schema "ribasim.leveldemand.time" LevelDemandTime @@ -220,7 +220,7 @@ end control_state::Union{Missing, String} end -@version UserStaticV1 begin +@version UserDemandStaticV1 begin node_id::Int active::Union{Missing, Bool} demand::Float64 @@ -229,7 +229,7 @@ end priority::Int end -@version UserTimeV1 begin +@version UserDemandTimeV1 begin node_id::Int time::DateTime demand::Float64 diff --git a/core/src/solve.jl b/core/src/solve.jl index e1efeb497..01fee6c6c 100644 --- a/core/src/solve.jl +++ b/core/src/solve.jl @@ -258,13 +258,13 @@ function continuous_control!( end function formulate_flow!( - user::User, + user_demand::UserDemand, p::Parameters, storage::AbstractVector, t::Number, )::Nothing (; graph, basin) = p - (; node_id, allocated, active, demand_itp, return_factor, min_level) = user + (; node_id, allocated, active, demand_itp, return_factor, min_level) = user_demand for (i, id) in enumerate(node_id) src_id = inflow_id(graph, id) @@ -647,7 +647,7 @@ function formulate_flows!(p::Parameters, storage::AbstractVector, t::Number)::No level_boundary, pump, outlet, - user, + user_demand, fractional_flow, terminal, ) = p @@ -658,7 +658,7 @@ function formulate_flows!(p::Parameters, storage::AbstractVector, t::Number)::No formulate_flow!(flow_boundary, p, storage, t) formulate_flow!(pump, p, storage, t) formulate_flow!(outlet, p, storage, t) - formulate_flow!(user, p, storage, t) + formulate_flow!(user_demand, p, storage, t) # do these last since they rely on formulated input flows formulate_flow!(fractional_flow, p, storage, t) diff --git a/core/src/sparsity.jl b/core/src/sparsity.jl index 171c6dc1f..a1be7b2e6 100644 --- a/core/src/sparsity.jl +++ b/core/src/sparsity.jl @@ -96,7 +96,7 @@ Upstream basins always depend on themselves. function update_jac_prototype!( jac_prototype::SparseMatrixCSC{Float64, Int64}, p::Parameters, - node::Union{Pump, Outlet, TabulatedRatingCurve, User}, + node::Union{Pump, Outlet, TabulatedRatingCurve, UserDemand}, )::Nothing (; basin, fractional_flow, graph) = p diff --git a/core/src/util.jl b/core/src/util.jl index 10dce7904..fd0a93f0e 100644 --- a/core/src/util.jl +++ b/core/src/util.jl @@ -621,11 +621,11 @@ function is_main_network(allocation_network_id::Int)::Bool end function get_user_demand(p::Parameters, node_id::NodeID, priority_idx::Int)::Float64 - (; user, allocation) = p - (; demand) = user - user_idx = findsorted(user.node_id, node_id) + (; user_demand, allocation) = p + (; demand) = user_demand + user_demand_idx = findsorted(user_demand.node_id, node_id) n_priorities = length(allocation.priorities) - return demand[(user_idx - 1) * n_priorities + priority_idx] + return demand[(user_demand_idx - 1) * n_priorities + priority_idx] end function set_user_demand!( @@ -634,11 +634,11 @@ function set_user_demand!( priority_idx::Int, value::Float64, )::Nothing - (; user, allocation) = p - (; demand) = user - user_idx = findsorted(user.node_id, node_id) + (; user_demand, allocation) = p + (; demand) = user_demand + user_demand_idx = findsorted(user_demand.node_id, node_id) n_priorities = length(allocation.priorities) - demand[(user_idx - 1) * n_priorities + priority_idx] = value + demand[(user_demand_idx - 1) * n_priorities + priority_idx] = value return nothing end @@ -646,7 +646,8 @@ function get_all_priorities(db::DB, config::Config)::Vector{Int} priorities = Set{Int}() # TODO: Is there a way to automatically grab all tables with a priority column? - for type in [UserStaticV1, UserTimeV1, LevelDemandStaticV1, LevelDemandTimeV1] + for type in + [UserDemandStaticV1, UserDemandTimeV1, LevelDemandStaticV1, LevelDemandTimeV1] union!(priorities, load_structvector(db, config, type).priority) end return sort(unique(priorities)) diff --git a/core/src/validation.jl b/core/src/validation.jl index 31d89c158..66427e6bb 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -2,7 +2,8 @@ neighbortypes(nodetype::Symbol) = neighbortypes(Val(nodetype)) neighbortypes(::Val{:pump}) = Set((:basin, :fractional_flow, :terminal, :level_boundary)) neighbortypes(::Val{:outlet}) = Set((:basin, :fractional_flow, :terminal, :level_boundary)) -neighbortypes(::Val{:user}) = Set((:basin, :fractional_flow, :terminal, :level_boundary)) +neighbortypes(::Val{:user_demand}) = + Set((:basin, :fractional_flow, :terminal, :level_boundary)) neighbortypes(::Val{:level_demand}) = Set((:basin,)) neighbortypes(::Val{:basin}) = Set(( :linear_resistance, @@ -10,7 +11,7 @@ neighbortypes(::Val{:basin}) = Set(( :manning_resistance, :pump, :outlet, - :user, + :user_demand, )) neighbortypes(::Val{:terminal}) = Set{Symbol}() # only endnode neighbortypes(::Val{:fractional_flow}) = Set((:basin, :terminal, :level_boundary)) @@ -57,7 +58,7 @@ n_neighbor_bounds_flow(::Val{:Outlet}) = n_neighbor_bounds(1, 1, 1, typemax(Int) n_neighbor_bounds_flow(::Val{:Terminal}) = n_neighbor_bounds(1, typemax(Int), 0, 0) n_neighbor_bounds_flow(::Val{:PidControl}) = n_neighbor_bounds(0, 0, 0, 0) n_neighbor_bounds_flow(::Val{:DiscreteControl}) = n_neighbor_bounds(0, 0, 0, 0) -n_neighbor_bounds_flow(::Val{:User}) = n_neighbor_bounds(1, 1, 1, 1) +n_neighbor_bounds_flow(::Val{:UserDemand}) = n_neighbor_bounds(1, 1, 1, 1) n_neighbor_bounds_flow(::Val{:LevelDemand}) = n_neighbor_bounds(0, 0, 0, 0) n_neighbor_bounds_flow(nodetype) = error("'n_neighbor_bounds_flow' not defined for $nodetype.") @@ -76,7 +77,7 @@ n_neighbor_bounds_control(::Val{:Terminal}) = n_neighbor_bounds(0, 0, 0, 0) n_neighbor_bounds_control(::Val{:PidControl}) = n_neighbor_bounds(0, 1, 1, 1) n_neighbor_bounds_control(::Val{:DiscreteControl}) = n_neighbor_bounds(0, 0, 1, typemax(Int)) -n_neighbor_bounds_control(::Val{:User}) = n_neighbor_bounds(0, 0, 0, 0) +n_neighbor_bounds_control(::Val{:UserDemand}) = n_neighbor_bounds(0, 0, 0, 0) n_neighbor_bounds_control(::Val{:LevelDemand}) = n_neighbor_bounds(0, 0, 1, typemax(Int)) n_neighbor_bounds_control(nodetype) = error("'n_neighbor_bounds_control' not defined for $nodetype.") @@ -103,8 +104,8 @@ sort_by_subgrid_level(row) = (row.subgrid_id, row.basin_level) sort_by_function(table::StructVector{<:Legolas.AbstractRecord}) = sort_by_id sort_by_function(table::StructVector{TabulatedRatingCurveStaticV1}) = sort_by_id_state_level sort_by_function(table::StructVector{BasinProfileV1}) = sort_by_id_level -sort_by_function(table::StructVector{UserStaticV1}) = sort_by_priority -sort_by_function(table::StructVector{UserTimeV1}) = sort_by_priority_time +sort_by_function(table::StructVector{UserDemandStaticV1}) = sort_by_priority +sort_by_function(table::StructVector{UserDemandTimeV1}) = sort_by_priority_time sort_by_function(table::StructVector{BasinSubgridV1}) = sort_by_subgrid_level const TimeSchemas = Union{ @@ -113,7 +114,7 @@ const TimeSchemas = Union{ LevelBoundaryTimeV1, PidControlTimeV1, TabulatedRatingCurveTimeV1, - UserTimeV1, + UserDemandTimeV1, } function sort_by_function(table::StructVector{<:TimeSchemas}) diff --git a/core/test/allocation_test.jl b/core/test/allocation_test.jl index 8f5777b4b..1c44a7eb4 100644 --- a/core/test/allocation_test.jl +++ b/core/test/allocation_test.jl @@ -31,22 +31,22 @@ F = allocation_model.problem[:F] @test JuMP.value(F[(NodeID(:Basin, 2), NodeID(:Basin, 6))]) ≈ 0.0 - @test JuMP.value(F[(NodeID(:Basin, 2), NodeID(:User, 10))]) ≈ 0.5 - @test JuMP.value(F[(NodeID(:Basin, 8), NodeID(:User, 12))]) ≈ 0.0 + @test JuMP.value(F[(NodeID(:Basin, 2), NodeID(:UserDemand, 10))]) ≈ 0.5 + @test JuMP.value(F[(NodeID(:Basin, 8), NodeID(:UserDemand, 12))]) ≈ 0.0 @test JuMP.value(F[(NodeID(:Basin, 6), NodeID(:Basin, 8))]) ≈ 0.0 @test JuMP.value(F[(NodeID(:FlowBoundary, 1), NodeID(:Basin, 2))]) ≈ 0.5 - @test JuMP.value(F[(NodeID(:Basin, 6), NodeID(:User, 11))]) ≈ 0.0 + @test JuMP.value(F[(NodeID(:Basin, 6), NodeID(:UserDemand, 11))]) ≈ 0.0 - allocated = p.user.allocated + allocated = p.user_demand.allocated @test allocated[1] ≈ [0.0, 0.5] @test allocated[2] ≈ [4.0, 0.0] @test allocated[3] ≈ [0.0, 0.0] - # Test getting and setting user demands - (; user) = p - Ribasim.set_user_demand!(p, NodeID(:User, 11), 2, Float64(π)) - @test user.demand[4] ≈ π - @test Ribasim.get_user_demand(p, NodeID(:User, 11), 2) ≈ π + # Test getting and setting UserDemand demands + (; user_demand) = p + Ribasim.set_user_demand!(p, NodeID(:UserDemand, 11), 2, Float64(π)) + @test user_demand.demand[4] ≈ π + @test Ribasim.get_user_demand(p, NodeID(:UserDemand, 11), 2) ≈ π end @testitem "Allocation objective: quadratic absolute" skip = true begin @@ -67,12 +67,12 @@ end @test objective isa JuMP.QuadExpr # Quadratic expression F = problem[:F] @test JuMP.UnorderedPair{JuMP.VariableRef}( - F[(NodeID(:Basin, 4), NodeID(:User, 5))], - F[(NodeID(:Basin, 4), NodeID(:User, 5))], + F[(NodeID(:Basin, 4), NodeID(:UserDemand, 5))], + F[(NodeID(:Basin, 4), NodeID(:UserDemand, 5))], ) in keys(objective.terms) # F[4,5]^2 term @test JuMP.UnorderedPair{JuMP.VariableRef}( - F[(NodeID(:Basin, 4), NodeID(:User, 6))], - F[(NodeID(:Basin, 4), NodeID(:User, 6))], + F[(NodeID(:Basin, 4), NodeID(:UserDemand, 6))], + F[(NodeID(:Basin, 4), NodeID(:UserDemand, 6))], ) in keys(objective.terms) # F[4,6]^2 term end @@ -95,12 +95,12 @@ end @test objective.aff.constant == 2.0 F = problem[:F] @test JuMP.UnorderedPair{JuMP.VariableRef}( - F[(NodeID(:Basin, 4), NodeID(:User, 5))], - F[(NodeID(:Basin, 4), NodeID(:User, 5))], + F[(NodeID(:Basin, 4), NodeID(:UserDemand, 5))], + F[(NodeID(:Basin, 4), NodeID(:UserDemand, 5))], ) in keys(objective.terms) # F[4,5]^2 term @test JuMP.UnorderedPair{JuMP.VariableRef}( - F[(NodeID(:Basin, 4), NodeID(:User, 6))], - F[(NodeID(:Basin, 4), NodeID(:User, 6))], + F[(NodeID(:Basin, 4), NodeID(:UserDemand, 6))], + F[(NodeID(:Basin, 4), NodeID(:UserDemand, 6))], ) in keys(objective.terms) # F[4,6]^2 term end @@ -120,12 +120,12 @@ end problem = model.integrator.p.allocation.allocation_models[1].problem objective = JuMP.objective_function(problem) @test objective isa JuMP.AffExpr # Affine expression - @test :F_abs_user in keys(problem.obj_dict) + @test :F_abs_user_demand in keys(problem.obj_dict) F = problem[:F] - F_abs_user = problem[:F_abs_user] + F_abs_user_demand = problem[:F_abs_user_demand] - @test objective.terms[F_abs_user[NodeID(:User, 5)]] == 1.0 - @test objective.terms[F_abs_user[NodeID(:User, 6)]] == 1.0 + @test objective.terms[F_abs_user_demand[NodeID(:UserDemand, 5)]] == 1.0 + @test objective.terms[F_abs_user_demand[NodeID(:UserDemand, 6)]] == 1.0 end @testitem "Allocation objective: linear relative" begin @@ -144,12 +144,12 @@ end problem = model.integrator.p.allocation.allocation_models[1].problem objective = JuMP.objective_function(problem) @test objective isa JuMP.AffExpr # Affine expression - @test :F_abs_user in keys(problem.obj_dict) + @test :F_abs_user_demand in keys(problem.obj_dict) F = problem[:F] - F_abs_user = problem[:F_abs_user] + F_abs_user_demand = problem[:F_abs_user_demand] - @test objective.terms[F_abs_user[NodeID(:User, 5)]] == 1.0 - @test objective.terms[F_abs_user[NodeID(:User, 6)]] == 1.0 + @test objective.terms[F_abs_user_demand[NodeID(:UserDemand, 5)]] == 1.0 + @test objective.terms[F_abs_user_demand[NodeID(:UserDemand, 6)]] == 1.0 end @testitem "Allocation with controlled fractional flow" begin @@ -183,13 +183,25 @@ end t_control = record_control.time[2] allocated_6_before = - groups[("User", 6, 1)][groups[("User", 6, 1)].time .< t_control, :].allocated + groups[("UserDemand", 6, 1)][ + groups[("UserDemand", 6, 1)].time .< t_control, + :, + ].allocated allocated_9_before = - groups[("User", 9, 1)][groups[("User", 9, 1)].time .< t_control, :].allocated + groups[("UserDemand", 9, 1)][ + groups[("UserDemand", 9, 1)].time .< t_control, + :, + ].allocated allocated_6_after = - groups[("User", 6, 1)][groups[("User", 6, 1)].time .> t_control, :].allocated + groups[("UserDemand", 6, 1)][ + groups[("UserDemand", 6, 1)].time .> t_control, + :, + ].allocated allocated_9_after = - groups[("User", 9, 1)][groups[("User", 9, 1)].time .> t_control, :].allocated + groups[("UserDemand", 9, 1)][ + groups[("UserDemand", 9, 1)].time .> t_control, + :, + ].allocated @test all( allocated_9_before ./ allocated_6_before .<= control_mapping[(NodeID(:FractionalFlow, 7), "A")].fraction / @@ -245,13 +257,13 @@ end (NodeID(:Basin, 10), NodeID(:Pump, 38)), ] ⊆ allocation_edges_main_network - # Subnetworks interpreted as users require variables and constraints to + # Subnetworks interpreted as user_demands require variables and constraints to # support absolute value expressions in the objective function allocation_model_main_network = Ribasim.get_allocation_model(p, 1) problem = allocation_model_main_network.problem - @test problem[:F_abs_user].axes[1] == NodeID.(:Pump, [11, 24, 38]) - @test problem[:abs_positive_user].axes[1] == NodeID.(:Pump, [11, 24, 38]) - @test problem[:abs_negative_user].axes[1] == NodeID.(:Pump, [11, 24, 38]) + @test problem[:F_abs_user_demand].axes[1] == NodeID.(:Pump, [11, 24, 38]) + @test problem[:abs_positive_user_demand].axes[1] == NodeID.(:Pump, [11, 24, 38]) + @test problem[:abs_negative_user_demand].axes[1] == NodeID.(:Pump, [11, 24, 38]) # In each subnetwork, the connection from the main network to the subnetwork is # interpreted as a source @@ -280,7 +292,7 @@ end p = Ribasim.Parameters(db, cfg) close(db) - (; allocation, user, graph, basin) = p + (; allocation, user_demand, graph, basin) = p (; allocation_models, subnetwork_demands, subnetwork_allocateds) = allocation t = 0.0 @@ -295,8 +307,7 @@ end @test subnetwork_demands[(NodeID(:Basin, 10), NodeID(:Pump, 38))] ≈ [0.001, 0.002, 0.002] - # Solving for the main network, - # containing subnetworks as users + # Solving for the main network, containing subnetworks as UserDemands allocation_model = allocation_models[1] (; problem) = allocation_model Ribasim.allocate!(p, allocation_model, t, u) @@ -304,10 +315,10 @@ end # Main network objective function objective = JuMP.objective_function(problem) objective_variables = keys(objective.terms) - F_abs_user = problem[:F_abs_user] - @test F_abs_user[NodeID(:Pump, 11)] ∈ objective_variables - @test F_abs_user[NodeID(:Pump, 24)] ∈ objective_variables - @test F_abs_user[NodeID(:Pump, 38)] ∈ objective_variables + F_abs_user_demand = problem[:F_abs_user_demand] + @test F_abs_user_demand[NodeID(:Pump, 11)] ∈ objective_variables + @test F_abs_user_demand[NodeID(:Pump, 24)] ∈ objective_variables + @test F_abs_user_demand[NodeID(:Pump, 38)] ∈ objective_variables # Running full allocation algorithm Ribasim.set_flow!(graph, NodeID(:FlowBoundary, 1), NodeID(:Basin, 2), 4.5) @@ -320,8 +331,8 @@ end [0.00399999999, 0.0, 0.0] @test subnetwork_allocateds[NodeID(:Basin, 10), NodeID(:Pump, 38)] ≈ [0.001, 0.0, 0.0] - @test user.allocated[2] ≈ [4.0, 0.0, 0.0] - @test user.allocated[7] ≈ [0.001, 0.0, 0.0] + @test user_demand.allocated[2] ≈ [4.0, 0.0, 0.0] + @test user_demand.allocated[7] ≈ [0.001, 0.0, 0.0] end @testitem "Allocation level control" begin @@ -333,9 +344,9 @@ end t = Ribasim.timesteps(model) p = model.integrator.p - (; user, graph, allocation, basin, level_demand) = p + (; user_demand, graph, allocation, basin, level_demand) = p - d = user.demand_itp[1][2](0) + d = user_demand.demand_itp[1][2](0) ϕ = 1e-3 # precipitation q = Ribasim.get_flow( graph, @@ -347,12 +358,12 @@ end l_max = level_demand.max_level[1](0) Δt_allocation = allocation.allocation_models[1].Δt_allocation - # Until the first allocation solve, the user abstracts fully + # Until the first allocation solve, the UserDemand abstracts fully stage_1 = t .<= Δt_allocation u_stage_1(τ) = storage[1] + (q + ϕ - d) * τ @test storage[stage_1] ≈ u_stage_1.(t[stage_1]) rtol = 1e-4 - # In this section the basin leaves no supply for the user + # In this section the Basin leaves no supply for the UserDemand stage_2 = Δt_allocation .<= t .<= 3 * Δt_allocation stage_2_start_idx = findfirst(stage_2) u_stage_2(τ) = storage[stage_2_start_idx] + (q + ϕ) * (τ - t[stage_2_start_idx]) @@ -360,7 +371,7 @@ end # In this section (and following sections) the basin has no longer a (positive) demand, # since precipitation provides enough water to get the basin to its target level - # The FlowBoundary flow gets fully allocated to the user + # The FlowBoundary flow gets fully allocated to the UserDemand stage_3 = 3 * Δt_allocation .<= t .<= 8 * Δt_allocation stage_3_start_idx = findfirst(stage_3) u_stage_3(τ) = storage[stage_3_start_idx] + ϕ * (τ - t[stage_3_start_idx]) @@ -375,7 +386,7 @@ end u_stage_4(τ) = storage[stage_4_start_idx] + (q + ϕ - d) * (τ - t[stage_4_start_idx]) @test storage[stage_4] ≈ u_stage_4.(t[stage_4]) rtol = 1e-4 - # At the start of this section precipitation stops, and so the user + # At the start of this section precipitation stops, and so the UserDemand # partly uses surplus water from the basin to fulfill its demand stage_5 = 13 * Δt_allocation .<= t .<= 16 * Δt_allocation stage_5_start_idx = findfirst(stage_5) @@ -383,7 +394,7 @@ end @test storage[stage_5] ≈ u_stage_5.(t[stage_5]) rtol = 1e-4 # From this point the basin is in a dynamical equilibrium, - # since the basin has no supply so the user abstracts precisely + # since the basin has no supply so the UserDemand abstracts precisely # the flow from the level boundary stage_6 = 17 * Δt_allocation .<= t stage_6_start_idx = findfirst(stage_6) diff --git a/core/test/run_models_test.jl b/core/test/run_models_test.jl index 051a27d75..a5f615c65 100644 --- a/core/test/run_models_test.jl +++ b/core/test/run_models_test.jl @@ -374,19 +374,19 @@ end all(isapprox.(level_basin[timesteps .>= t_maximum_level], level.u[3], atol = 5e-2)) end -@testitem "User" begin +@testitem "UserDemand" begin using SciMLBase: successful_retcode - toml_path = normpath(@__DIR__, "../../generated_testmodels/user/ribasim.toml") + toml_path = normpath(@__DIR__, "../../generated_testmodels/user_demand/ribasim.toml") @test ispath(toml_path) model = Ribasim.run(toml_path) @test successful_retcode(model) day = 86400.0 @test only(model.integrator.sol(0day)) == 1000.0 - # constant user withdraws to 0.9m/900m3 + # constant UserDemand withdraws to 0.9m/900m3 @test only(model.integrator.sol(150day)) ≈ 900 atol = 5 - # dynamic user withdraws to 0.5m/509m3 + # dynamic UserDemand withdraws to 0.5m/509m3 @test only(model.integrator.sol(180day)) ≈ 509 atol = 1 end diff --git a/core/test/validation_test.jl b/core/test/validation_test.jl index aec09f671..f7231af8a 100644 --- a/core/test/validation_test.jl +++ b/core/test/validation_test.jl @@ -411,8 +411,8 @@ end logger = TestLogger() with_logger(logger) do - @test_throws "Invalid demand" Ribasim.User( - [NodeID(:User, 1)], + @test_throws "Invalid demand" Ribasim.UserDemand( + [NodeID(:UserDemand, 1)], [true], [0.0], [[LinearInterpolation([-5.0, -5.0], [-1.8, 1.8])]], @@ -427,5 +427,5 @@ end @test length(logger.logs) == 1 @test logger.logs[1].level == Error @test logger.logs[1].message == - "Demand of User #1 with priority 1 should be non-negative" + "Demand of UserDemand #1 with priority 1 should be non-negative" end diff --git a/docs/_quarto.yml b/docs/_quarto.yml index db0e2c94c..01f4a9ea1 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -80,7 +80,7 @@ quartodoc: - TabulatedRatingCurve - Pump - Outlet - - User + - UserDemand - LevelBoundary - FlowBoundary - LinearResistance diff --git a/docs/core/allocation.qmd b/docs/core/allocation.qmd index b9d821348..73be116e9 100644 --- a/docs/core/allocation.qmd +++ b/docs/core/allocation.qmd @@ -3,7 +3,7 @@ title: "Allocation" --- # Introduction -Allocation is the process of assigning an allocated abstraction flow rate to user nodes in the physical layer of the model based on information about sources, user demands over various priorities, constraints introduced by nodes, local water availability and graph topology. The allocation procedure implemented in Ribasim is heavily inspired by the [maximum flow problem](https://en.wikipedia.org/wiki/Maximum_flow_problem). +Allocation is the process of assigning an allocated abstraction flow rate to demand nodes in the physical layer of the model based on information about sources, the different demand nodes over various priorities, constraints introduced by nodes, local water availability and graph topology. The allocation procedure implemented in Ribasim is heavily inspired by the [maximum flow problem](https://en.wikipedia.org/wiki/Maximum_flow_problem). The allocation problem is solved per subnetwork of the Ribasim model. The subnetwork is used to formulate an optimization problem with the [JuMP](https://jump.dev/JuMP.jl/stable/) package, which is solved using the [HiGHS solver](https://highs.dev/). For more in-depth information see also the example of solving the maximum flow problem with `JuMP.jl` [here](https://jump.dev/JuMP.jl/stable/tutorials/linear/network_flows/#The-max-flow-problem). @@ -11,8 +11,8 @@ The allocation problem is solved per subnetwork of the Ribasim model. The subnet The allocation algorithm consists of 3 types of optimization problems: 1. **Subnetwork demand collection**: Collect the demands of a subnetwork from the main network by optimizing with unlimited capacity from the main network; -2. **Main network allocation**: Allocate to subnetworks with the above collected demand, and users in the main network; -3. **Subnetwork allocation**: Allocate to users in the subnetworks with the flows allocated to the subnetwork in the main network allocation. +2. **Main network allocation**: Allocate to subnetworks with the above collected demand, and demands in the main network; +3. **Subnetwork allocation**: Allocate to demands in the subnetworks with the flows allocated to the subnetwork in the main network allocation. The total allocation algorithm consists of performing 1 for all subnetworks, then performing 2, then performing 3 for all subnetworks. Not having a main network is also supported, then 1 and 2 are skipped. @@ -36,7 +36,7 @@ That is, if $(i,j) \in E_S^\text{source}$, then $Q_{ij}$ (see the [formal model ### User demands -The subnetwork contains a subset of user nodes $U_S \subset S$, who all have time varying demands over various priorities $p$: +The subnetwork contains a subset of UserDemand nodes $U_S \subset S$, who all have time varying demands over various priorities $p$: $$ d^p_i(t), \quad i \in U_S, p = 1,2,\ldots, p_{\max}. $$ @@ -73,8 +73,8 @@ for all $i \in B_S$. Here $\Delta t_{\text{alloc}}$ is the simulated time betwee #### Flow magnitude and direction constraints Nodes in the Ribasim model that have a `max_flow_rate`, i.e. pumps and outlets, put a constraint on the flow through that node. Some nodes only allow flow in one direction, like pumps, outlets and tabulated rating curves. -#### Fractional flows and user return flows -Both fractional flow nodes and user nodes dictate proportional relationships between flows over edges in the subnetwork. Users have a return factor $0 \le r_i \le 1, i \in U_S$. +#### FractionalFlow and UserDemand return flows +Both FractionalFlow and UserDemand nodes dictate proportional relationships between flows over edges in the subnetwork. UserDemands have a return factor $0 \le r_i \le 1, i \in U_S$. ## The allocation network @@ -84,7 +84,8 @@ A new graph is created from the subnetwork, which we call an allocation network. The allocation network consists of: -- Nodes $V'_S \subset V_S$, where each basin, source and user in the subnetwork get a node in the allocation network. Also nodes that have fractional flow outneighbors get a node in the allocation network. +- Nodes $V'_S \subset V_S$, where each Basin, source and demand in the subnetwork get a node in the allocation network. +Also nodes that have FractionalFlow outneighbors get a node in the allocation network. - Edges $E_S$, which are either edges that also appear between nodes in the subnetwork or represent a sequence of those, creating a shortcut. For notational convenience, we use the notation @@ -124,39 +125,40 @@ There are several types of variable whose value has to be determined to solve th The goal of allocation is to get the flow to nodes with demands as close as possible to these demands. To achieve this, a sum error of terms is minimized. The form of these error terms is determined by the choice of one of the supported objective function types, which are discussed further below. $$ - \min E_{\text{user}} + E_{\text{basin}} + \min E_{\text{user_demand}} + E_{\text{basin}} $$ ### User demands - `quadratic_absolute`: $$ - E_{\text{user}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left( F_{ij} - d_j^p(t)\right)^2 + E_{\text{user_demand}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left( F_{ij} - d_j^p(t)\right)^2 $$ - `quadratic_relative`: $$ - E_{\text{user}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left( 1 - \frac{F_{ij}}{d_j^p(t)}\right)^2 + E_{\text{user_demand}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left( 1 - \frac{F_{ij}}{d_j^p(t)}\right)^2 $$ - `linear_absolute` (default): $$ - E_{\text{user}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left| F_{ij} - d_j^p(t)\right| + E_{\text{user_demand}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left| F_{ij} - d_j^p(t)\right| $$ - `linear_relative`: $$ - E_{\text{user}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left|1 - \frac{F_{ij}}{d_j^p(t)}\right| + E_{\text{user_demand}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left|1 - \frac{F_{ij}}{d_j^p(t)}\right| $$ :::{.callout-note} -When performing main network allocation, the connections to subnetworks are also interpreted as users with demands determined by subnetwork demand collection. +When performing main network allocation, the connections to subnetworks are also interpreted as UserDemand with demands determined by subnetwork demand collection. ::: To avoid division by $0$ errors, if a `*_relative` objective is used and a demand is $0$, the coefficient of the flow $F_{ij}$ 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. +For `*_absolute` objectives the optimizer cares about the actual amount of water allocated to a demand, for `*_relative` objectives it cares about the fraction of the demand allocated. +For `quadratic_*` objectives the optimizer cares about avoiding large shortages, for `linear_*` objectives it treats all deviations equally. :::{.callout-note} -These options for objectives for allocation to users have not been tested thoroughly, and might change in the future. +These options for objectives for allocation to demands 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). @@ -189,7 +191,7 @@ $$ $$ F^\text{basin in}_k + \sum_{j=1}^{n'} F_{kj} = F^\text{basin out}_k + \sum_{i=1}^{n'} F_{ik}, \quad \forall k \in B_S . $$ {#eq-flowconservationconstraint} -Note that we do not require equality here; in the allocation we do not mind that excess flow is 'forgotten' if it cannot contribute to the allocation to the users. +Note that we do not require equality here; in the allocation we do not mind that excess flow is 'forgotten' if it cannot contribute to the allocation to the demands. :::{.callout-note} In @eq-flowconservationconstraint, the placement of the basin flows might seem counter-intuitive. Think of the basin storage as a separate node connected to the basin node. @@ -210,15 +212,15 @@ When performing subnetwork demand collection, these capacities are set to $\inft ::: -- User outflow: The outflow of the user is dictated by the inflow and the return factor: +- UserDemand outflow: The outflow of the UserDemand is dictated by the inflow and the return factor: $$ F_{ik} = r_k \cdot F_{kj} \quad \quad \forall k \in U_S, \quad V^{\text{in}}_S(k) = \{i\},\; V^{\text{out}}_S(k) = \{j\}. $$ {#eq-returnflowconstraint} -Here we use that each user node in the allocation network has a unique in-edge and out-edge. -- User demand: user demand constraints are discussed in [the next section](allocation.qmd#sec-solving-allocation). +Here we use that each UserDemand node in the allocation network has a unique in-edge and out-edge. +- User demand: UserDemand demand constraints are discussed in [the next section](allocation.qmd#sec-solving-allocation). - Fractional flow: Let $L_S \subset V_S$ be the set of nodes in the max flow graph with fractional flow outneighbors, and $f_j$ the flow fraction associated with fractional flow node $j \in V_S$. Then $$ F_{ij} \le f_j \sum_{k\in V^\text{in}_S(i)} F_{ki} \qquad @@ -230,10 +232,10 @@ $$ {#eq-fractionalflowconstraint} ## Final notes on the allocation problem -### Users using their own return flow +### UserDemands using their own return flow -If not explicitly avoided, users can use their own return flow in this allocation problem formulation. -Therefore, return flow of users is only taken into account by allocation if that return flow is downstream of the user where it comes from. That is, if there is no path in the directed allocation network from the user outflow node back to the user. +If not explicitly avoided, UserDemands can use their own return flow in this allocation problem formulation. +Therefore, return flow of UserDemand is only taken into account by allocation if that return flow is downstream of the UserDemand where it comes from. That is, if there is no path in the directed allocation network from the UserDemand outflow node back to the UserDemand. # Solving the allocation problem {#sec-solving-allocation} @@ -243,7 +245,7 @@ The allocation problem for an allocation network at time $t$ is solved per prior $$ C_S^p \leftarrow C_S; $$ -2. Set the capacities of the edges that end in an user to their priority 1 demands: +2. Set the capacities of the edges that end in a UserDemand to their priority 1 demands: $$ (C_S^p)_{i,j} \leftarrow d_j^1(t) \quad\text{ for all } (i,j) \in U_S; $$ @@ -256,8 +258,8 @@ $$ :::{.callout-note} 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. +- One before optimizing for demands, taking the demand/supply from basins into account; +- One after optimizing for demands, taking preferences over sources into account. ::: ## Example diff --git a/docs/core/equations.qmd b/docs/core/equations.qmd index 2630806e6..5c616b21a 100644 --- a/docs/core/equations.qmd +++ b/docs/core/equations.qmd @@ -13,7 +13,7 @@ Ribasim currently simulates the following "natural" water balance terms: Additionally, Ribasim simulates the following "allocated" water balance terms: -1. General user +1. UserDemand 2. Flushing Depending on the type of boundary conditions, Ribasim requires relation between @@ -446,25 +446,25 @@ $$ B = w + 2 d \sqrt{\left(\frac{\Delta y}{\Delta z}\right)^2 + 1} $$ -# User allocation -Users have an allocated flow rate $F^p$ per priority $p=1,2,\ldots, p_{\max}$, which is either determined by [allocation optimization](allocation.qmd) or simply equal to the demand at time $t$; $F^p = d^p(t)$. The actual abstraction rate of a user is given by +# UserDemand allocation +UserDemands have an allocated flow rate $F^p$ per priority $p=1,2,\ldots, p_{\max}$, which is either determined by [allocation optimization](allocation.qmd) or simply equal to the demand at time $t$; $F^p = d^p(t)$. The actual abstraction rate of a UserDemand is given by $$ - Q_\text{user, in} = \phi(u, 10.0)\phi(h-l_{\min}, 0.1)\sum_{p=1}^{p_{\max}} \min\left(F^p, d^p(t)\right). + Q_\text{user_demand, in} = \phi(u, 10.0)\phi(h-l_{\min}, 0.1)\sum_{p=1}^{p_{\max}} \min\left(F^p, d^p(t)\right). $$ From left to right: -- The first reduction factor lets the user abstraction go smoothly to $0$ as the source basin dries out; -- The second reduction factor lets the user abstraction go smoothly to $0$ as the source basin level approaches the minimum source basin level (from above); +- The first reduction factor lets the UserDemand abstraction go smoothly to $0$ as the source Basin dries out; +- The second reduction factor lets the UserDemand abstraction go smoothly to $0$ as the source Basin level approaches the minimum source Basin level (from above); - The last term is the sum of the allocations over the priorities. If the current demand happens to be lower than the allocation at some priority, the demand is taken instead. -Users also have a return factor $0 \le r \le 1$, which determines the return flow (outflow) of the user: +UserDemands also have a return factor $0 \le r \le 1$, which determines the return flow (outflow) of the UserDemand: $$ -Q_\text{user, out} = r \cdot Q_\text{user, in}. +Q_\text{user_demand, out} = r \cdot Q_\text{user_demand, in}. $$ -Note that this means that the user has a consumption rate of $(1-r)Q_\text{user, in}$. +Note that this means that the user_demand has a consumption rate of $(1-r)Q_\text{user_demand, in}$. # PID controller {#sec-PID} diff --git a/docs/core/index.qmd b/docs/core/index.qmd index 82445ff6d..86df98110 100644 --- a/docs/core/index.qmd +++ b/docs/core/index.qmd @@ -48,7 +48,7 @@ sequenceDiagram Param-->>Optim: Input State-->>Optim: Input Optim->>Optim: Optimize Basin allocations if below target level - Optim->>Optim: Optimize User allocation, per priority + Optim->>Optim: Optimize UserDemand allocation, per priority Optim-->>Param: Set allocated flow rates deactivate Optim end @@ -88,9 +88,9 @@ The allocation will then be conducted in three steps: 1. conduct an inventory of demands from the sub-networks to inlets from the main network, 2. allocate the available water in the main network to the subnetworks inlets, -3. allocate the assigned water within each subnetwork to the individual water users. +3. allocate the assigned water within each subnetwork to the individual demand nodes. -The users then will request this updated demand from the rule-based simulation. +The demand nodes then will request this updated demand from the rule-based simulation. Whether this updated demand is indeed abstracted depends on all dry-fall control mechanism implemented in the rule-based simulation. The following sequence diagram illustrates this calculation process within then allocation phase. @@ -99,25 +99,25 @@ The following sequence diagram illustrates this calculation process within then sequenceDiagram participant boundary participant basin -participant user +participant user_demand participant allocation_subNetwork participant allocation_mainNetwork -user->>allocation_subNetwork: demand +user_demand->>allocation_subNetwork: demand loop allocation_subNetwork-->>allocation_mainNetwork: demand inventory at inlets end -user->>allocation_mainNetwork: demand +user_demand->>allocation_mainNetwork: demand boundary->>allocation_mainNetwork: source availability basin->>allocation_mainNetwork: source availability -allocation_mainNetwork-->>allocation_mainNetwork: allocate to inlets (and users) -allocation_mainNetwork->>user: allocated +allocation_mainNetwork-->>allocation_mainNetwork: allocate to inlets (and user_demands) +allocation_mainNetwork->>user_demand: allocated allocation_mainNetwork->>allocation_subNetwork: allocated loop - allocation_subNetwork-->>allocation_subNetwork: allocate to users + allocation_subNetwork-->>allocation_subNetwork: allocate to user_demands end -allocation_subNetwork->>user: allocated -user->>basin: abstracted +allocation_subNetwork->>user_demand: allocated +user_demand->>basin: abstracted ``` # Coupling diff --git a/docs/core/modelconcept.qmd b/docs/core/modelconcept.qmd index 8f13075db..20f260399 100644 --- a/docs/core/modelconcept.qmd +++ b/docs/core/modelconcept.qmd @@ -6,7 +6,7 @@ As indicated, the model concept is organized in three layers: - a physical layer representing water bodies and associated infrastructure, - a rule-based control layer to manage the infrastructure, and -- a priority-based allocation layer to take centralized decisions on user abstractions. +- a priority-based allocation layer to take centralized decisions on user_demand abstractions. # Physical layer diff --git a/docs/core/usage.qmd b/docs/core/usage.qmd index 6fb8b10b7..e12e0e45d 100644 --- a/docs/core/usage.qmd +++ b/docs/core/usage.qmd @@ -195,9 +195,9 @@ name it must have in the database if it is stored there. - `Pump / static`: flow rate - Outlet: let water flow with a prescribed flux under the conditions of positive head difference and the upstream level being higher than the minimum crest level - `Outlet / static`: flow rate, minimum crest level -- User: sets water usage demands at certain priorities - - `User / static`: demands - - `User / time`: dynamic demands +- UserDemand: sets water usage demands at certain priorities + - `UserDemand / static`: demands + - `UserDemand / time`: dynamic demands - LevelDemand: Indicates minimum and maximum target level of connected basins for allocation - `LevelDemand / static`: static target levels - `LevelDemand / time`: dynamic target levels @@ -425,18 +425,18 @@ min_flow_rate | Float64 | $m^3 s^{-1}$ | (optional, default 0.0) max_flow_rate | Float64 | $m^3 s^{-1}$ | (optional) min_crest_level | Float64 | $m$ | (optional) -# User {#sec-user} +# UserDemand {#sec-user-demand} -A user can demand a certain flow from the basin that supplies it, +A UserDemand can demand a certain flow from the basin that supplies it, distributed over multiple priorities 1,2,3,... where priority 1 denotes the most important demand. -Currently the user attempts to extract the complete demand from the Basin. -Only if the Basin is almost empty or reaches the minimum level at which the user can extract +Currently the UserDemand attempts to extract the complete demand from the Basin. +Only if the Basin is almost empty or reaches the minimum level at which the UserDemand can extract water (`min_level`), will it take less than the demand. -In the future water can be allocated to users based on their priority. -Users need an outgoing flow edge along which they can send their return flow, +When allocation is used, water can be allocated to UserDemand based on their priority. +UserDemands need an outgoing flow edge along which they can send their return flow, this can also be to the same basin from which it extracts water. -The amount of return flow is always a fraction of the inflow into the user. -The difference is consumed by the user. +The amount of return flow is always a fraction of the inflow into the UserDemand. +The difference is consumed by the UserDemand. column | type | unit | restriction ------------- | ------- | ------------ | ----------- @@ -447,9 +447,9 @@ return_factor | Float64 | - | between [0 - 1] min_level | Float64 | $m$ | - priority | Int | - | positive, sorted per node id -## User / time +## UserDemand / time -This table is the transient form of the `User` table. +This table is the transient form of the `UserDemand` table. The only difference is that a time column is added and activity is assumed to be true. The table must by sorted by time, and per time it must be sorted by `node_id`. With this the demand can be updated over time. In between the given times the diff --git a/docs/index.qmd b/docs/index.qmd index a478c0477..e54af16b2 100644 --- a/docs/index.qmd +++ b/docs/index.qmd @@ -56,7 +56,7 @@ flowchart TB physical:::layer rbc:::layer allocation:::layer -user +user_demand basin connector[basin connector] control[control rules] @@ -64,7 +64,7 @@ condition alloc[global allocation] subgraph physical[physical layer] - user-->|abstraction| basin + user_demand-->|abstraction| basin basin<-->|flow| connector end @@ -76,8 +76,8 @@ subgraph allocation[allocation layer] alloc end -user-->|request demand| alloc -alloc-->|assign allocation| user +user_demand-->|request demand| alloc +alloc-->|assign allocation| user_demand basin-->|volume| alloc basin --> |volume or level| condition alloc --> |optional flow update| control diff --git a/docs/python/examples.ipynb b/docs/python/examples.ipynb index 776ca7e52..3f7573a2e 100644 --- a/docs/python/examples.ipynb +++ b/docs/python/examples.ipynb @@ -1425,17 +1425,17 @@ " [\n", " (0.0, 0.0), # 1: FlowBoundary\n", " (1.0, 0.0), # 2: Basin\n", - " (1.0, 1.0), # 3: User\n", + " (1.0, 1.0), # 3: UserDemand\n", " (2.0, 0.0), # 4: LinearResistance\n", " (3.0, 0.0), # 5: Basin\n", - " (3.0, 1.0), # 6: User\n", + " (3.0, 1.0), # 6: UserDemand\n", " (4.0, 0.0), # 7: TabulatedRatingCurve\n", " (4.5, 0.0), # 8: FractionalFlow\n", " (4.5, 0.5), # 9: FractionalFlow\n", " (5.0, 0.0), # 10: Terminal\n", " (4.5, 0.25), # 11: DiscreteControl\n", " (4.5, 1.0), # 12: Basin\n", - " (5.0, 1.0), # 13: User\n", + " (5.0, 1.0), # 13: UserDemand\n", " ]\n", ")\n", "node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n", @@ -1443,17 +1443,17 @@ "node_type = [\n", " \"FlowBoundary\",\n", " \"Basin\",\n", - " \"User\",\n", + " \"UserDemand\",\n", " \"LinearResistance\",\n", " \"Basin\",\n", - " \"User\",\n", + " \"UserDemand\",\n", " \"TabulatedRatingCurve\",\n", " \"FractionalFlow\",\n", " \"FractionalFlow\",\n", " \"Terminal\",\n", " \"DiscreteControl\",\n", " \"Basin\",\n", - " \"User\",\n", + " \"UserDemand\",\n", "]\n", "\n", "# All nodes belong to allocation network id 1\n", @@ -1695,7 +1695,7 @@ "metadata": {}, "outputs": [], "source": [ - "user = ribasim.User(\n", + "user_demand = ribasim.UserDemand(\n", " static=pd.DataFrame(\n", " data={\n", " \"node_id\": [6, 13],\n", @@ -1757,7 +1757,7 @@ " linear_resistance=linear_resistance,\n", " tabulated_rating_curve=tabulated_rating_curve,\n", " terminal=terminal,\n", - " user=user,\n", + " user_demand=user_demand,\n", " discrete_control=discrete_control,\n", " fractional_flow=fractional_flow,\n", " allocation=allocation,\n", @@ -1912,14 +1912,14 @@ " [\n", " (0.0, 0.0), # 1: FlowBoundary\n", " (1.0, 0.0), # 2: Basin\n", - " (2.0, 0.0), # 3: User\n", + " (2.0, 0.0), # 3: UserDemand\n", " (1.0, -1.0), # 4: LevelDemand\n", " (2.0, -1.0), # 5: Basin\n", " ]\n", ")\n", "node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n", "\n", - "node_type = [\"FlowBoundary\", \"Basin\", \"User\", \"LevelDemand\", \"Basin\"]\n", + "node_type = [\"FlowBoundary\", \"Basin\", \"UserDemand\", \"LevelDemand\", \"Basin\"]\n", "\n", "# Make sure the feature id starts at 1: explicitly give an index.\n", "node = ribasim.Node(\n", @@ -2061,7 +2061,7 @@ "metadata": {}, "outputs": [], "source": [ - "user = ribasim.User(\n", + "user_demand = ribasim.UserDemand(\n", " static=pd.DataFrame(\n", " data={\n", " \"node_id\": [3],\n", @@ -2108,7 +2108,7 @@ " basin=basin,\n", " flow_boundary=flow_boundary,\n", " level_demand=level_demand,\n", - " user=user,\n", + " user_demand=user_demand,\n", " allocation=allocation,\n", " starttime=\"2020-01-01 00:00:00\",\n", " endtime=\"2020-02-01 00:00:00\",\n", @@ -2202,12 +2202,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In the plot above, the line denotes the level of basin #2 over time and the dots denote the times at which allocation optimization was run, with intervals of $\\Delta t_{\\text{alloc}}$. The basin level is a piecewise linear function of time, with several stages explained below.\n", + "In the plot above, the line denotes the level of Basin #2 over time and the dots denote the times at which allocation optimization was run, with intervals of $\\Delta t_{\\text{alloc}}$.\n", + "The Basin level is a piecewise linear function of time, with several stages explained below.\n", "\n", "Constants:\n", - "- $d$: user #3 demand,\n", - "- $\\phi$: precipitation rate on basin #2,\n", - "- $q$: flow of the level boundary." + "- $d$: UserDemand #3 demand,\n", + "- $\\phi$: Basin #2 precipitation rate,\n", + "- $q$: LevelBoundary flow." ] }, { @@ -2215,12 +2216,12 @@ "metadata": {}, "source": [ "Stages:\n", - "- In the first stage the user abstracts fully, so the net change of basin #2 is $q + \\phi - d$;\n", - "- In the second stage the basin takes precedence so the user doesn't abstract, hence the net change of basin #2 is $q + \\phi$;\n", - "- In the third stage (and following stages) the basin has no longer a positive demand, since precipitation provides enough water to get the basin to its target level. The FlowBoundary flow gets fully allocated to the user, hence the net change of basin #2 is $\\phi$;\n", - "- In the fourth stage the basin enters its surplus stage, even though initially the level is below the maximum level. This is because the simulation anticipates that the current precipitation is going to bring the basin level over its maximum level. The net change of basin #2 is now $q + \\phi - d$;\n", - "- At the start of the fifth stage the precipitation stops, and so the user partly uses surplus water from the basin to fulfill its demand. The net change of basin #2 becomes $q - d$.\n", - "- In the final stage the basin is in a dynamical equilibrium, since the basin has no supply so the user abstracts precisely the flow from the level boundary." + "- In the first stage the UserDemand abstracts fully, so the net change of Basin #2 is $q + \\phi - d$;\n", + "- In the second stage the Basin takes precedence so the UserDemand doesn't abstract, hence the net change of Basin #2 is $q + \\phi$;\n", + "- In the third stage (and following stages) the Basin no longer has a positive demand, since precipitation provides enough water to get the Basin to its target level. The FlowBoundary flow gets fully allocated to the UserDemand, hence the net change of Basin #2 is $\\phi$;\n", + "- In the fourth stage the Basin enters its surplus stage, even though initially the level is below the maximum level. This is because the simulation anticipates that the current precipitation is going to bring the Basin level over its maximum level. The net change of Basin #2 is now $q + \\phi - d$;\n", + "- At the start of the fifth stage the precipitation stops, and so the UserDemand partly uses surplus water from the Basin to fulfill its demand. The net change of Basin #2 becomes $q - d$.\n", + "- In the final stage the Basin is in a dynamical equilibrium, since the Basin has no supply so the user abstracts precisely the flow from the LevelBoundary." ] }, { diff --git a/python/ribasim/ribasim/__init__.py b/python/ribasim/ribasim/__init__.py index 97dc9a556..6227052c9 100644 --- a/python/ribasim/ribasim/__init__.py +++ b/python/ribasim/ribasim/__init__.py @@ -20,7 +20,7 @@ Solver, TabulatedRatingCurve, Terminal, - User, + UserDemand, Verbosity, ) from ribasim.geometry.edge import Edge, EdgeSchema @@ -51,7 +51,7 @@ "Solver", "TabulatedRatingCurve", "Terminal", - "User", + "UserDemand", "utils", "Verbosity", ] diff --git a/python/ribasim/ribasim/config.py b/python/ribasim/ribasim/config.py index 997bc1b11..d6c3005a0 100644 --- a/python/ribasim/ribasim/config.py +++ b/python/ribasim/ribasim/config.py @@ -29,8 +29,8 @@ TabulatedRatingCurveStaticSchema, TabulatedRatingCurveTimeSchema, TerminalStaticSchema, - UserStaticSchema, - UserTimeSchema, + UserDemandStaticSchema, + UserDemandTimeSchema, ) @@ -120,13 +120,13 @@ class TabulatedRatingCurve(NodeModel): ) -class User(NodeModel): - static: TableModel[UserStaticSchema] = Field( - default_factory=TableModel[UserStaticSchema], +class UserDemand(NodeModel): + static: TableModel[UserDemandStaticSchema] = Field( + default_factory=TableModel[UserDemandStaticSchema], json_schema_extra={"sort_keys": ["node_id", "priority"]}, ) - time: TableModel[UserTimeSchema] = Field( - default_factory=TableModel[UserTimeSchema], + time: TableModel[UserDemandTimeSchema] = Field( + default_factory=TableModel[UserDemandTimeSchema], json_schema_extra={"sort_keys": ["node_id", "priority", "time"]}, ) diff --git a/python/ribasim/ribasim/geometry/node.py b/python/ribasim/ribasim/geometry/node.py index 7ab402189..33bb77b98 100644 --- a/python/ribasim/ribasim/geometry/node.py +++ b/python/ribasim/ribasim/geometry/node.py @@ -215,7 +215,7 @@ def plot(self, ax=None, zorder=None) -> Any: "FlowBoundary": "h", "DiscreteControl": "*", "PidControl": "x", - "User": "s", + "UserDemand": "s", "LevelDemand": "o", "": "o", } @@ -233,7 +233,7 @@ def plot(self, ax=None, zorder=None) -> Any: "FlowBoundary": "m", "DiscreteControl": "k", "PidControl": "k", - "User": "g", + "UserDemand": "g", "LevelDemand": "k", "": "k", } diff --git a/python/ribasim/ribasim/model.py b/python/ribasim/ribasim/model.py index f12aafc77..702804df8 100644 --- a/python/ribasim/ribasim/model.py +++ b/python/ribasim/ribasim/model.py @@ -34,7 +34,7 @@ Solver, TabulatedRatingCurve, Terminal, - User, + UserDemand, ) from ribasim.geometry.edge import Edge from ribasim.geometry.node import Node @@ -152,8 +152,8 @@ class Model(FileModel): Discrete control logic. pid_control : PidControl PID controller attempting to set the level of a basin to a desired value using a pump/outlet. - user : User - User node type with demand and priority. + user_demand : UserDemand + UserDemand node type with demand and priority. """ starttime: datetime.datetime @@ -183,7 +183,7 @@ class Model(FileModel): terminal: Terminal = Field(default_factory=Terminal) discrete_control: DiscreteControl = Field(default_factory=DiscreteControl) pid_control: PidControl = Field(default_factory=PidControl) - user: User = Field(default_factory=User) + user_demand: UserDemand = Field(default_factory=UserDemand) @model_validator(mode="after") def set_node_parent(self) -> "Model": diff --git a/python/ribasim/ribasim/schemas.py b/python/ribasim/ribasim/schemas.py index a97f8a1cf..52120e5d7 100644 --- a/python/ribasim/ribasim/schemas.py +++ b/python/ribasim/ribasim/schemas.py @@ -188,7 +188,7 @@ class TerminalStaticSchema(_BaseSchema): node_id: Series[int] = pa.Field(nullable=False) -class UserStaticSchema(_BaseSchema): +class UserDemandStaticSchema(_BaseSchema): node_id: Series[int] = pa.Field(nullable=False) active: Series[pa.BOOL] = pa.Field(nullable=True) demand: Series[float] = pa.Field(nullable=False) @@ -197,7 +197,7 @@ class UserStaticSchema(_BaseSchema): priority: Series[int] = pa.Field(nullable=False) -class UserTimeSchema(_BaseSchema): +class UserDemandTimeSchema(_BaseSchema): node_id: Series[int] = pa.Field(nullable=False) time: Series[Timestamp] = pa.Field(nullable=False) demand: Series[float] = pa.Field(nullable=False) diff --git a/python/ribasim_testmodels/ribasim_testmodels/__init__.py b/python/ribasim_testmodels/ribasim_testmodels/__init__.py index 781ce9806..7004e5aa1 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/__init__.py +++ b/python/ribasim_testmodels/ribasim_testmodels/__init__.py @@ -13,7 +13,7 @@ main_network_with_subnetworks_model, minimal_subnetwork_model, subnetwork_model, - user_model, + user_demand_model, ) from ribasim_testmodels.backwater import backwater_model from ribasim_testmodels.basic import ( @@ -89,7 +89,7 @@ "tabulated_rating_curve_model", "trivial_model", "two_basin_model", - "user_model", + "user_demand_model", ] # provide a mapping from model name to its constructor, so we can iterate over all models diff --git a/python/ribasim_testmodels/ribasim_testmodels/allocation.py b/python/ribasim_testmodels/ribasim_testmodels/allocation.py index 4cbdb077b..0c51d0f2b 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/allocation.py +++ b/python/ribasim_testmodels/ribasim_testmodels/allocation.py @@ -4,21 +4,21 @@ import ribasim -def user_model(): - """Create a user test model with static and dynamic users on the same basin.""" +def user_demand_model(): + """Create a UserDemand test model with static and dynamic UserDemand on the same basin.""" # Set up the nodes: xy = np.array( [ (0.0, 0.0), # 1: Basin - (1.0, 0.5), # 2: User - (1.0, -0.5), # 3: User + (1.0, 0.5), # 2: UserDemand + (1.0, -0.5), # 3: UserDemand (2.0, 0.0), # 4: Terminal ] ) node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = ["Basin", "User", "User", "Terminal"] + node_type = ["Basin", "UserDemand", "UserDemand", "Terminal"] # Make sure the feature id starts at 1: explicitly give an index. node = ribasim.Node( @@ -59,8 +59,8 @@ def user_model(): basin = ribasim.Basin(profile=profile, state=state) - # Setup the users: - user = ribasim.User( + # Setup the UserDemands: + user_demand = ribasim.UserDemand( static=pd.DataFrame( data={ "node_id": [2], @@ -101,7 +101,7 @@ def user_model(): model = ribasim.Model( network=ribasim.Network(node=node, edge=edge), basin=basin, - user=user, + user_demand=user_demand, terminal=terminal, solver=solver, starttime="2020-01-01 00:00:00", @@ -112,7 +112,7 @@ def user_model(): def subnetwork_model(): - """Create a user testmodel representing a subnetwork. + """Create a UserDemand testmodel representing a subnetwork. This model is merged into main_network_with_subnetworks_model. """ @@ -128,9 +128,9 @@ def subnetwork_model(): (1.0, 3.0), # 7: Outlet (0.0, 3.0), # 8: Basin (2.0, 5.0), # 9: Terminal - (2.0, 0.0), # 10: User - (3.0, 3.0), # 11: User - (0.0, 4.0), # 12: User + (2.0, 0.0), # 10: UserDemand + (3.0, 3.0), # 11: UserDemand + (0.0, 4.0), # 12: UserDemand (2.0, 4.0), # 13: Outlet ] ) @@ -146,9 +146,9 @@ def subnetwork_model(): "Outlet", "Basin", "Terminal", - "User", - "User", - "User", + "UserDemand", + "UserDemand", + "UserDemand", "Outlet", ] @@ -203,8 +203,8 @@ def subnetwork_model(): ) ) - # Setup the users: - user = ribasim.User( + # Setup the UserDemand: + user_demand = ribasim.UserDemand( static=pd.DataFrame( data={ "node_id": [10, 11, 12], @@ -249,7 +249,7 @@ def subnetwork_model(): model = ribasim.Model( network=ribasim.Network(node=node, edge=edge), basin=basin, - user=user, + user_demand=user_demand, flow_boundary=flow_boundary, pump=pump, outlet=outlet, @@ -263,13 +263,13 @@ def subnetwork_model(): def looped_subnetwork_model(): - """Create a user testmodel representing a subnetwork containing a loop in the topology. + """Create a UserDemand testmodel representing a subnetwork containing a loop in the topology. This model is merged into main_network_with_subnetworks_model. """ # Setup the nodes: xy = np.array( [ - (0.0, 0.0), # 1: User + (0.0, 0.0), # 1: UserDemand (0.0, 1.0), # 2: Basin (-1.0, 1.0), # 3: Outlet (-2.0, 1.0), # 4: Terminal @@ -280,25 +280,25 @@ def looped_subnetwork_model(): (0.0, 3.0), # 9: Basin (1.0, 3.0), # 10: Outlet (2.0, 3.0), # 11: Basin - (-2.0, 4.0), # 12: User + (-2.0, 4.0), # 12: UserDemand (0.0, 4.0), # 13: TabulatedRatingCurve (2.0, 4.0), # 14: TabulatedRatingCurve (0.0, 5.0), # 15: Basin (1.0, 5.0), # 16: Pump (2.0, 5.0), # 17: Basin - (-1.0, 6.0), # 18: User + (-1.0, 6.0), # 18: UserDemand (0.0, 6.0), # 19: TabulatedRatingCurve - (2.0, 6.0), # 20: User + (2.0, 6.0), # 20: UserDemand (0.0, 7.0), # 21: Basin (0.0, 8.0), # 22: Outlet (0.0, 9.0), # 23: Terminal - (3.0, 3.0), # 24: User + (3.0, 3.0), # 24: UserDemand ] ) node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) node_type = [ - "User", + "UserDemand", "Basin", "Outlet", "Terminal", @@ -309,19 +309,19 @@ def looped_subnetwork_model(): "Basin", "Outlet", "Basin", - "User", + "UserDemand", "TabulatedRatingCurve", "TabulatedRatingCurve", "Basin", "Pump", "Basin", - "User", + "UserDemand", "TabulatedRatingCurve", - "User", + "UserDemand", "Basin", "Outlet", "Terminal", - "User", + "UserDemand", ] # Make sure the feature id starts at 1: explicitly give an index. @@ -437,8 +437,8 @@ def looped_subnetwork_model(): static=pd.DataFrame(data={"node_id": [5], "flow_rate": [4.5e-3]}) ) - # Setup the users: - user = ribasim.User( + # Setup the user_demands: + user_demand = ribasim.UserDemand( static=pd.DataFrame( data={ "node_id": [1, 12, 18, 20, 24], @@ -495,7 +495,7 @@ def looped_subnetwork_model(): network=ribasim.Network(node=node, edge=edge), basin=basin, flow_boundary=flow_boundary, - user=user, + user_demand=user_demand, pump=pump, outlet=outlet, tabulated_rating_curve=rating_curve, @@ -516,13 +516,13 @@ def minimal_subnetwork_model(): (0.0, 1.0), # 2: Basin (0.0, 2.0), # 3: Pump (0.0, 3.0), # 4: Basin - (-1.0, 4.0), # 5: User - (1.0, 4.0), # 6: User + (-1.0, 4.0), # 5: UserDemand + (1.0, 4.0), # 6: UserDemand ] ) node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = ["FlowBoundary", "Basin", "Pump", "Basin", "User", "User"] + node_type = ["FlowBoundary", "Basin", "Pump", "Basin", "UserDemand", "UserDemand"] # Make sure the feature id starts at 1: explicitly give an index. node = ribasim.Node( @@ -593,8 +593,8 @@ def minimal_subnetwork_model(): ) ) - # Setup the users: - user = ribasim.User( + # Setup the UserDemand: + user_demand = ribasim.UserDemand( static=pd.DataFrame( data={ "node_id": [5], @@ -627,7 +627,7 @@ def minimal_subnetwork_model(): basin=basin, flow_boundary=flow_boundary, pump=pump, - user=user, + user_demand=user_demand, allocation=allocation, starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", @@ -648,10 +648,10 @@ def fractional_flow_subnetwork_model(): (0.0, 2.0), # 3: TabulatedRatingCurve (-1.0, 3.0), # 4: FractionalFlow (-2.0, 4.0), # 5: Basin - (-3.0, 5.0), # 6: User + (-3.0, 5.0), # 6: UserDemand (1.0, 3.0), # 7: FractionalFlow (2.0, 4.0), # 8: Basin - (3.0, 5.0), # 9: User + (3.0, 5.0), # 9: UserDemand (-1.0, 2.0), # 10: DiscreteControl ] ) @@ -663,10 +663,10 @@ def fractional_flow_subnetwork_model(): "TabulatedRatingCurve", "FractionalFlow", "Basin", - "User", + "UserDemand", "FractionalFlow", "Basin", - "User", + "UserDemand", "DiscreteControl", ] @@ -740,8 +740,8 @@ def fractional_flow_subnetwork_model(): ) ) - # Setup the users: - user = ribasim.User( + # Setup the UserDemands + user_demand = ribasim.UserDemand( static=pd.DataFrame( data={ "node_id": [6], @@ -805,7 +805,7 @@ def fractional_flow_subnetwork_model(): basin=basin, flow_boundary=flow_boundary, tabulated_rating_curve=rating_curve, - user=user, + user_demand=user_demand, allocation=allocation, fractional_flow=fractional_flow, discrete_control=discrete_control, @@ -823,17 +823,17 @@ def allocation_example_model(): [ (0.0, 0.0), # 1: FlowBoundary (1.0, 0.0), # 2: Basin - (1.0, 1.0), # 3: User + (1.0, 1.0), # 3: UserDemand (2.0, 0.0), # 4: LinearResistance (3.0, 0.0), # 5: Basin - (3.0, 1.0), # 6: User + (3.0, 1.0), # 6: UserDemand (4.0, 0.0), # 7: TabulatedRatingCurve (4.5, 0.0), # 8: FractionalFlow (4.5, 0.5), # 9: FractionalFlow (5.0, 0.0), # 10: Terminal (4.5, 0.25), # 11: DiscreteControl (4.5, 1.0), # 12: Basin - (5.0, 1.0), # 13: User + (5.0, 1.0), # 13: UserDemand ] ) node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) @@ -841,17 +841,17 @@ def allocation_example_model(): node_type = [ "FlowBoundary", "Basin", - "User", + "UserDemand", "LinearResistance", "Basin", - "User", + "UserDemand", "TabulatedRatingCurve", "FractionalFlow", "FractionalFlow", "Terminal", "DiscreteControl", "Basin", - "User", + "UserDemand", ] # All nodes belong to allocation network id 2 @@ -968,7 +968,7 @@ def allocation_example_model(): discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) - user = ribasim.User( + user_demand = ribasim.UserDemand( static=pd.DataFrame( data={ "node_id": [6, 13], @@ -1003,7 +1003,7 @@ def allocation_example_model(): linear_resistance=linear_resistance, tabulated_rating_curve=tabulated_rating_curve, terminal=terminal, - user=user, + user_demand=user_demand, discrete_control=discrete_control, fractional_flow=fractional_flow, allocation=allocation, @@ -1101,21 +1101,21 @@ def main_network_with_subnetworks_model(): "Outlet", "Basin", "Terminal", - "User", - "User", - "User", + "UserDemand", + "UserDemand", + "UserDemand", "Outlet", "Pump", "Basin", "TabulatedRatingCurve", "FractionalFlow", "Basin", - "User", + "UserDemand", "FractionalFlow", "Basin", - "User", + "UserDemand", "DiscreteControl", - "User", + "UserDemand", "Basin", "Outlet", "Terminal", @@ -1126,19 +1126,19 @@ def main_network_with_subnetworks_model(): "Basin", "Outlet", "Basin", - "User", + "UserDemand", "TabulatedRatingCurve", "TabulatedRatingCurve", "Basin", "Pump", "Basin", - "User", + "UserDemand", "TabulatedRatingCurve", - "User", + "UserDemand", "Basin", "Outlet", "Terminal", - "User", + "UserDemand", ] subnetwork_id = np.ones(57, dtype=int) @@ -1586,8 +1586,8 @@ def main_network_with_subnetworks_model(): # Setup terminal node terminal = ribasim.Terminal(static=pd.DataFrame(data={"node_id": [14, 19, 37, 56]})) - # Setup the user - user = ribasim.User( + # Setup the UserDemand + user_demand = ribasim.UserDemand( static=pd.DataFrame( data={ "node_id": [20, 21, 22, 29, 34, 45, 51, 53, 57], @@ -1632,7 +1632,7 @@ def main_network_with_subnetworks_model(): outlet=outlet, pump=pump, terminal=terminal, - user=user, + user_demand=user_demand, tabulated_rating_curve=rating_curve, allocation=allocation, starttime="2020-01-01 00:00:00", @@ -1648,14 +1648,14 @@ def level_demand_model(): [ (0.0, 0.0), # 1: FlowBoundary (1.0, 0.0), # 2: Basin - (2.0, 0.0), # 3: User + (2.0, 0.0), # 3: UserDemand (1.0, -1.0), # 4: LevelDemand (2.0, -1.0), # 5: Basin ] ) node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = ["FlowBoundary", "Basin", "User", "LevelDemand", "Basin"] + node_type = ["FlowBoundary", "Basin", "UserDemand", "LevelDemand", "Basin"] # Make sure the feature id starts at 1: explicitly give an index. node = ribasim.Node( @@ -1731,8 +1731,8 @@ def level_demand_model(): ) ) - # Setup user - user = ribasim.User( + # Setup UserDemand + user_demand = ribasim.UserDemand( static=pd.DataFrame( data={ "node_id": [3], @@ -1752,7 +1752,7 @@ def level_demand_model(): basin=basin, flow_boundary=flow_boundary, level_demand=level_demand, - user=user, + user_demand=user_demand, allocation=allocation, starttime="2020-01-01 00:00:00", endtime="2020-02-01 00:00:00", diff --git a/ribasim_qgis/core/nodes.py b/ribasim_qgis/core/nodes.py index dd7bd90a3..bf59700bd 100644 --- a/ribasim_qgis/core/nodes.py +++ b/ribasim_qgis/core/nodes.py @@ -214,7 +214,7 @@ def renderer(self) -> QgsCategorizedSymbolRenderer: "Terminal": (QColor("purple"), "Terminal", shape.Square), "DiscreteControl": (QColor("black"), "DiscreteControl", shape.Star), "PidControl": (QColor("black"), "PidControl", shape.Cross2), - "User": (QColor("green"), "User", shape.Square), + "UserDemand": (QColor("green"), "UserDemand", shape.Square), "LevelDemand": ( QColor("black"), "LevelDemand", @@ -747,10 +747,10 @@ def attributes(cls) -> list[QgsField]: ] -class UserStatic(Input): +class UserDemandStatic(Input): @classmethod def input_type(cls) -> str: - return "User / static" + return "UserDemand / static" @classmethod def geometry_type(cls) -> str: @@ -767,10 +767,10 @@ def attributes(cls) -> list[QgsField]: ] -class UserTime(Input): +class UserDemandTime(Input): @classmethod def input_type(cls) -> str: - return "User / time" + return "UserDemand / time" @classmethod def geometry_type(cls) -> str: