diff --git a/contribute/addnode.html b/contribute/addnode.html index fd1b5e1ab..3a036500d 100644 --- a/contribute/addnode.html +++ b/contribute/addnode.html @@ -305,6 +305,11 @@

node_id::Vector{NodeID} # Other fields end +

Another abstract type which subtypes from AbstractParameterNode is called AbstractDemandNode. For creating new node type used in allocation, define a struct:

+
struct NewNodeType <: AbstractDemandNode
+    node_id::Vector{NodeID}
+    # Other fields
+end

These fields do not have to correspond 1:1 with the input tables (see below). The vector with all node IDs that are of the new type in a given model is a mandatory field. Now you can:

-
class NewNodeType(NodeModel):
-    static: TableModel[NewNodeTypeStaticSchema] = Field(
-        default_factory=TableModel[NewNodeTypeStaticSchema],
-        json_schema_extra={"sort_keys": ["node_id"]},
-    )
+
class NewNodeType(NodeModel):
+    static: TableModel[NewNodeTypeStaticSchema] = Field(
+        default_factory=TableModel[NewNodeTypeStaticSchema],
+        json_schema_extra={"sort_keys": ["node_id"]},
+    )

In python/ribasim/ribasim/__init__.py add

-
class NewNodeTypeStatic:
-    input_type = "NewNodeType / static"
-    geometry_type = "No Geometry"
-    attributes = [
-        QgsField("node_id", QVariant.Int)
-        # Other fields for properties of this node
-    ]
+
class NewNodeTypeStatic:
+    input_type = "NewNodeType / static"
+    geometry_type = "No Geometry"
+    attributes = [
+        QgsField("node_id", QVariant.Int)
+        # Other fields for properties of this node
+    ]

4 Validation

The new node type might have associated restrictions for a model with the new node type so that it behaves properly. Basic node ID and node type validation happens in Model.validate_model in python/ribasim/ribasim/model.py, which automatically considers all node types in the node_types module.

Connectivity validation happens in valid_edges and valid_n_flow_neighbors in core/src/solve.jl. Connectivity rules are specified in core/src/validation.jl. Allowed upstream and downstream neighbor types for new_node_type (the snake case version of NewNodeType) are specified as follows:

-
# set allowed downstream types
-neighbortypes(::Val{:new_node_type}) = Set((:basin,))
-# add your newnodetype as acceptable downstream connection of other types
-neighbortypes(::Val{:pump}) = Set((:basin, :new_node_type))
+
# set allowed downstream types
+neighbortypes(::Val{:new_node_type}) = Set((:basin,))
+# add your newnodetype as acceptable downstream connection of other types
+neighbortypes(::Val{:pump}) = Set((:basin, :new_node_type))

The minimum and maximum allowed number of inneighbors and outneighbors for NewNodeType are specified as follows:

-
# Allowed number of flow/control inneighbors and outneighbors per node type
-struct n_neighbor_bounds
-    in_min::Int
-    in_max::Int
-    out_min::Int
-    out_max::Int
-end
-
-n_neighbor_bounds_flow(::Val{:NewNodeType}) =
-    n_neighbor_bounds(0, 0, 1, typemax(Int))
-
-n_neighbor_bounds_control(::Val{:NewNodeType}) =
-    n_neighbor_bounds(0, 1, 0, 0)
+
# Allowed number of flow/control inneighbors and outneighbors per node type
+struct n_neighbor_bounds
+    in_min::Int
+    in_max::Int
+    out_min::Int
+    out_max::Int
+end
+
+n_neighbor_bounds_flow(::Val{:NewNodeType}) =
+    n_neighbor_bounds(0, 0, 1, typemax(Int))
+
+n_neighbor_bounds_control(::Val{:NewNodeType}) =
+    n_neighbor_bounds(0, 1, 0, 0)

Here typemax(Int) effectively means unbounded.

5 Tests

Models for the julia tests are generated by running pixi run generate-testmodels, which uses model definitions from the ribasim_testmodels package, see here. These models should also be updated to contain the new node type. Note that certain tests must be updated accordingly when the models used for certain tests are updated, e.g. the final state of the models in core/test/basin.jl. The following function is used to format the array of this final state.

-
reprf(x) = repr(convert(Vector{Float32}, x))
+
reprf(x) = repr(convert(Vector{Float32}, x))

See here for monitoring of Python test coverage.

If the new node type introduces new (somewhat) complex behaviour, a good test is to construct a minimal model containing the new node type in python/ribasim_testmodels/ribasim_testmodels/equations.py and compare the simulation result to the analytical solution (if possible) in core/test/equations.jl.

diff --git a/core/allocation.html b/core/allocation.html index f81009a5e..99e232c45 100644 --- a/core/allocation.html +++ b/core/allocation.html @@ -565,7 +565,7 @@

4.4 Example

The following is an example of an optimization problem for the example shown here:

-
+
Code
using Ribasim
@@ -588,41 +588,41 @@ 

println(p.allocation.allocation_models[1].problem)

-
Min F[(Basin #2, UserDemand #3)]² + F[(Basin #12, UserDemand #13)]² + F[(Basin #5, UserDemand #6)]²
+
Min F[(Basin #2, UserDemand #3)]² + F[(Basin #5, UserDemand #6)]² + F[(Basin #12, UserDemand #13)]²
 Subject to
- F[(UserDemand #3, Basin #2)] ≥ 0
- F[(Basin #2, UserDemand #3)] ≥ 0
- F[(LinearResistance #4, Basin #5)] ≥ 0
- F[(Basin #5, LinearResistance #4)] ≥ 0
- F[(Basin #12, UserDemand #13)] ≥ 0
  F[(Basin #2, LinearResistance #4)] ≥ 0
  F[(LinearResistance #4, Basin #2)] ≥ 0
- F[(UserDemand #13, Terminal #10)] ≥ 0
- F[(FlowBoundary #1, Basin #2)] ≥ 0
- F[(Basin #5, TabulatedRatingCurve #7)] ≥ 0
  F[(FractionalFlow #8, Terminal #10)] ≥ 0
+ F[(TabulatedRatingCurve #7, FractionalFlow #9)] ≥ 0
+ F[(TabulatedRatingCurve #7, FractionalFlow #8)] ≥ 0
+ F[(Basin #2, UserDemand #3)] ≥ 0
+ F[(UserDemand #3, Basin #2)] ≥ 0
+ F[(FlowBoundary #1, Basin #2)] ≥ 0
  F[(UserDemand #6, Basin #5)] ≥ 0
+ F[(LinearResistance #4, Basin #5)] ≥ 0
+ F[(Basin #5, LinearResistance #4)] ≥ 0
  F[(FractionalFlow #9, Basin #12)] ≥ 0
  F[(Basin #5, UserDemand #6)] ≥ 0
- F[(TabulatedRatingCurve #7, FractionalFlow #8)] ≥ 0
- F[(TabulatedRatingCurve #7, FractionalFlow #9)] ≥ 0
+ F[(Basin #5, TabulatedRatingCurve #7)] ≥ 0
+ F[(Basin #12, UserDemand #13)] ≥ 0
+ F[(UserDemand #13, Terminal #10)] ≥ 0
  F_flow_buffer_in[TabulatedRatingCurve #7] ≥ 0
  F_flow_buffer_out[TabulatedRatingCurve #7] ≥ 0
  source[(FlowBoundary #1, Basin #2)] : F[(FlowBoundary #1, Basin #2)] ≤ 172800
- source_user[UserDemand #13] : F[(UserDemand #13, Terminal #10)] ≤ 0
- source_user[UserDemand #3] : F[(UserDemand #3, Basin #2)] ≤ 0
  source_user[UserDemand #6] : F[(UserDemand #6, Basin #5)] ≤ 0
- fractional_flow[(TabulatedRatingCurve #7, FractionalFlow #8)] : -0.6 F[(Basin #5, TabulatedRatingCurve #7)] + F[(TabulatedRatingCurve #7, FractionalFlow #8)] ≤ 0
- fractional_flow[(TabulatedRatingCurve #7, FractionalFlow #9)] : -0.4 F[(Basin #5, TabulatedRatingCurve #7)] + F[(TabulatedRatingCurve #7, FractionalFlow #9)] ≤ 0
+ source_user[UserDemand #3] : F[(UserDemand #3, Basin #2)] ≤ 0
+ source_user[UserDemand #13] : F[(UserDemand #13, Terminal #10)] ≤ 0
+ fractional_flow[(TabulatedRatingCurve #7, FractionalFlow #8)] : F[(TabulatedRatingCurve #7, FractionalFlow #8)] - 0.6 F[(Basin #5, TabulatedRatingCurve #7)] ≤ 0
+ fractional_flow[(TabulatedRatingCurve #7, FractionalFlow #9)] : F[(TabulatedRatingCurve #7, FractionalFlow #9)] - 0.4 F[(Basin #5, TabulatedRatingCurve #7)] ≤ 0
  flow_buffer_outflow[TabulatedRatingCurve #7] : F_flow_buffer_out[TabulatedRatingCurve #7] ≤ 0
- flow_conservation[FractionalFlow #9] : -F[(FractionalFlow #9, Basin #12)] + F[(TabulatedRatingCurve #7, FractionalFlow #9)] = 0
- flow_conservation[Basin #12] : -F[(Basin #12, UserDemand #13)] + F[(FractionalFlow #9, Basin #12)] = 0
- flow_conservation[TabulatedRatingCurve #7] : F[(Basin #5, TabulatedRatingCurve #7)] - F[(TabulatedRatingCurve #7, FractionalFlow #8)] - F[(TabulatedRatingCurve #7, FractionalFlow #9)] - F_flow_buffer_in[TabulatedRatingCurve #7] + F_flow_buffer_out[TabulatedRatingCurve #7] = 0
- flow_conservation[Terminal #10] : F[(UserDemand #13, Terminal #10)] + F[(FractionalFlow #8, Terminal #10)] = 0
- flow_conservation[Basin #2] : F[(UserDemand #3, Basin #2)] - F[(Basin #2, UserDemand #3)] - F[(Basin #2, LinearResistance #4)] + F[(LinearResistance #4, Basin #2)] + F[(FlowBoundary #1, Basin #2)] = 0
  flow_conservation[FractionalFlow #8] : -F[(FractionalFlow #8, Terminal #10)] + F[(TabulatedRatingCurve #7, FractionalFlow #8)] = 0
- flow_conservation[LinearResistance #4] : -F[(LinearResistance #4, Basin #5)] + F[(Basin #5, LinearResistance #4)] + F[(Basin #2, LinearResistance #4)] - F[(LinearResistance #4, Basin #2)] = 0
- flow_conservation[Basin #5] : F[(LinearResistance #4, Basin #5)] - F[(Basin #5, LinearResistance #4)] - F[(Basin #5, TabulatedRatingCurve #7)] + F[(UserDemand #6, Basin #5)] - F[(Basin #5, UserDemand #6)] = 0
+ flow_conservation[Terminal #10] : F[(FractionalFlow #8, Terminal #10)] + F[(UserDemand #13, Terminal #10)] = 0
+ flow_conservation[Basin #12] : F[(FractionalFlow #9, Basin #12)] - F[(Basin #12, UserDemand #13)] = 0
+ flow_conservation[FractionalFlow #9] : F[(TabulatedRatingCurve #7, FractionalFlow #9)] - F[(FractionalFlow #9, Basin #12)] = 0
+ flow_conservation[TabulatedRatingCurve #7] : -F[(TabulatedRatingCurve #7, FractionalFlow #9)] - F[(TabulatedRatingCurve #7, FractionalFlow #8)] + F[(Basin #5, TabulatedRatingCurve #7)] - F_flow_buffer_in[TabulatedRatingCurve #7] + F_flow_buffer_out[TabulatedRatingCurve #7] = 0
+ flow_conservation[Basin #5] : F[(UserDemand #6, Basin #5)] + F[(LinearResistance #4, Basin #5)] - F[(Basin #5, LinearResistance #4)] - F[(Basin #5, UserDemand #6)] - F[(Basin #5, TabulatedRatingCurve #7)] = 0
+ flow_conservation[LinearResistance #4] : F[(Basin #2, LinearResistance #4)] - F[(LinearResistance #4, Basin #2)] - F[(LinearResistance #4, Basin #5)] + F[(Basin #5, LinearResistance #4)] = 0
+ flow_conservation[Basin #2] : -F[(Basin #2, LinearResistance #4)] + F[(LinearResistance #4, Basin #2)] - F[(Basin #2, UserDemand #3)] + F[(UserDemand #3, Basin #2)] + F[(FlowBoundary #1, Basin #2)] = 0
 
diff --git a/core/equations.html b/core/equations.html index c0a334f27..f903d750a 100644 --- a/core/equations.html +++ b/core/equations.html @@ -427,7 +427,7 @@

Here \(p > 0\) is the threshold value which determines the interval \([0,p]\) of the smooth transition between \(0\) and \(1\), see the plot below.

-
+
Code
import numpy as np
diff --git a/core/validation.html b/core/validation.html
index 022a20a49..7ac265faf 100644
--- a/core/validation.html
+++ b/core/validation.html
@@ -262,7 +262,7 @@ 

Validation

1 Connectivity

In the table below, each column shows which node types are allowed to be downstream (or ‘down-control’) of the node type at the top of the column.

-
+
Code
using Ribasim
@@ -612,7 +612,7 @@ 

1 Connectivity

2 Neighbor amounts

The table below shows for each node type between which bounds the amount of in- and outneighbors must be, for both flow and control edges.

-
+
Code
flow_in_min = Vector{String}()
diff --git a/python/examples_files/figure-html/cell-71-output-1.png b/python/examples_files/figure-html/cell-71-output-1.png
index 2e273a56f..42dfff946 100644
Binary files a/python/examples_files/figure-html/cell-71-output-1.png and b/python/examples_files/figure-html/cell-71-output-1.png differ
diff --git a/python/test-models.html b/python/test-models.html
index a6746f1fe..ae0040236 100644
--- a/python/test-models.html
+++ b/python/test-models.html
@@ -227,7 +227,7 @@ 

Test models

Ribasim developers use the following models in their testbench and in order to test new features.

-
+
Code
import ribasim_testmodels
diff --git a/search.json b/search.json
index d188e2f5a..1be9a8fd3 100644
--- a/search.json
+++ b/search.json
@@ -793,7 +793,7 @@
     "href": "contribute/addnode.html#parameters",
     "title": "Adding node types",
     "section": "1.1 Parameters",
-    "text": "1.1 Parameters\nThe parameters object (defined in parameter.jl) passed to the ODE solver must be made aware of the new node type. Therefore define a struct in parameter.jl which holds the data for each node of the new node type:\nstruct NewNodeType <: AbstractParameterNode\n    node_id::Vector{NodeID}\n    # Other fields\nend\nThese fields do not have to correspond 1:1 with the input tables (see below). The vector with all node IDs that are of the new type in a given model is a mandatory field. Now you can:\n\nAdd new_node_type::NewNodeType to the Parameters object;\nAdd new_node_type = NewNodeType(db,config) to the function Parameters in read.jl and add new_node_type at the proper location in the Parameters constructor call.",
+    "text": "1.1 Parameters\nThe parameters object (defined in parameter.jl) passed to the ODE solver must be made aware of the new node type. Therefore define a struct in parameter.jl which holds the data for each node of the new node type:\nstruct NewNodeType <: AbstractParameterNode\n    node_id::Vector{NodeID}\n    # Other fields\nend\nAnother abstract type which subtypes from AbstractParameterNode is called AbstractDemandNode. For creating new node type used in allocation, define a struct:\nstruct NewNodeType <: AbstractDemandNode\n    node_id::Vector{NodeID}\n    # Other fields\nend\nThese fields do not have to correspond 1:1 with the input tables (see below). The vector with all node IDs that are of the new type in a given model is a mandatory field. Now you can:\n\nAdd new_node_type::NewNodeType to the Parameters object;\nAdd new_node_type = NewNodeType(db,config) to the function Parameters in read.jl and add new_node_type at the proper location in the Parameters constructor call.",
     "crumbs": [
       "Contributing",
       "Adding node types"
@@ -1343,7 +1343,7 @@
     "href": "core/allocation.html#example",
     "title": "Allocation",
     "section": "4.4 Example",
-    "text": "4.4 Example\nThe following is an example of an optimization problem for the example shown here:\n\n\nCode\nusing Ribasim\nusing Ribasim: NodeID\nusing SQLite\nusing ComponentArrays: ComponentVector\n\ntoml_path = normpath(@__DIR__, \"../../generated_testmodels/allocation_example/ribasim.toml\")\np = Ribasim.Model(toml_path).integrator.p\nu = ComponentVector(; storage = zeros(length(p.basin.node_id)))\n\nallocation_model = p.allocation.allocation_models[1]\nt = 0.0\npriority_idx = 1\n\nRibasim.set_flow!(p.graph, NodeID(:FlowBoundary, 1), NodeID(:Basin, 2), 1.0)\nRibasim.set_objective_priority!(allocation_model, p, u, t, priority_idx)\nRibasim.set_initial_values!(allocation_model, p, u, t)\n\nprintln(p.allocation.allocation_models[1].problem)\n\n\nMin F[(Basin #2, UserDemand #3)]² + F[(Basin #12, UserDemand #13)]² + F[(Basin #5, UserDemand #6)]²\nSubject to\n F[(UserDemand #3, Basin #2)] ≥ 0\n F[(Basin #2, UserDemand #3)] ≥ 0\n F[(LinearResistance #4, Basin #5)] ≥ 0\n F[(Basin #5, LinearResistance #4)] ≥ 0\n F[(Basin #12, UserDemand #13)] ≥ 0\n F[(Basin #2, LinearResistance #4)] ≥ 0\n F[(LinearResistance #4, Basin #2)] ≥ 0\n F[(UserDemand #13, Terminal #10)] ≥ 0\n F[(FlowBoundary #1, Basin #2)] ≥ 0\n F[(Basin #5, TabulatedRatingCurve #7)] ≥ 0\n F[(FractionalFlow #8, Terminal #10)] ≥ 0\n F[(UserDemand #6, Basin #5)] ≥ 0\n F[(FractionalFlow #9, Basin #12)] ≥ 0\n F[(Basin #5, UserDemand #6)] ≥ 0\n F[(TabulatedRatingCurve #7, FractionalFlow #8)] ≥ 0\n F[(TabulatedRatingCurve #7, FractionalFlow #9)] ≥ 0\n F_flow_buffer_in[TabulatedRatingCurve #7] ≥ 0\n F_flow_buffer_out[TabulatedRatingCurve #7] ≥ 0\n source[(FlowBoundary #1, Basin #2)] : F[(FlowBoundary #1, Basin #2)] ≤ 172800\n source_user[UserDemand #13] : F[(UserDemand #13, Terminal #10)] ≤ 0\n source_user[UserDemand #3] : F[(UserDemand #3, Basin #2)] ≤ 0\n source_user[UserDemand #6] : F[(UserDemand #6, Basin #5)] ≤ 0\n fractional_flow[(TabulatedRatingCurve #7, FractionalFlow #8)] : -0.6 F[(Basin #5, TabulatedRatingCurve #7)] + F[(TabulatedRatingCurve #7, FractionalFlow #8)] ≤ 0\n fractional_flow[(TabulatedRatingCurve #7, FractionalFlow #9)] : -0.4 F[(Basin #5, TabulatedRatingCurve #7)] + F[(TabulatedRatingCurve #7, FractionalFlow #9)] ≤ 0\n flow_buffer_outflow[TabulatedRatingCurve #7] : F_flow_buffer_out[TabulatedRatingCurve #7] ≤ 0\n flow_conservation[FractionalFlow #9] : -F[(FractionalFlow #9, Basin #12)] + F[(TabulatedRatingCurve #7, FractionalFlow #9)] = 0\n flow_conservation[Basin #12] : -F[(Basin #12, UserDemand #13)] + F[(FractionalFlow #9, Basin #12)] = 0\n flow_conservation[TabulatedRatingCurve #7] : F[(Basin #5, TabulatedRatingCurve #7)] - F[(TabulatedRatingCurve #7, FractionalFlow #8)] - F[(TabulatedRatingCurve #7, FractionalFlow #9)] - F_flow_buffer_in[TabulatedRatingCurve #7] + F_flow_buffer_out[TabulatedRatingCurve #7] = 0\n flow_conservation[Terminal #10] : F[(UserDemand #13, Terminal #10)] + F[(FractionalFlow #8, Terminal #10)] = 0\n flow_conservation[Basin #2] : F[(UserDemand #3, Basin #2)] - F[(Basin #2, UserDemand #3)] - F[(Basin #2, LinearResistance #4)] + F[(LinearResistance #4, Basin #2)] + F[(FlowBoundary #1, Basin #2)] = 0\n flow_conservation[FractionalFlow #8] : -F[(FractionalFlow #8, Terminal #10)] + F[(TabulatedRatingCurve #7, FractionalFlow #8)] = 0\n flow_conservation[LinearResistance #4] : -F[(LinearResistance #4, Basin #5)] + F[(Basin #5, LinearResistance #4)] + F[(Basin #2, LinearResistance #4)] - F[(LinearResistance #4, Basin #2)] = 0\n flow_conservation[Basin #5] : F[(LinearResistance #4, Basin #5)] - F[(Basin #5, LinearResistance #4)] - F[(Basin #5, TabulatedRatingCurve #7)] + F[(UserDemand #6, Basin #5)] - F[(Basin #5, UserDemand #6)] = 0",
+    "text": "4.4 Example\nThe following is an example of an optimization problem for the example shown here:\n\n\nCode\nusing Ribasim\nusing Ribasim: NodeID\nusing SQLite\nusing ComponentArrays: ComponentVector\n\ntoml_path = normpath(@__DIR__, \"../../generated_testmodels/allocation_example/ribasim.toml\")\np = Ribasim.Model(toml_path).integrator.p\nu = ComponentVector(; storage = zeros(length(p.basin.node_id)))\n\nallocation_model = p.allocation.allocation_models[1]\nt = 0.0\npriority_idx = 1\n\nRibasim.set_flow!(p.graph, NodeID(:FlowBoundary, 1), NodeID(:Basin, 2), 1.0)\nRibasim.set_objective_priority!(allocation_model, p, u, t, priority_idx)\nRibasim.set_initial_values!(allocation_model, p, u, t)\n\nprintln(p.allocation.allocation_models[1].problem)\n\n\nMin F[(Basin #2, UserDemand #3)]² + F[(Basin #5, UserDemand #6)]² + F[(Basin #12, UserDemand #13)]²\nSubject to\n F[(Basin #2, LinearResistance #4)] ≥ 0\n F[(LinearResistance #4, Basin #2)] ≥ 0\n F[(FractionalFlow #8, Terminal #10)] ≥ 0\n F[(TabulatedRatingCurve #7, FractionalFlow #9)] ≥ 0\n F[(TabulatedRatingCurve #7, FractionalFlow #8)] ≥ 0\n F[(Basin #2, UserDemand #3)] ≥ 0\n F[(UserDemand #3, Basin #2)] ≥ 0\n F[(FlowBoundary #1, Basin #2)] ≥ 0\n F[(UserDemand #6, Basin #5)] ≥ 0\n F[(LinearResistance #4, Basin #5)] ≥ 0\n F[(Basin #5, LinearResistance #4)] ≥ 0\n F[(FractionalFlow #9, Basin #12)] ≥ 0\n F[(Basin #5, UserDemand #6)] ≥ 0\n F[(Basin #5, TabulatedRatingCurve #7)] ≥ 0\n F[(Basin #12, UserDemand #13)] ≥ 0\n F[(UserDemand #13, Terminal #10)] ≥ 0\n F_flow_buffer_in[TabulatedRatingCurve #7] ≥ 0\n F_flow_buffer_out[TabulatedRatingCurve #7] ≥ 0\n source[(FlowBoundary #1, Basin #2)] : F[(FlowBoundary #1, Basin #2)] ≤ 172800\n source_user[UserDemand #6] : F[(UserDemand #6, Basin #5)] ≤ 0\n source_user[UserDemand #3] : F[(UserDemand #3, Basin #2)] ≤ 0\n source_user[UserDemand #13] : F[(UserDemand #13, Terminal #10)] ≤ 0\n fractional_flow[(TabulatedRatingCurve #7, FractionalFlow #8)] : F[(TabulatedRatingCurve #7, FractionalFlow #8)] - 0.6 F[(Basin #5, TabulatedRatingCurve #7)] ≤ 0\n fractional_flow[(TabulatedRatingCurve #7, FractionalFlow #9)] : F[(TabulatedRatingCurve #7, FractionalFlow #9)] - 0.4 F[(Basin #5, TabulatedRatingCurve #7)] ≤ 0\n flow_buffer_outflow[TabulatedRatingCurve #7] : F_flow_buffer_out[TabulatedRatingCurve #7] ≤ 0\n flow_conservation[FractionalFlow #8] : -F[(FractionalFlow #8, Terminal #10)] + F[(TabulatedRatingCurve #7, FractionalFlow #8)] = 0\n flow_conservation[Terminal #10] : F[(FractionalFlow #8, Terminal #10)] + F[(UserDemand #13, Terminal #10)] = 0\n flow_conservation[Basin #12] : F[(FractionalFlow #9, Basin #12)] - F[(Basin #12, UserDemand #13)] = 0\n flow_conservation[FractionalFlow #9] : F[(TabulatedRatingCurve #7, FractionalFlow #9)] - F[(FractionalFlow #9, Basin #12)] = 0\n flow_conservation[TabulatedRatingCurve #7] : -F[(TabulatedRatingCurve #7, FractionalFlow #9)] - F[(TabulatedRatingCurve #7, FractionalFlow #8)] + F[(Basin #5, TabulatedRatingCurve #7)] - F_flow_buffer_in[TabulatedRatingCurve #7] + F_flow_buffer_out[TabulatedRatingCurve #7] = 0\n flow_conservation[Basin #5] : F[(UserDemand #6, Basin #5)] + F[(LinearResistance #4, Basin #5)] - F[(Basin #5, LinearResistance #4)] - F[(Basin #5, UserDemand #6)] - F[(Basin #5, TabulatedRatingCurve #7)] = 0\n flow_conservation[LinearResistance #4] : F[(Basin #2, LinearResistance #4)] - F[(LinearResistance #4, Basin #2)] - F[(LinearResistance #4, Basin #5)] + F[(Basin #5, LinearResistance #4)] = 0\n flow_conservation[Basin #2] : -F[(Basin #2, LinearResistance #4)] + F[(LinearResistance #4, Basin #2)] - F[(Basin #2, UserDemand #3)] + F[(UserDemand #3, Basin #2)] + F[(FlowBoundary #1, Basin #2)] = 0",
     "crumbs": [
       "Julia core",
       "Allocation"