Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate allocation input #972

Merged
merged 7 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion core/src/Ribasim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ using Graphs:
inneighbors,
nv,
outneighbors,
rem_edge!
rem_edge!,
induced_subgraph,
is_connected

using Legolas: Legolas, @schema, @version, validate, SchemaVersion, declared
using Logging: with_logger, LogLevel, AbstractLogger
Expand Down
6 changes: 6 additions & 0 deletions core/src/create.jl
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@
function generate_allocation_models!(p::Parameters, config::Config)::Nothing
(; graph, allocation_models) = p

errors = non_positive_allocation_network_id(graph)

if errors
error("Allocation network initialization failed.")

Check warning on line 205 in core/src/create.jl

View check run for this annotation

Codecov / codecov/patch

core/src/create.jl#L205

Added line #L205 was not covered by tests
end

for allocation_network_id in keys(graph[].node_ids)
push!(
allocation_models,
Expand Down
26 changes: 26 additions & 0 deletions core/src/solve.jl
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,32 @@ struct User <: AbstractParameterNode
allocated::Vector{Float64},
abstracted::Vector{Float64},
}

function User(
node_id,
active,
demand,
allocated,
return_factor,
min_level,
priorities,
record,
)
if valid_demand(node_id, demand, priorities)
return new(
node_id,
active,
demand,
allocated,
return_factor,
min_level,
priorities,
record,
)
else
error("Invalid demand")
end
end
end

"Subgrid linearly interpolates basin levels."
Expand Down
4 changes: 4 additions & 0 deletions core/src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
end
end

if incomplete_subnetwork(graph, node_ids)
error("Incomplete connectivity in subnetwork")

Check warning on line 105 in core/src/utils.jl

View check run for this annotation

Codecov / codecov/patch

core/src/utils.jl#L105

Added line #L105 was not covered by tests
end

flow = zeros(flow_counter)
flow_vertical = zeros(flow_vertical_counter)
if config.solver.autodiff
Expand Down
43 changes: 43 additions & 0 deletions core/src/validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -636,3 +636,46 @@ function valid_subgrid(

return !errors
end

function valid_demand(
node_id::Vector{NodeID},
demand::Vector{
Vector{LinearInterpolation{Vector{Float64}, Vector{Float64}, true, Float64}},
},
priorities::Vector{Int},
)::Bool
errors = false

for (col, id) in zip(demand, node_id)
for (demand_p_itp, p_itp) in zip(col, priorities)
if any(demand_p_itp.u .< 0.0)
@error "Demand of user node $id with priority $p_itp should be non-negative"
errors = true
end
end
end
return !errors
end

function incomplete_subnetwork(graph::MetaGraph, node_ids::Dict{Int, Set{NodeID}})::Bool
errors = false
for (allocation_network_id, subnetwork_node_ids) in node_ids
subnetwork, _ = induced_subgraph(graph, code_for.(Ref(graph), subnetwork_node_ids))
if !is_connected(subnetwork)
@error "All nodes in subnetwork $allocation_network_id should be connected"
errors = true
end
end
return errors
end

function non_positive_allocation_network_id(graph::MetaGraph)::Bool
errors = false
for allocation_network_id in keys(graph[].node_ids)
if (allocation_network_id <= 0)
@error "Allocation network id $allocation_network_id needs to be a positive integer."
errors = true
end
end
return errors
end
89 changes: 89 additions & 0 deletions core/test/create_test.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
@testitem "Non-positive allocation network ID" begin
using MetaGraphsNext
using Graphs
using Logging
using Ribasim
using Accessors: @set

graph = MetaGraph(
DiGraph();
label_type = Ribasim.NodeID,
vertex_data_type = Ribasim.NodeMetadata,
edge_data_type = Symbol,
graph_data = Tuple,
)

graph[Ribasim.NodeID(1)] = Ribasim.NodeMetadata(Symbol(:delft), 1)
graph[Ribasim.NodeID(2)] = Ribasim.NodeMetadata(Symbol(:denhaag), -1)

graph[1, 2] = :yes

node_ids = Dict{Int, Set{Ribasim.NodeID}}()
node_ids[0] = Set{Ribasim.NodeID}()
node_ids[-1] = Set{Ribasim.NodeID}()
push!(node_ids[0], Ribasim.NodeID(1))
push!(node_ids[-1], Ribasim.NodeID(2))

graph_data = (; node_ids,)
graph = @set graph.graph_data = graph_data

logger = TestLogger()
with_logger(logger) do
Ribasim.non_positive_allocation_network_id(graph)
end

@test length(logger.logs) == 2
@test logger.logs[1].level == Error
@test logger.logs[1].message ==
"Allocation network id 0 needs to be a positive integer."
@test logger.logs[2].level == Error
@test logger.logs[2].message ==
"Allocation network id -1 needs to be a positive integer."
end

@testitem "Incomplete subnetwork" begin
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding this test: It is good practice to create your own test model to do this, but I thought of an easier way: load the parameters of an existing model, say

toml_path = normpath(@__DIR__, "../../generated_testmodels/allocation_example/ribasim.toml")
p = Ribasim.Model(toml_path).integrator.p

remove an edge to make it invalid and run your validation test on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This morning I tried to load an existing model and change one of the allocation id to negative number. It gave error saying the immutable struct of type cannot be changed. I remembered you mentioned this yesterday, it probably can't be changed or maybe I didn't do it the correct way. Anyhow, in my new code I created a graph that has the required feature and used it as the test model.

using MetaGraphsNext
using Graphs
using Logging
using Ribasim

graph = MetaGraph(
DiGraph();
label_type = Ribasim.NodeID,
vertex_data_type = Ribasim.NodeMetadata,
edge_data_type = Symbol,
graph_data = Tuple,
)

node_ids = Dict{Int, Set{Ribasim.NodeID}}()
node_ids[1] = Set{Ribasim.NodeID}()
push!(node_ids[1], Ribasim.NodeID(1))
push!(node_ids[1], Ribasim.NodeID(2))
push!(node_ids[1], Ribasim.NodeID(3))
node_ids[2] = Set{Ribasim.NodeID}()
push!(node_ids[2], Ribasim.NodeID(4))
push!(node_ids[2], Ribasim.NodeID(5))
push!(node_ids[2], Ribasim.NodeID(6))

graph[Ribasim.NodeID(1)] = Ribasim.NodeMetadata(Symbol(:delft), 1)
graph[Ribasim.NodeID(2)] = Ribasim.NodeMetadata(Symbol(:denhaag), 1)
graph[Ribasim.NodeID(3)] = Ribasim.NodeMetadata(Symbol(:rdam), 1)
graph[Ribasim.NodeID(4)] = Ribasim.NodeMetadata(Symbol(:adam), 2)
graph[Ribasim.NodeID(5)] = Ribasim.NodeMetadata(Symbol(:utrecht), 2)
graph[Ribasim.NodeID(6)] = Ribasim.NodeMetadata(Symbol(:leiden), 2)

graph[Ribasim.NodeID(1), Ribasim.NodeID(2)] = :yes
graph[Ribasim.NodeID(1), Ribasim.NodeID(3)] = :yes
graph[4, 5] = :yes

logger = TestLogger()

with_logger(logger) do
errors = Ribasim.incomplete_subnetwork(graph, node_ids)
@test errors == true
end

@test length(logger.logs) == 1
@test logger.logs[1].level == Error
@test logger.logs[1].message == "All nodes in subnetwork 2 should be connected"
end
24 changes: 24 additions & 0 deletions core/test/validation_test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,27 @@ end
@test logger.logs[2].message ==
"Basin / subgrid_level subgrid_id 1 has repeated element levels, this cannot be interpolated."
end

@testitem "negative demand" begin
using Logging
using DataInterpolations: LinearInterpolation
logger = TestLogger()

with_logger(logger) do
@test_throws "Invalid demand" Ribasim.User(
[Ribasim.NodeID(1)],
[true],
[[LinearInterpolation([-5.0, -5.0], [-1.8, 1.8])]],
[0.0, -0.0],
[0.9],
[0.9],
[1],
[],
)
end

@test length(logger.logs) == 1
@test logger.logs[1].level == Error
@test logger.logs[1].message ==
"Demand of user node #1 with priority 1 should be non-negative"
end
Loading