From d1e98cb7c1be890309d95fda99a023db0080dea6 Mon Sep 17 00:00:00 2001
From: Quarto GHA Workflow Runner # Return the bottom elevation of the basin with index i, or nothing if it doesn’t exist # Get the bottom on both ends of a node. If only one has a bottom, use that for both. # Create the different callbacks that are used to store output and feed the simulation with new data. The different callbacks are combined to a CallbackSet that goes to the integrator. Returns the CallbackSet and the SavedValues for flow. # Return a directed graph, and a mapping from source and target nodes to edge fid. # Read the Basin / profile table and return all area and level and computed storage values # Convert a Real that represents the seconds passed since the simulation start to the nearest DateTime. This is used to convert between the solver’s inner float time, and the calendar. # Change parameters based on the control logic. # An downcrossing means that a condition (always greater than) becomes false. # An upcrossing means that a condition (always greater than) becomes true. # Listens for changes in condition truths. # Replace the truth states in the logic mapping which contain wildcards with all possible explicit truth states. # For an element # Find the index of element x in a sorted collection a. Returns the index of x if it exists, or nothing if it doesn’t. If x occurs more than once, throw an error. # Smoothly let the evaporation flux go to 0 when at small water depths Currently at less than 0.1 m. # Directed graph: outflow is positive! # Conservation of energy for two basins, a and b:1 API Reference
1.1 Functions
Ribasim.basin_bottom
— Method.Ribasim.basin_bottoms
— Method.Ribasim.create_callbacks
— Method.Ribasim.create_graph
— Method.Ribasim.create_storage_tables
— Method.Ribasim.datetime_since
— Method.datetime_since(t::Real, t0::DateTime)::DateTime
Ribasim.discrete_control_affect!
— Method.Ribasim.discrete_control_affect_downcrossing!
— Method.Ribasim.discrete_control_affect_upcrossing!
— Method.Ribasim.discrete_control_condition
— Method.Ribasim.expand_logic_mapping
— Method.Ribasim.findlastgroup
— Method.id
and a vector of elements ids
, get the range of indices of the last consecutive block of id
. Returns the empty range 1:0
if id
is not in ids
.# 1 2 3 4 5 6 7 8 9
findlastgroup(2, [5,4,2,2,5,2,2,2,1])
Ribasim.# output
6:8
Ribasim.findsorted
— Method.Ribasim.formulate!
— Method.Ribasim.formulate_flow!
— Method.Ribasim.formulate_flow!
— Method.
@@ -311,121 +311,121 @@ h_a + v_a^2 / (2 * g) = h_b + v_b^2 / (2 * g) + S_f * L + C / 2 * g * (v_b^2 - v_a^2)
source
# Ribasim.formulate_flow!
— Method.
Directed graph: outflow is positive!
- +# Ribasim.get_area_and_level
— Method.
Compute the area and level of a basin given its storage. Also returns darea/dlevel as it is needed for the Jacobian.
- +# Ribasim.get_compressor
— Method.
Get the compressor based on the Output
- +# Ribasim.get_fractional_flow_connected_basins
— Method.
Get the node type specific indices of the fractional flows and basins, that are consecutively connected to a node of given id.
- +# Ribasim.get_jac_prototype
— Method.
Get a sparse matrix whose sparsity matches the sparsity of the Jacobian of the ODE problem. All nodes are taken into consideration, also the ones that are inactive.
In Ribasim the Jacobian is typically sparse because each state only depends on a small number of other states.
Note: the name ‘prototype’ does not mean this code is a prototype, it comes from the naming convention of this sparsity structure in the differentialequations.jl docs.
- +# Ribasim.get_level
— Method.
Get the current water level of a node ID. The ID can belong to either a Basin or a LevelBoundary.
- +# Ribasim.get_scalar_interpolation
— Method.
Linear interpolation of a scalar with constant extrapolation.
- +# Ribasim.get_storage_from_level
— Method.
Get the storage of a basin from its level.
- +# Ribasim.get_storages_and_levels
— Method.
Get the storage and level of all basins as matrices of nbasin × ntime
- +# Ribasim.get_storages_from_levels
— Method.
Compute the storages of the basins based on the water level of the basins.
- +# Ribasim.get_tstops
— Method.
From an iterable of DateTimes, find the times the solver needs to stop
- +# Ribasim.get_value
— Method.
Get a value for a condition. Currently supports getting levels from basins and flows from flow boundaries.
- +# Ribasim.id_index
— Method.
Get the index of an ID in a set of indices.
- +# Ribasim.input_path
— Method.
Construct a path relative to both the TOML directory and the optional input_dir
# Ribasim.load_data
— Method.
load_data(db::DB, config::Config, nodetype::Symbol, kind::Symbol)::Union{Table, Query, Nothing}
Load data from Arrow files if available, otherwise the GeoPackage. Returns either an Arrow.Table
, SQLite.Query
or nothing
if the data is not present.
# Ribasim.load_structvector
— Method.
load_structvector(db::DB, config::Config, ::Type{T})::StructVector{T}
Load data from Arrow files if available, otherwise the GeoPackage. Always returns a StructVector of the given struct type T, which is empty if the table is not found. This function validates the schema, and enforces the required sort order.
- +# Ribasim.nodefields
— Method.
Get all node fieldnames of the parameter object.
- +# Ribasim.nodetype
— Method.
From a SchemaVersion(“ribasim.flowboundary.static”, 1) return (:FlowBoundary, :static)
- +# Ribasim.output_path
— Method.
Construct a path relative to both the TOML directory and the optional output_dir
# Ribasim.parse_static_and_time
— Method.
Process the data in the static and time tables for a given node type. The ‘defaults’ named tuple dictates how missing data is filled in. ‘time_interpolatables’ is a vector of Symbols of parameter names for which a time interpolation (linear) object must be constructed. The control mapping for DiscreteControl is also constructed in this function. This function currently does not support node states that are defined by more than one row in a table, as is the case for TabulatedRatingCurve.
- +# Ribasim.profile_storage
— Method.
Calculate a profile storage by integrating the areas over the levels
- +# Ribasim.qh_interpolation
— Method.
From a table with columns nodeid, discharge (Q) and level (h), create a LinearInterpolation from level to discharge for a given nodeid.
- +# Ribasim.reduction_factor
— Method.
Function that goes smoothly from 0 to 1 in the interval [0,threshold], and is constant outside this interval.
- +# Ribasim.save_flow
— Method.
Copy the current flow to the SavedValues
- +# Ribasim.scalar_interpolation_derivative
— Method.
Derivative of scalar interpolation.
- +# Ribasim.seconds_since
— Method.
seconds_since(t::DateTime, t0::DateTime)::Float64
Convert a DateTime to a float that is the number of seconds since the start of the simulation. This is used to convert between the solver’s inner float time, and the calendar.
- +# Ribasim.set_current_value!
— Method.
From a timeseries table time
, load the most recent applicable data into table
. table
must be a NamedTuple of vectors with all variables that must be loaded. The most recent applicable data is non-NaN data for a given ID that is on or before t
.
# Ribasim.set_static_value!
— Method.
Load data from a source table static
into a destination table
. Data is matched based on the node_id, which is sorted.
# Ribasim.set_table_row!
— Method.
Update table
at row index i
, with the values of a given row. table
must be a NamedTuple of vectors with all variables that must be loaded. The row must contain all the column names that are present in the table. If a value is NaN, it is not set.
# Ribasim.sorted_table!
— Method.
Depending on if a table can be sorted, either sort it or assert that it is sorted.
Tables loaded from GeoPackage into memory can be sorted. Tables loaded from Arrow files are memory mapped and can therefore not be sorted.
- +# Ribasim.update_basin
— Method.
Load updates from ‘Basin / time’ into the parameters
- +# Ribasim.update_jac_prototype!
— Method.
Method for nodes that do not contribute to the Jacobian
- +# Ribasim.update_jac_prototype!
— Method.
The controlled basin affects itself and the basins upstream and downstream of the controlled pump affect eachother if there is a basin upstream of the pump. The state for the integral term and the controlled basin affect eachother, and the same for the integral state and the basin upstream of the pump if it is indeed a basin.
- +# Ribasim.update_jac_prototype!
— Method.
If both the unique node upstream and the unique node downstream of these nodes are basins, then these directly depend on eachother and affect the Jacobian 2x Basins always depend on themselves.
- +# Ribasim.update_jac_prototype!
— Method.
If both the unique node upstream and the nodes down stream (or one node further if a fractional flow is in between) are basins, then the downstream basin depends on the upstream basin(s) and affect the Jacobian as many times as there are downstream basins Upstream basins always depend on themselves.
- +# Ribasim.update_tabulated_rating_curve!
— Method.
Load updates from ‘TabulatedRatingCurve / time’ into the parameters
- +# Ribasim.valid_discrete_control
— Method.
Check:
# Ribasim.valid_edge_types
— Method.
Check that only supported edge types are declared.
- +# Ribasim.valid_edges
— Method.
Test for each node given its node type whether the nodes that
are downstream (‘down-edge’) of this node are of an allowed type
- +# Ribasim.valid_flow_rates
— Method.
Test whether static or discrete controlled flow rates are indeed non-negative.
- +# Ribasim.valid_fractional_flow
— Method.
Check that nodes that have fractional flow outneighbors do not have any other type of outneighbor, that the fractions leaving a node add up to ≈1 and that the fractions are non-negative.
- +# Ribasim.valid_n_neighbors
— Method.
Test for each node given its node type whether it has an allowed number of flow/control inneighbors and outneighbors
- +# Ribasim.valid_profiles
— Method.
Check whether the profile data has no repeats in the levels and the areas start positive.
- +# Ribasim.water_balance!
— Method.
The right hand side function of the system of ODEs set up by Ribasim.
- +# Ribasim.config.algorithm
— Method.
Create an OrdinaryDiffEqAlgorithm from solver config
- +# Ribasim.config.snake_case
— Method.
Convert a string from CamelCase to snake_case.
- + @@ -476,24 +476,24 @@# Ribasim.Connectivity
— Type.
Store the connectivity information
graphflow, graphcontrol: directed graph with vertices equal to ids flow: store the flow on every flow edge edgeidsflow, edgeidscontrol: get the external edge id from (src, dst) edgeconnectiontypeflow, edgeconnectiontypescontrol: get (srcnodetype, dstnodetype) from edge id
if autodiff T = DiffCache{SparseArrays.SparseMatrixCSC{Float64, Int64}, Vector{Float64}} else T = SparseMatrixCSC{Float64, Int} end
- +# Ribasim.DiscreteControl
— Type.
nodeid: node ID of the DiscreteControl node; these are not unique but repeated by the amount of conditions of this DiscreteControl node listenfeatureid: the ID of the node/edge being condition on variable: the name of the variable in the condition greaterthan: The threshold value in the condition conditionvalue: The current value of each condition controlstate: Dictionary: node ID => (control state, control state start) logic_mapping: Dictionary: (control node ID, truth state) => control state record: Namedtuple with discrete control information for output
- +# Ribasim.FlatVector
— Type.
struct FlatVector{T} <: AbstractVector{T}
A FlatVector is an AbstractVector that iterates the T of a Vector{Vector{T}}
.
Each inner vector is assumed to be of equal length.
It is similar to Iterators.flatten
, though that doesn’t work with the Tables.Column
interface, which needs length
and getindex
support.
# Ribasim.FlowBoundary
— Type.
nodeid: node ID of the FlowBoundary node active: whether this node is active and thus contributes flow flowrate: target flow rate
- +# Ribasim.FractionalFlow
— Type.
Requirements:
# Ribasim.LevelBoundary
— Type.
node_id: node ID of the LevelBoundary node active: whether this node is active level: the fixed level of this ‘infinitely big basin’
- +# Ribasim.LinearResistance
— Type.
Requirements:
# Ribasim.ManningResistance
— Type.
This is a simple Manning-Gauckler reach connection.
# Ribasim.Model
— Type.
Model(
::MTK.AbstractODESystem,
@@ -552,30 +552,33 @@ sys::SciMLBase.AbstractODEIntegrator
integrator )
Struct that combines data from the System and Integrator that we will need during and after model construction.
- +# Ribasim.Outlet
— Type.
nodeid: node ID of the Outlet node active: whether this node is active and thus contributes flow flowrate: target flow rate minflowrate: The minimal flow rate of the outlet maxflowrate: The maximum flow rate of the outlet controlmapping: dictionary from (nodeid, controlstate) to target flow rate ispid_controlled: whether the flow rate of this outlet is governed by PID control
- +# Ribasim.PidControl
— Type.
PID control currently only supports regulating basin levels.
nodeid: node ID of the PidControl node active: whether this node is active and thus sets flow rates listennodeid: the id of the basin being controlled pidparams: a vector interpolation for parameters changing over time. The parameters are respectively target, proportional, integral, derivative, where the last three are the coefficients for the PID equation. error: the current error; basintarget - currentlevel
- +# Ribasim.Pump
— Type.
nodeid: node ID of the Pump node active: whether this node is active and thus contributes flow flowrate: target flow rate minflowrate: The minimal flow rate of the pump maxflowrate: The maximum flow rate of the pump controlmapping: dictionary from (nodeid, controlstate) to target flow rate ispid_controlled: whether the flow rate of this pump is governed by PID control
- +# Ribasim.TabulatedRatingCurve
— Type.
struct TabulatedRatingCurve{C}
Rating curve from level to discharge. The rating curve is a lookup table with linear interpolation in between. Relation can be updated in time, which is done by moving data from the time
field into the tables
, which is done in the update_tabulated_rating_curve
callback.
Type parameter C indicates the content backing the StructVector, which can be a NamedTuple of Vectors or Arrow Primitives, and is added to avoid type instabilities.
nodeid: node ID of the TabulatedRatingCurve node active: whether this node is active and thus contributes flows tables: The current Q(h) relationships time: The time table used for updating the tables controlmapping: dictionary from (nodeid, controlstate) to Q(h) and/or active state
- +# Ribasim.Terminal
— Type.
node_id: node ID of the Terminal node
- + +# Ribasim.User
— Type.
demand: water flux demand of user over time active: whether this node is active and thus demands water allocated: water flux currently allocated to user returnfactor: the factor in [0,1] of how much of the abstracted water is given back to the system minlevel: The level of the source basin below which the user does not abstract priority: integer > 0, the lower the number the higher the priority of the users demand
+# Ribasim.config.Config
— Method.
Config(config_path::AbstractString; kwargs...)
Parse a TOML file to a Config. Keys can be overruled using keyword arguments. To overrule keys from a subsection, e.g. dt
from the solver
section, use underscores: solver_dt
.
Ribasim.Pump
Ribasim.TabulatedRatingCurve
Ribasim.Terminal
Ribasim.User
Ribasim.config.Config
Ribasim.basin_bottom
Ribasim.basin_bottoms
Ribasim.findlastgroup
Ribasim.findsorted
Ribasim.formulate!
Ribasim.formulate_flow!
Ribasim.formulate_flow!
Ribasim.formulate_flow!
Ribasim.formulate_flow!
Ribasim.get_area_and_level
Ribasim.get_compressor
Ribasim.get_fractional_flow_connected_basins
Ribasim.set_table_row!
Ribasim.sorted_table!
Ribasim.update_basin
Ribasim.update_jac_prototype!
Ribasim.update_jac_prototype!
Ribasim.update_jac_prototype!
Ribasim.update_jac_prototype!
Ribasim.update_jac_prototype!
Ribasim.update_tabulated_rating_curve!
Ribasim.valid_discrete_control
Ribasim.valid_edge_types
function NewNodeType(db::DB, config::Config)::NewNodeType
static = load_structvector(db, config, NewNodeTypeStaticV1)
- defaults = (; active = true)
+ defaults = (; foo = 1, bar = false)
# Process potential control states in the static data
- static_parsed = parse_static(static, db, "Outlet", defaults)
+ parsed_parameters, valid = parse_static_and_time(db, config, "Outlet"; static, defaults)
- # Unpack the fields of static as inputs for the NewNodeType constructor
- return NewNodeType(
- static_parsed.node_id,
- static_parsed.some_property,
- static_parsed.control_mapping)
-end
+ if !valid
+ error("Errors occurred when parsing NewNodeType data.")
+ end
+
+ # Unpack the fields of static as inputs for the NewNodeType constructor
+ return NewNodeType(
+ parsed_parameters.node_id,
+ parsed_parameters.some_property,
+ parsed_parameters.control_mapping)
+end
LinearResistance
, ManningResistance
. If either the in-neighbor or out-neighbor of a node of this group is a basin, the storage of this basin depends on itself. If both the in-neighbor and the out-neighbor are basins, their storages also depend on eachother.
PidControl
node is a special case which is discussed in equations.In the methods formulate_jac!
in jac.jl
the analytical expressions for the partial derivatives \(\frac{\partial Q_{i',j'}}{\partial u_i}\) (and the ones related to PID integral states) are hardcoded. For NewNodeType
either a new method of formulate_jac!
has to be introduced, or it has to be added to the list of node types that do not contribute to the Jacobian in the method of formulate_jac!
whose signature contains node::AbstractParameterNode
.
Using jac_prototype
the Jacobian of water_balance!
is computed automatically using ForwardDiff.jl with memory management provided by PreallocationTools.jl. These computations make use of DiffCache
and dual numbers.
Create a new file python/ribasim/ribasim/node_types/new_node_type.py
which is structured as follows:
import pandas as pd
-import pandera as pa
-from pandera.engines.pandas_engine import PydanticModel
-from pandera.typing import DataFrame
-
-from ribasim import models
-from ribasim.input_base import TableModel
-
-= ("NewNodeType",)
- __all__
-class StaticSchema(pa.SchemaModel):
-class Config:
- """Config with dataframe-level data type."""
-
-= PydanticModel(models.NewNodeTypeStatic)
- dtype
-# Possible other schemas
-
+from typing import Optional
+
+import pandera as pa
+from pandera.engines.pandas_engine import PydanticModel
+from pandera.typing import DataFrame
+
+from ribasim import models
+from ribasim.input_base import TableModel
+
+= ("NewNodeType",)
+ __all__
+class StaticSchema(pa.SchemaModel):
+class Config:
+ """Config with dataframe-level data type."""
+
+= PydanticModel(models.NewNodeTypeStatic)
+ dtype
+# Possible other schemas
-class NewNodeType(TableModel):
-"""
- Description of this node type.
-
- Parameters
- ----------
- static: pandas.DataFrame
- table with data for this node type.
-
- possible other schemas
- """
-
-= None
- static: Optional[DataFrame[StaticSchema]] # possible other schemas
-
-class Config:
- = True
- validate_assignment
+
+class NewNodeType(TableModel):
+"""
+ Description of this node type.
+
+ Parameters
+ ----------
+ static: pandas.DataFrame
+ table with data for this node type.
+
+ possible other schemas
+ """
+
+= None
+ static: Optional[DataFrame[StaticSchema]] # possible other schemas
+
+class Config:
+ = True
validate_assignment
-
-def sort(self):
- self.static = self.static.sort_values("node_id", ignore_index=True)
+def sort(self):
+ self.static.sort_values("node_id", ignore_index=True, inplace=True)
The sort
method should implement the same sorting as in validation.jl
.
Now in both python/ribasim/ribasim/__init__.py
and python/ribasim/ribasim/node_types/__init__.py
add
Outlet / static
: flow rate, minimum crest levelUser / static
: demandsUser / time
: dynamic demandsTerminal / static
: - (only node IDs)A user can demand a certain flow from the basin that supplies it. 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 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, 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.
column | +type | +unit | +restriction | +
---|---|---|---|
node_id | +Int | +- | +sorted | +
active | +Bool | +- | +(optional, default true) | +
demand | +Float64 | +\(m^3 s^{-1}\) | +- | +
return_factor | +Float64 | +- | +between [0 - 1] | +
min_level | +Float64 | +\(m\) | +(optional) | +
priority | +Int | +- | +- | +
This table is the transient form of the User
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 demand is interpolated linearly, and outside the demand is constant given by the nearest time value. Note that a node_id
can be either in this table or in the static one, but not both.
column | +type | +unit | +restriction | +
---|---|---|---|
node_id | +Int | +- | +sorted per time | +
time | +DateTime | +- | +sorted | +
demand | +Float64 | +\(m^3 s^{-1}\) | +- | +
return_factor | +Float64 | +- | +between [0 - 1] | +
min_level | +Float64 | +\(m\) | +(optional) | +
priority | +Int | +- | +- | +
Acts like an infinitely large basin where the level does not change by flow. This can be connected to a basin via a LinearResistance
. This boundary node will then exchange water with the basin based on the difference in water level between the two.
level | Float64 | -\(m^3\) | +\(m\) | - |
This table is the transient form of the LevelBoundary
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 levels can be updated over time. In between the given times the level is interpolated linearly, and outside the flow rate is constant given by the nearest time value. Note that a node_id
can be either in this table or in the static one, but not both.
level | Float64 | -\(m^3 s^{-1}\) | +\(m\) | - |
Pump water to a destination node. We require that the edge connecting the flow boundary to the Basin should point towards the basin, so that positive flow corresponds to water being added to the model. The set flow rate will be pumped unless the intake storage (for a negative flow rate) is less than \(10~m^3\), in which case the flow rate will be linearly reduced to \(0~m^3/s\). Note that the connected node must always be a Basin.
This table is the transient form of the FlowBoundary
table. The only differences are that a time column is added and the nodes are assumed to be active so this column is removed. The table must by sorted by time, and per time it must be sorted by node_id
. With this the flow rates can be updated over time. In between the given times the flow rate is interpolated linearly, and outside the flow rate is constant given by the nearest time value. Note that a node_id
can be either in this table or in the static one, but not both.
Flow through this connection is estimated by conservation of energy and the Manning-Gauckler formula to estimate friction losses.
A terminal is a water sink without state or properties. Any water that flows into a terminal node is removed from the model. No water can come into the model from a terminal node. For example, terminal nodes can be used as a downstream boundary.
DiscreteControl is implemented based on VectorContinuousCallback.
-The condition schema defines conditions of the form ‘the discrete_control node with this node id listens to whether the given variable of the node with the given listen feature id is grater than the given value’. If the condition variable comes from a time-series, a look ahead \(\Delta t\) can be supplied.
The logic schema defines which control states are triggered based on the truth of the conditions a discrete_control node listens to. DiscreteControl is applied in the Julia core as follows:
The control table contains a record of each change of control state: when it happened, which control node was involved, to which control state it changed and based on which truth state.
The PidControl node controls the level in a basin by continuously controlling the flow rate of a connected pump or outlet. See also PID controller. When A PidControl node is made inactive, the node under its control retains the last flow rate value, and the error integral is reset to 0.
The pump must be an outneighbor of the controlled basin, the outlet must be an inneighbor. Thus for the same coefficient values in proportional
, integral
and derivative
, the pump and outlet have the oppositve effect on the controlled basin.
In the future controlling the flow on a particular edge could be supported.
@@ -1488,8 +1601,8 @@This table is the transient form of the PidControl
table. The differences are that a time column is added and the nodes are assumed to be active so this column is removed. The table must by sorted by time, and per time it must be sorted by node_id
. With this the target level and PID coefficients can be updated over time. In between the given times the these values interpolated linearly, and outside these values area constant given by the nearest time value. Note that a node_id
can be either in this table or in the static one, but not both.
user |
+Optional[User] | +User node type with demand and priority. | +required | + +
starttime |
Union[str, datetime.datetime] | Starting time of the simulation. | required |
endtime |
Union[str, datetime.datetime] | End time of the simulation. | required |
solver |
Optional[Solver] | Solver settings. | required |
logging |
Optional[logging] | Logging settings. | diff --git a/schema/Config.schema.json b/schema/Config.schema.json index f324fc81b..2125ba6da 100644 --- a/schema/Config.schema.json +++ b/schema/Config.schema.json @@ -38,6 +38,13 @@ }, "$ref": "https://deltares.github.io/Ribasim/schema/level_boundary.schema.json" }, + "user": { + "default": { + "static": null, + "time": null + }, + "$ref": "https://deltares.github.io/Ribasim/schema/user.schema.json" + }, "pump": { "default": { "static": null @@ -165,6 +172,7 @@ "level_boundary", "pump", "tabulated_rating_curve", + "user", "flow_boundary", "basin", "manning_resistance", diff --git a/schema/DiscreteControlCondition.schema.json b/schema/DiscreteControlCondition.schema.json index 3ce188fe2..00cd84ec3 100644 --- a/schema/DiscreteControlCondition.schema.json +++ b/schema/DiscreteControlCondition.schema.json @@ -27,10 +27,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "number" }, { - "type": "number" + "type": "null" } ] } diff --git a/schema/FlowBoundaryStatic.schema.json b/schema/FlowBoundaryStatic.schema.json index 8300ea9fd..429fe8f20 100644 --- a/schema/FlowBoundaryStatic.schema.json +++ b/schema/FlowBoundaryStatic.schema.json @@ -11,10 +11,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "boolean" }, { - "type": "boolean" + "type": "null" } ] }, diff --git a/schema/LevelBoundaryStatic.schema.json b/schema/LevelBoundaryStatic.schema.json index f635afce0..85ff75b67 100644 --- a/schema/LevelBoundaryStatic.schema.json +++ b/schema/LevelBoundaryStatic.schema.json @@ -11,10 +11,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "boolean" }, { - "type": "boolean" + "type": "null" } ] }, diff --git a/schema/LinearResistanceStatic.schema.json b/schema/LinearResistanceStatic.schema.json index 5a5146ea9..c0261ad6f 100644 --- a/schema/LinearResistanceStatic.schema.json +++ b/schema/LinearResistanceStatic.schema.json @@ -11,10 +11,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "boolean" }, { - "type": "boolean" + "type": "null" } ] }, diff --git a/schema/ManningResistanceStatic.schema.json b/schema/ManningResistanceStatic.schema.json index 22bad56df..f8ec17058 100644 --- a/schema/ManningResistanceStatic.schema.json +++ b/schema/ManningResistanceStatic.schema.json @@ -19,10 +19,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "boolean" }, { - "type": "boolean" + "type": "null" } ] }, diff --git a/schema/OutletStatic.schema.json b/schema/OutletStatic.schema.json index d8feae988..8c24fac8c 100644 --- a/schema/OutletStatic.schema.json +++ b/schema/OutletStatic.schema.json @@ -5,10 +5,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "number" }, { - "type": "number" + "type": "null" } ] }, @@ -22,10 +22,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "boolean" }, { - "type": "boolean" + "type": "null" } ] }, @@ -33,10 +33,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "number" }, { - "type": "number" + "type": "null" } ] }, @@ -63,10 +63,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "number" }, { - "type": "number" + "type": "null" } ] } diff --git a/schema/PIDControlStatic.schema.json b/schema/PIDControlStatic.schema.json index df64576ee..c3226017b 100644 --- a/schema/PIDControlStatic.schema.json +++ b/schema/PIDControlStatic.schema.json @@ -19,10 +19,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "boolean" }, { - "type": "boolean" + "type": "null" } ] }, diff --git a/schema/PumpStatic.schema.json b/schema/PumpStatic.schema.json index afe80e42c..fa15283fa 100644 --- a/schema/PumpStatic.schema.json +++ b/schema/PumpStatic.schema.json @@ -5,10 +5,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "number" }, { - "type": "number" + "type": "null" } ] }, @@ -22,10 +22,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "boolean" }, { - "type": "boolean" + "type": "null" } ] }, @@ -52,10 +52,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "number" }, { - "type": "number" + "type": "null" } ] } diff --git a/schema/TabulatedRatingCurveStatic.schema.json b/schema/TabulatedRatingCurveStatic.schema.json index cb244575b..c073e11c1 100644 --- a/schema/TabulatedRatingCurveStatic.schema.json +++ b/schema/TabulatedRatingCurveStatic.schema.json @@ -11,10 +11,10 @@ "format": "default", "anyOf": [ { - "type": "null" + "type": "boolean" }, { - "type": "boolean" + "type": "null" } ] }, diff --git a/schema/UserStatic.schema.json b/schema/UserStatic.schema.json new file mode 100644 index 000000000..d0c806834 --- /dev/null +++ b/schema/UserStatic.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "remarks": { + "format": "default", + "default": "", + "description": "a hack for pandera", + "type": "string" + }, + "priority": { + "format": "default", + "type": "integer" + }, + "active": { + "format": "default", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "demand": { + "format": "double", + "type": "number" + }, + "return_factor": { + "format": "double", + "type": "number" + }, + "min_level": { + "format": "double", + "type": "number" + }, + "node_id": { + "format": "default", + "type": "integer" + } + }, + "required": [ + "node_id", + "demand", + "return_factor", + "min_level", + "priority" + ], + "$id": "https://deltares.github.io/Ribasim/schema/UserStatic.schema.json", + "title": "UserStatic", + "description": "A UserStatic object based on Ribasim.UserStaticV1", + "type": "object" +} diff --git a/schema/UserTime.schema.json b/schema/UserTime.schema.json new file mode 100644 index 000000000..0b930a790 --- /dev/null +++ b/schema/UserTime.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "remarks": { + "format": "default", + "default": "", + "description": "a hack for pandera", + "type": "string" + }, + "priority": { + "format": "default", + "type": "integer" + }, + "time": { + "format": "date-time", + "type": "string" + }, + "demand": { + "format": "double", + "type": "number" + }, + "return_factor": { + "format": "double", + "type": "number" + }, + "min_level": { + "format": "double", + "type": "number" + }, + "node_id": { + "format": "default", + "type": "integer" + } + }, + "required": [ + "node_id", + "time", + "demand", + "return_factor", + "min_level", + "priority" + ], + "$id": "https://deltares.github.io/Ribasim/schema/UserTime.schema.json", + "title": "UserTime", + "description": "A UserTime object based on Ribasim.UserTimeV1", + "type": "object" +} diff --git a/schema/root.schema.json b/schema/root.schema.json index db2018158..469dcc354 100644 --- a/schema/root.schema.json +++ b/schema/root.schema.json @@ -10,12 +10,18 @@ "FlowBoundaryTime": { "$ref": "FlowBoundaryTime.schema.json" }, + "UserStatic": { + "$ref": "UserStatic.schema.json" + }, "PumpStatic": { "$ref": "PumpStatic.schema.json" }, "LevelBoundaryStatic": { "$ref": "LevelBoundaryStatic.schema.json" }, + "UserTime": { + "$ref": "UserTime.schema.json" + }, "DiscreteControlCondition": { "$ref": "DiscreteControlCondition.schema.json" }, diff --git a/schema/user.schema.json b/schema/user.schema.json new file mode 100644 index 000000000..6fcacadf3 --- /dev/null +++ b/schema/user.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "time": { + "format": "default", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "default": null + }, + "static": { + "format": "default", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "default": null + } + }, + "required": [ + ], + "$id": "https://deltares.github.io/Ribasim/schema/user.schema.json", + "title": "user", + "description": "A user object based on Ribasim.config.user", + "type": "object" +} diff --git a/search.json b/search.json index 403eb3e9d..957d2ca4e 100644 --- a/search.json +++ b/search.json @@ -46,14 +46,14 @@ "href": "python/reference/Model.html", "title": "1 Model", "section": "", - "text": "Model()\nA full Ribasim model schematisation with all input.\nRibasim model containing the location of the nodes, the edges between the nodes, and the node parametrization.\n\n\n\n\n\n\n\n\n\n\n\nName\nType\nDescription\nDefault\n\n\n\n\nmodelname\nstr\nModel name, used in TOML and GeoPackage file name.\nrequired\n\n\nnode\nNode\nThe ID, type and geometry of each node.\nrequired\n\n\nedge\nEdge\nHow the nodes are connected.\nrequired\n\n\nbasin\nBasin\nThe waterbodies.\nrequired\n\n\nfractional_flow\nOptional[FractionalFlow]\nSplit flows into fractions.\nrequired\n\n\nlevel_boundary\nOptional[LevelBoundary]\nBoundary condition specifying the water level.\nrequired\n\n\nflow_boundary\nOptional[FlowBoundary]\nBoundary conditions specifying the flow.\nrequired\n\n\nlinear_resistance\n\nLinear flow resistance.\nrequired\n\n\nmanning_resistance\nOptional[ManningResistance]\nFlow resistance based on the Manning formula.\nrequired\n\n\ntabulated_rating_curve\nOptional[TabulatedRatingCurve]\nTabulated rating curve describing flow based on the upstream water level.\nrequired\n\n\npump\nOptional[Pump]\nPrescribed flow rate from one basin to the other.\nrequired\n\n\noutlet\nOptional[Outlet]\nPrescribed flow rate from one basin to the other.\nrequired\n\n\nterminal\nOptional[Terminal]\nWater sink without state or properties.\nrequired\n\n\ndiscrete_control\nOptional[DiscreteControl]\nDiscrete control logic.\nrequired\n\n\npid_control\nOptional[PidControl]\nPID controller attempting to set the level of a basin to a desired value using a pump/outlet.\nrequired\n\n\nstarttime\nUnion[str, datetime.datetime]\nStarting time of the simulation.\nrequired\n\n\nendtime\nUnion[str, datetime.datetime]\nEnd time of the simulation.\nrequired\n\n\nsolver\nOptional[Solver]\nSolver settings.\nrequired\n\n\nlogging\nOptional[logging]\nLogging settings.\nrequired\n\n\n\n\n\n\n\n\n\nName\nDescription\n\n\n\n\nfields\nReturn the names of the fields contained in the Model.\n\n\nfrom_toml\nInitialize a model from the TOML configuration file.\n\n\nplot\nPlot the nodes and edges of the model.\n\n\nsort\nSort all input tables as required.\n\n\nvalidate_model\nValidate the model.\n\n\nvalidate_model_node_IDs\nCheck whether the node IDs in the node field correspond to the node IDs on the node type fields.\n\n\nvalidate_model_node_field_IDs\nCheck whether the node IDs of the node_type fields are valid.\n\n\nvalidate_model_node_types\nCheck whether all node types in the node field are valid.\n\n\nwrite\nWrite the contents of the model to a GeoPackage and a TOML configuration file.\n\n\n\n\n\nModel.fields(cls)\nReturn the names of the fields contained in the Model.\n\n\n\nModel.from_toml(path)\nInitialize a model from the TOML configuration file.\n\n\n\n\n\n\n\n\n\n\n\nName\nType\nDescription\nDefault\n\n\n\n\npath\nFilePath\nPath to the configuration TOML file.\nrequired\n\n\n\n\n\n\n\n\n\nType\nDescription\n\n\n\n\nModel\n\n\n\n\n\n\n\n\nModel.plot(self, ax=None)\nPlot the nodes and edges of the model.\n\n\n\n\n\n\n\n\n\n\n\nName\nType\nDescription\nDefault\n\n\n\n\nax\nmatplotlib.pyplot.Artist\nAxes on which to draw the plot.\nNone\n\n\n\n\n\n\n\n\n\nType\nDescription\n\n\n\n\nmatplotlib.pyplot.Artist\n\n\n\n\n\n\n\n\nModel.sort(self)\nSort all input tables as required.\nTables are sorted by “node_id”, unless otherwise specified. Sorting is done automatically before writing the table.\n\n\n\nModel.validate_model(self)\nValidate the model.\nChecks: - Whether all node types in the node field are valid - Whether the node IDs of the node_type fields are valid - Whether the node IDs in the node field correspond to the node IDs on the node type fields\n\n\n\nModel.validate_model_node_IDs(self)\nCheck whether the node IDs in the node field correspond to the node IDs on the node type fields.\n\n\n\nModel.validate_model_node_field_IDs(self)\nCheck whether the node IDs of the node_type fields are valid.\n\n\n\nModel.validate_model_node_types(self)\nCheck whether all node types in the node field are valid.\n\n\n\nModel.write(self, directory)\nWrite the contents of the model to a GeoPackage and a TOML configuration file.\nIf directory does not exist, it is created before writing. The GeoPackage and TOML file will be called {modelname}.gpkg and {modelname}.toml respectively.\n\n\n\n\n\nName\nType\nDescription\nDefault\n\n\n\n\ndirectory\nFilePath\n\nrequired" + "text": "Model()\nA full Ribasim model schematisation with all input.\nRibasim model containing the location of the nodes, the edges between the nodes, and the node parametrization.\n\n\n\n\n\n\n\n\n\n\n\nName\nType\nDescription\nDefault\n\n\n\n\nmodelname\nstr\nModel name, used in TOML and GeoPackage file name.\nrequired\n\n\nnode\nNode\nThe ID, type and geometry of each node.\nrequired\n\n\nedge\nEdge\nHow the nodes are connected.\nrequired\n\n\nbasin\nBasin\nThe waterbodies.\nrequired\n\n\nfractional_flow\nOptional[FractionalFlow]\nSplit flows into fractions.\nrequired\n\n\nlevel_boundary\nOptional[LevelBoundary]\nBoundary condition specifying the water level.\nrequired\n\n\nflow_boundary\nOptional[FlowBoundary]\nBoundary conditions specifying the flow.\nrequired\n\n\nlinear_resistance\n\nLinear flow resistance.\nrequired\n\n\nmanning_resistance\nOptional[ManningResistance]\nFlow resistance based on the Manning formula.\nrequired\n\n\ntabulated_rating_curve\nOptional[TabulatedRatingCurve]\nTabulated rating curve describing flow based on the upstream water level.\nrequired\n\n\npump\nOptional[Pump]\nPrescribed flow rate from one basin to the other.\nrequired\n\n\noutlet\nOptional[Outlet]\nPrescribed flow rate from one basin to the other.\nrequired\n\n\nterminal\nOptional[Terminal]\nWater sink without state or properties.\nrequired\n\n\ndiscrete_control\nOptional[DiscreteControl]\nDiscrete control logic.\nrequired\n\n\npid_control\nOptional[PidControl]\nPID controller attempting to set the level of a basin to a desired value using a pump/outlet.\nrequired\n\n\nuser\nOptional[User]\nUser node type with demand and priority.\nrequired\n\n\nstarttime\nUnion[str, datetime.datetime]\nStarting time of the simulation.\nrequired\n\n\nendtime\nUnion[str, datetime.datetime]\nEnd time of the simulation.\nrequired\n\n\nsolver\nOptional[Solver]\nSolver settings.\nrequired\n\n\nlogging\nOptional[logging]\nLogging settings.\nrequired\n\n\n\n\n\n\n\n\n\nName\nDescription\n\n\n\n\nfields\nReturn the names of the fields contained in the Model.\n\n\nfrom_toml\nInitialize a model from the TOML configuration file.\n\n\nplot\nPlot the nodes and edges of the model.\n\n\nsort\nSort all input tables as required.\n\n\nvalidate_model\nValidate the model.\n\n\nvalidate_model_node_IDs\nCheck whether the node IDs in the node field correspond to the node IDs on the node type fields.\n\n\nvalidate_model_node_field_IDs\nCheck whether the node IDs of the node_type fields are valid.\n\n\nvalidate_model_node_types\nCheck whether all node types in the node field are valid.\n\n\nwrite\nWrite the contents of the model to a GeoPackage and a TOML configuration file.\n\n\n\n\n\nModel.fields(cls)\nReturn the names of the fields contained in the Model.\n\n\n\nModel.from_toml(path)\nInitialize a model from the TOML configuration file.\n\n\n\n\n\n\n\n\n\n\n\nName\nType\nDescription\nDefault\n\n\n\n\npath\nFilePath\nPath to the configuration TOML file.\nrequired\n\n\n\n\n\n\n\n\n\nType\nDescription\n\n\n\n\nModel\n\n\n\n\n\n\n\n\nModel.plot(self, ax=None)\nPlot the nodes and edges of the model.\n\n\n\n\n\n\n\n\n\n\n\nName\nType\nDescription\nDefault\n\n\n\n\nax\nmatplotlib.pyplot.Artist\nAxes on which to draw the plot.\nNone\n\n\n\n\n\n\n\n\n\nType\nDescription\n\n\n\n\nmatplotlib.pyplot.Artist\n\n\n\n\n\n\n\n\nModel.sort(self)\nSort all input tables as required.\nTables are sorted by “node_id”, unless otherwise specified. Sorting is done automatically before writing the table.\n\n\n\nModel.validate_model(self)\nValidate the model.\nChecks: - Whether all node types in the node field are valid - Whether the node IDs of the node_type fields are valid - Whether the node IDs in the node field correspond to the node IDs on the node type fields\n\n\n\nModel.validate_model_node_IDs(self)\nCheck whether the node IDs in the node field correspond to the node IDs on the node type fields.\n\n\n\nModel.validate_model_node_field_IDs(self)\nCheck whether the node IDs of the node_type fields are valid.\n\n\n\nModel.validate_model_node_types(self)\nCheck whether all node types in the node field are valid.\n\n\n\nModel.write(self, directory)\nWrite the contents of the model to a GeoPackage and a TOML configuration file.\nIf directory does not exist, it is created before writing. The GeoPackage and TOML file will be called {modelname}.gpkg and {modelname}.toml respectively.\n\n\n\n\n\nName\nType\nDescription\nDefault\n\n\n\n\ndirectory\nFilePath\n\nrequired" }, { "objectID": "python/reference/Model.html#parameters", "href": "python/reference/Model.html#parameters", "title": "1 Model", "section": "", - "text": "Name\nType\nDescription\nDefault\n\n\n\n\nmodelname\nstr\nModel name, used in TOML and GeoPackage file name.\nrequired\n\n\nnode\nNode\nThe ID, type and geometry of each node.\nrequired\n\n\nedge\nEdge\nHow the nodes are connected.\nrequired\n\n\nbasin\nBasin\nThe waterbodies.\nrequired\n\n\nfractional_flow\nOptional[FractionalFlow]\nSplit flows into fractions.\nrequired\n\n\nlevel_boundary\nOptional[LevelBoundary]\nBoundary condition specifying the water level.\nrequired\n\n\nflow_boundary\nOptional[FlowBoundary]\nBoundary conditions specifying the flow.\nrequired\n\n\nlinear_resistance\n\nLinear flow resistance.\nrequired\n\n\nmanning_resistance\nOptional[ManningResistance]\nFlow resistance based on the Manning formula.\nrequired\n\n\ntabulated_rating_curve\nOptional[TabulatedRatingCurve]\nTabulated rating curve describing flow based on the upstream water level.\nrequired\n\n\npump\nOptional[Pump]\nPrescribed flow rate from one basin to the other.\nrequired\n\n\noutlet\nOptional[Outlet]\nPrescribed flow rate from one basin to the other.\nrequired\n\n\nterminal\nOptional[Terminal]\nWater sink without state or properties.\nrequired\n\n\ndiscrete_control\nOptional[DiscreteControl]\nDiscrete control logic.\nrequired\n\n\npid_control\nOptional[PidControl]\nPID controller attempting to set the level of a basin to a desired value using a pump/outlet.\nrequired\n\n\nstarttime\nUnion[str, datetime.datetime]\nStarting time of the simulation.\nrequired\n\n\nendtime\nUnion[str, datetime.datetime]\nEnd time of the simulation.\nrequired\n\n\nsolver\nOptional[Solver]\nSolver settings.\nrequired\n\n\nlogging\nOptional[logging]\nLogging settings.\nrequired" + "text": "Name\nType\nDescription\nDefault\n\n\n\n\nmodelname\nstr\nModel name, used in TOML and GeoPackage file name.\nrequired\n\n\nnode\nNode\nThe ID, type and geometry of each node.\nrequired\n\n\nedge\nEdge\nHow the nodes are connected.\nrequired\n\n\nbasin\nBasin\nThe waterbodies.\nrequired\n\n\nfractional_flow\nOptional[FractionalFlow]\nSplit flows into fractions.\nrequired\n\n\nlevel_boundary\nOptional[LevelBoundary]\nBoundary condition specifying the water level.\nrequired\n\n\nflow_boundary\nOptional[FlowBoundary]\nBoundary conditions specifying the flow.\nrequired\n\n\nlinear_resistance\n\nLinear flow resistance.\nrequired\n\n\nmanning_resistance\nOptional[ManningResistance]\nFlow resistance based on the Manning formula.\nrequired\n\n\ntabulated_rating_curve\nOptional[TabulatedRatingCurve]\nTabulated rating curve describing flow based on the upstream water level.\nrequired\n\n\npump\nOptional[Pump]\nPrescribed flow rate from one basin to the other.\nrequired\n\n\noutlet\nOptional[Outlet]\nPrescribed flow rate from one basin to the other.\nrequired\n\n\nterminal\nOptional[Terminal]\nWater sink without state or properties.\nrequired\n\n\ndiscrete_control\nOptional[DiscreteControl]\nDiscrete control logic.\nrequired\n\n\npid_control\nOptional[PidControl]\nPID controller attempting to set the level of a basin to a desired value using a pump/outlet.\nrequired\n\n\nuser\nOptional[User]\nUser node type with demand and priority.\nrequired\n\n\nstarttime\nUnion[str, datetime.datetime]\nStarting time of the simulation.\nrequired\n\n\nendtime\nUnion[str, datetime.datetime]\nEnd time of the simulation.\nrequired\n\n\nsolver\nOptional[Solver]\nSolver settings.\nrequired\n\n\nlogging\nOptional[logging]\nLogging settings.\nrequired" }, { "objectID": "python/reference/Model.html#methods", @@ -179,14 +179,14 @@ "href": "build/index.html#types", "title": "1 API Reference", "section": "1.2 Types", - "text": "1.2 Types\n# Ribasim.Basin — Type.\nRequirements:\n\nMust be positive: precipitation, evaporation, infiltration, drainage\nIndex points to a Basin\nvolume, area, level must all be positive and monotonic increasing.\n\nType parameter C indicates the content backing the StructVector, which can be a NamedTuple of vectors or Arrow Tables, and is added to avoid type instabilities. The nodeid are Indices to support fast lookup of e.g. currentlevel using ID.\nif autodiff T = DiffCache{Vector{Float64}} else T = Vector{Float64} end\nsource\n# Ribasim.Connectivity — Type.\nStore the connectivity information\ngraphflow, graphcontrol: directed graph with vertices equal to ids flow: store the flow on every flow edge edgeidsflow, edgeidscontrol: get the external edge id from (src, dst) edgeconnectiontypeflow, edgeconnectiontypescontrol: get (srcnodetype, dstnodetype) from edge id\nif autodiff T = DiffCache{SparseArrays.SparseMatrixCSC{Float64, Int64}, Vector{Float64}} else T = SparseMatrixCSC{Float64, Int} end\nsource\n# Ribasim.DiscreteControl — Type.\nnodeid: node ID of the DiscreteControl node; these are not unique but repeated by the amount of conditions of this DiscreteControl node listenfeatureid: the ID of the node/edge being condition on variable: the name of the variable in the condition greaterthan: The threshold value in the condition conditionvalue: The current value of each condition controlstate: Dictionary: node ID => (control state, control state start) logic_mapping: Dictionary: (control node ID, truth state) => control state record: Namedtuple with discrete control information for output\nsource\n# Ribasim.FlatVector — Type.\nstruct FlatVector{T} <: AbstractVector{T}\nA FlatVector is an AbstractVector that iterates the T of a Vector{Vector{T}}.\nEach inner vector is assumed to be of equal length.\nIt is similar to Iterators.flatten, though that doesn’t work with the Tables.Column interface, which needs length and getindex support.\nsource\n# Ribasim.FlowBoundary — Type.\nnodeid: node ID of the FlowBoundary node active: whether this node is active and thus contributes flow flowrate: target flow rate\nsource\n# Ribasim.FractionalFlow — Type.\nRequirements:\n\nfrom: must be (TabulatedRatingCurve,) node\nto: must be (Basin,) node\nfraction must be positive.\n\nnodeid: node ID of the TabulatedRatingCurve node fraction: The fraction in [0,1] of flow the node lets through controlmapping: dictionary from (nodeid, controlstate) to fraction\nsource\n# Ribasim.LevelBoundary — Type.\nnode_id: node ID of the LevelBoundary node active: whether this node is active level: the fixed level of this ‘infinitely big basin’\nsource\n# Ribasim.LinearResistance — Type.\nRequirements:\n\nfrom: must be (Basin,) node\nto: must be (Basin,) node\n\nnodeid: node ID of the LinearResistance node active: whether this node is active and thus contributes flows resistance: the resistance to flow; Q = Δh/resistance controlmapping: dictionary from (nodeid, controlstate) to resistance and/or active state\nsource\n# Ribasim.ManningResistance — Type.\nThis is a simple Manning-Gauckler reach connection.\n\nLength describes the reach length.\nroughness describes Manning’s n in (SI units).\n\nThe profile is described by a trapezoid:\n \\ / ^\n \\ / |\n \\ / | dz\nbottom \\______/ |\n^ <--->\n| dy\n| <------>\n| width\n|\n|\n+ datum (e.g. MSL)\nWith profile_slope = dy / dz. A rectangular profile requires a slope of 0.0.\nRequirements:\n\nfrom: must be (Basin,) node\nto: must be (Basin,) node\nlength > 0\nroughess > 0\nprofile_width >= 0\nprofile_slope >= 0\n(profilewidth == 0) xor (profileslope == 0)\n\nsource\n# Ribasim.Model — Type.\nModel(\n sys::MTK.AbstractODESystem,\n config::Config,\n saved_flow::SavedValues(Float64, Vector{Float64}),\n integrator::SciMLBase.AbstractODEIntegrator\n)\nStruct that combines data from the System and Integrator that we will need during and after model construction.\nsource\n# Ribasim.Outlet — Type.\nnodeid: node ID of the Outlet node active: whether this node is active and thus contributes flow flowrate: target flow rate minflowrate: The minimal flow rate of the outlet maxflowrate: The maximum flow rate of the outlet controlmapping: dictionary from (nodeid, controlstate) to target flow rate ispid_controlled: whether the flow rate of this outlet is governed by PID control\nsource\n# Ribasim.PidControl — Type.\nPID control currently only supports regulating basin levels.\nnodeid: node ID of the PidControl node active: whether this node is active and thus sets flow rates listennodeid: the id of the basin being controlled pidparams: a vector interpolation for parameters changing over time. The parameters are respectively target, proportional, integral, derivative, where the last three are the coefficients for the PID equation. error: the current error; basintarget - currentlevel\nsource\n# Ribasim.Pump — Type.\nnodeid: node ID of the Pump node active: whether this node is active and thus contributes flow flowrate: target flow rate minflowrate: The minimal flow rate of the pump maxflowrate: The maximum flow rate of the pump controlmapping: dictionary from (nodeid, controlstate) to target flow rate ispid_controlled: whether the flow rate of this pump is governed by PID control\nsource\n# Ribasim.TabulatedRatingCurve — Type.\nstruct TabulatedRatingCurve{C}\nRating curve from level to discharge. The rating curve is a lookup table with linear interpolation in between. Relation can be updated in time, which is done by moving data from the time field into the tables, which is done in the update_tabulated_rating_curve callback.\nType parameter C indicates the content backing the StructVector, which can be a NamedTuple of Vectors or Arrow Primitives, and is added to avoid type instabilities.\nnodeid: node ID of the TabulatedRatingCurve node active: whether this node is active and thus contributes flows tables: The current Q(h) relationships time: The time table used for updating the tables controlmapping: dictionary from (nodeid, controlstate) to Q(h) and/or active state\nsource\n# Ribasim.Terminal — Type.\nnode_id: node ID of the Terminal node\nsource\n# Ribasim.config.Config — Method.\nConfig(config_path::AbstractString; kwargs...)\nParse a TOML file to a Config. Keys can be overruled using keyword arguments. To overrule keys from a subsection, e.g. dt from the solver section, use underscores: solver_dt.\nsource" + "text": "1.2 Types\n# Ribasim.Basin — Type.\nRequirements:\n\nMust be positive: precipitation, evaporation, infiltration, drainage\nIndex points to a Basin\nvolume, area, level must all be positive and monotonic increasing.\n\nType parameter C indicates the content backing the StructVector, which can be a NamedTuple of vectors or Arrow Tables, and is added to avoid type instabilities. The nodeid are Indices to support fast lookup of e.g. currentlevel using ID.\nif autodiff T = DiffCache{Vector{Float64}} else T = Vector{Float64} end\nsource\n# Ribasim.Connectivity — Type.\nStore the connectivity information\ngraphflow, graphcontrol: directed graph with vertices equal to ids flow: store the flow on every flow edge edgeidsflow, edgeidscontrol: get the external edge id from (src, dst) edgeconnectiontypeflow, edgeconnectiontypescontrol: get (srcnodetype, dstnodetype) from edge id\nif autodiff T = DiffCache{SparseArrays.SparseMatrixCSC{Float64, Int64}, Vector{Float64}} else T = SparseMatrixCSC{Float64, Int} end\nsource\n# Ribasim.DiscreteControl — Type.\nnodeid: node ID of the DiscreteControl node; these are not unique but repeated by the amount of conditions of this DiscreteControl node listenfeatureid: the ID of the node/edge being condition on variable: the name of the variable in the condition greaterthan: The threshold value in the condition conditionvalue: The current value of each condition controlstate: Dictionary: node ID => (control state, control state start) logic_mapping: Dictionary: (control node ID, truth state) => control state record: Namedtuple with discrete control information for output\nsource\n# Ribasim.FlatVector — Type.\nstruct FlatVector{T} <: AbstractVector{T}\nA FlatVector is an AbstractVector that iterates the T of a Vector{Vector{T}}.\nEach inner vector is assumed to be of equal length.\nIt is similar to Iterators.flatten, though that doesn’t work with the Tables.Column interface, which needs length and getindex support.\nsource\n# Ribasim.FlowBoundary — Type.\nnodeid: node ID of the FlowBoundary node active: whether this node is active and thus contributes flow flowrate: target flow rate\nsource\n# Ribasim.FractionalFlow — Type.\nRequirements:\n\nfrom: must be (TabulatedRatingCurve,) node\nto: must be (Basin,) node\nfraction must be positive.\n\nnodeid: node ID of the TabulatedRatingCurve node fraction: The fraction in [0,1] of flow the node lets through controlmapping: dictionary from (nodeid, controlstate) to fraction\nsource\n# Ribasim.LevelBoundary — Type.\nnode_id: node ID of the LevelBoundary node active: whether this node is active level: the fixed level of this ‘infinitely big basin’\nsource\n# Ribasim.LinearResistance — Type.\nRequirements:\n\nfrom: must be (Basin,) node\nto: must be (Basin,) node\n\nnodeid: node ID of the LinearResistance node active: whether this node is active and thus contributes flows resistance: the resistance to flow; Q = Δh/resistance controlmapping: dictionary from (nodeid, controlstate) to resistance and/or active state\nsource\n# Ribasim.ManningResistance — Type.\nThis is a simple Manning-Gauckler reach connection.\n\nLength describes the reach length.\nroughness describes Manning’s n in (SI units).\n\nThe profile is described by a trapezoid:\n \\ / ^\n \\ / |\n \\ / | dz\nbottom \\______/ |\n^ <--->\n| dy\n| <------>\n| width\n|\n|\n+ datum (e.g. MSL)\nWith profile_slope = dy / dz. A rectangular profile requires a slope of 0.0.\nRequirements:\n\nfrom: must be (Basin,) node\nto: must be (Basin,) node\nlength > 0\nroughess > 0\nprofile_width >= 0\nprofile_slope >= 0\n(profilewidth == 0) xor (profileslope == 0)\n\nsource\n# Ribasim.Model — Type.\nModel(\n sys::MTK.AbstractODESystem,\n config::Config,\n saved_flow::SavedValues(Float64, Vector{Float64}),\n integrator::SciMLBase.AbstractODEIntegrator\n)\nStruct that combines data from the System and Integrator that we will need during and after model construction.\nsource\n# Ribasim.Outlet — Type.\nnodeid: node ID of the Outlet node active: whether this node is active and thus contributes flow flowrate: target flow rate minflowrate: The minimal flow rate of the outlet maxflowrate: The maximum flow rate of the outlet controlmapping: dictionary from (nodeid, controlstate) to target flow rate ispid_controlled: whether the flow rate of this outlet is governed by PID control\nsource\n# Ribasim.PidControl — Type.\nPID control currently only supports regulating basin levels.\nnodeid: node ID of the PidControl node active: whether this node is active and thus sets flow rates listennodeid: the id of the basin being controlled pidparams: a vector interpolation for parameters changing over time. The parameters are respectively target, proportional, integral, derivative, where the last three are the coefficients for the PID equation. error: the current error; basintarget - currentlevel\nsource\n# Ribasim.Pump — Type.\nnodeid: node ID of the Pump node active: whether this node is active and thus contributes flow flowrate: target flow rate minflowrate: The minimal flow rate of the pump maxflowrate: The maximum flow rate of the pump controlmapping: dictionary from (nodeid, controlstate) to target flow rate ispid_controlled: whether the flow rate of this pump is governed by PID control\nsource\n# Ribasim.TabulatedRatingCurve — Type.\nstruct TabulatedRatingCurve{C}\nRating curve from level to discharge. The rating curve is a lookup table with linear interpolation in between. Relation can be updated in time, which is done by moving data from the time field into the tables, which is done in the update_tabulated_rating_curve callback.\nType parameter C indicates the content backing the StructVector, which can be a NamedTuple of Vectors or Arrow Primitives, and is added to avoid type instabilities.\nnodeid: node ID of the TabulatedRatingCurve node active: whether this node is active and thus contributes flows tables: The current Q(h) relationships time: The time table used for updating the tables controlmapping: dictionary from (nodeid, controlstate) to Q(h) and/or active state\nsource\n# Ribasim.Terminal — Type.\nnode_id: node ID of the Terminal node\nsource\n# Ribasim.User — Type.\ndemand: water flux demand of user over time active: whether this node is active and thus demands water allocated: water flux currently allocated to user returnfactor: the factor in [0,1] of how much of the abstracted water is given back to the system minlevel: The level of the source basin below which the user does not abstract priority: integer > 0, the lower the number the higher the priority of the users demand\nsource\n# Ribasim.config.Config — Method.\nConfig(config_path::AbstractString; kwargs...)\nParse a TOML file to a Config. Keys can be overruled using keyword arguments. To overrule keys from a subsection, e.g. dt from the solver section, use underscores: solver_dt.\nsource" }, { "objectID": "build/index.html#index", "href": "build/index.html#index", "title": "1 API Reference", "section": "1.3 Index", - "text": "1.3 Index\n\nRibasim.Basin\nRibasim.Connectivity\nRibasim.DiscreteControl\nRibasim.FlatVector\nRibasim.FlowBoundary\nRibasim.FractionalFlow\nRibasim.LevelBoundary\nRibasim.LinearResistance\nRibasim.ManningResistance\nRibasim.Model\nRibasim.Outlet\nRibasim.PidControl\nRibasim.Pump\nRibasim.TabulatedRatingCurve\nRibasim.Terminal\nRibasim.config.Config\nRibasim.basin_bottom\nRibasim.basin_bottoms\nRibasim.config.algorithm\nRibasim.config.snake_case\nRibasim.create_callbacks\nRibasim.create_graph\nRibasim.create_storage_tables\nRibasim.datetime_since\nRibasim.discrete_control_affect!\nRibasim.discrete_control_affect_downcrossing!\nRibasim.discrete_control_affect_upcrossing!\nRibasim.discrete_control_condition\nRibasim.expand_logic_mapping\nRibasim.findlastgroup\nRibasim.findsorted\nRibasim.formulate!\nRibasim.formulate_flow!\nRibasim.formulate_flow!\nRibasim.formulate_flow!\nRibasim.get_area_and_level\nRibasim.get_compressor\nRibasim.get_fractional_flow_connected_basins\nRibasim.get_jac_prototype\nRibasim.get_level\nRibasim.get_scalar_interpolation\nRibasim.get_storage_from_level\nRibasim.get_storages_and_levels\nRibasim.get_storages_from_levels\nRibasim.get_tstops\nRibasim.get_value\nRibasim.id_index\nRibasim.input_path\nRibasim.load_data\nRibasim.load_structvector\nRibasim.nodefields\nRibasim.nodetype\nRibasim.output_path\nRibasim.parse_static_and_time\nRibasim.profile_storage\nRibasim.qh_interpolation\nRibasim.reduction_factor\nRibasim.save_flow\nRibasim.scalar_interpolation_derivative\nRibasim.seconds_since\nRibasim.set_current_value!\nRibasim.set_static_value!\nRibasim.set_table_row!\nRibasim.sorted_table!\nRibasim.update_basin\nRibasim.update_jac_prototype!\nRibasim.update_jac_prototype!\nRibasim.update_jac_prototype!\nRibasim.update_jac_prototype!\nRibasim.update_tabulated_rating_curve!\nRibasim.valid_discrete_control\nRibasim.valid_edge_types\nRibasim.valid_edges\nRibasim.valid_flow_rates\nRibasim.valid_fractional_flow\nRibasim.valid_n_neighbors\nRibasim.valid_profiles\nRibasim.water_balance!" + "text": "1.3 Index\n\nRibasim.Basin\nRibasim.Connectivity\nRibasim.DiscreteControl\nRibasim.FlatVector\nRibasim.FlowBoundary\nRibasim.FractionalFlow\nRibasim.LevelBoundary\nRibasim.LinearResistance\nRibasim.ManningResistance\nRibasim.Model\nRibasim.Outlet\nRibasim.PidControl\nRibasim.Pump\nRibasim.TabulatedRatingCurve\nRibasim.Terminal\nRibasim.User\nRibasim.config.Config\nRibasim.basin_bottom\nRibasim.basin_bottoms\nRibasim.config.algorithm\nRibasim.config.snake_case\nRibasim.create_callbacks\nRibasim.create_graph\nRibasim.create_storage_tables\nRibasim.datetime_since\nRibasim.discrete_control_affect!\nRibasim.discrete_control_affect_downcrossing!\nRibasim.discrete_control_affect_upcrossing!\nRibasim.discrete_control_condition\nRibasim.expand_logic_mapping\nRibasim.findlastgroup\nRibasim.findsorted\nRibasim.formulate!\nRibasim.formulate_flow!\nRibasim.formulate_flow!\nRibasim.formulate_flow!\nRibasim.get_area_and_level\nRibasim.get_compressor\nRibasim.get_fractional_flow_connected_basins\nRibasim.get_jac_prototype\nRibasim.get_level\nRibasim.get_scalar_interpolation\nRibasim.get_storage_from_level\nRibasim.get_storages_and_levels\nRibasim.get_storages_from_levels\nRibasim.get_tstops\nRibasim.get_value\nRibasim.id_index\nRibasim.input_path\nRibasim.load_data\nRibasim.load_structvector\nRibasim.nodefields\nRibasim.nodetype\nRibasim.output_path\nRibasim.parse_static_and_time\nRibasim.profile_storage\nRibasim.qh_interpolation\nRibasim.reduction_factor\nRibasim.save_flow\nRibasim.scalar_interpolation_derivative\nRibasim.seconds_since\nRibasim.set_current_value!\nRibasim.set_static_value!\nRibasim.set_table_row!\nRibasim.sorted_table!\nRibasim.update_basin\nRibasim.update_jac_prototype!\nRibasim.update_jac_prototype!\nRibasim.update_jac_prototype!\nRibasim.update_jac_prototype!\nRibasim.update_tabulated_rating_curve!\nRibasim.valid_discrete_control\nRibasim.valid_edge_types\nRibasim.valid_edges\nRibasim.valid_flow_rates\nRibasim.valid_fractional_flow\nRibasim.valid_n_neighbors\nRibasim.valid_profiles\nRibasim.water_balance!" }, { "objectID": "core/usage.html", @@ -244,47 +244,54 @@ "section": "7.1 TabulatedRatingCurve / time", "text": "7.1 TabulatedRatingCurve / time\nThis table is the transient form of the TabulatedRatingCurve table. The only difference is that a time column is added. The table must by sorted by time, and per time it must be sorted by node_id. With this the rating curves can be updated over time. Note that a node_id can be either in this table or in the static one, but not both.\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\ntime\nDateTime\n-\nsorted\n\n\nnode_id\nInt\n-\nsorted per time\n\n\nlevel\nFloat64\n\\(m\\)\n-\n\n\ndischarge\nFloat64\n\\(m^3 s^{-1}\\)\nnon-negative" }, + { + "objectID": "core/usage.html#user-time", + "href": "core/usage.html#user-time", + "title": "Usage", + "section": "10.1 User / time", + "text": "10.1 User / time\nThis table is the transient form of the User 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 demand is interpolated linearly, and outside the demand is constant given by the nearest time value. Note that a node_id can be either in this table or in the static one, but not both.\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\nnode_id\nInt\n-\nsorted per time\n\n\ntime\nDateTime\n-\nsorted\n\n\ndemand\nFloat64\n\\(m^3 s^{-1}\\)\n-\n\n\nreturn_factor\nFloat64\n-\nbetween [0 - 1]\n\n\nmin_level\nFloat64\n\\(m\\)\n(optional)\n\n\npriority\nInt\n-\n-" + }, { "objectID": "core/usage.html#levelboundary-time", "href": "core/usage.html#levelboundary-time", "title": "Usage", - "section": "10.1 LevelBoundary / time", - "text": "10.1 LevelBoundary / time\nThis table is the transient form of the LevelBoundary 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 levels can be updated over time. In between the given times the level is interpolated linearly, and outside the flow rate is constant given by the nearest time value. Note that a node_id can be either in this table or in the static one, but not both.\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\ntime\nDateTime\n-\nsorted\n\n\nnode_id\nInt\n-\nsorted per time\n\n\nlevel\nFloat64\n\\(m^3 s^{-1}\\)\n-" + "section": "11.1 LevelBoundary / time", + "text": "11.1 LevelBoundary / time\nThis table is the transient form of the LevelBoundary 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 levels can be updated over time. In between the given times the level is interpolated linearly, and outside the flow rate is constant given by the nearest time value. Note that a node_id can be either in this table or in the static one, but not both.\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\ntime\nDateTime\n-\nsorted\n\n\nnode_id\nInt\n-\nsorted per time\n\n\nlevel\nFloat64\n\\(m\\)\n-" }, { "objectID": "core/usage.html#flowboundary-time", "href": "core/usage.html#flowboundary-time", "title": "Usage", - "section": "11.1 FlowBoundary / time", - "text": "11.1 FlowBoundary / time\nThis table is the transient form of the FlowBoundary table. The only differences are that a time column is added and the nodes are assumed to be active so this column is removed. The table must by sorted by time, and per time it must be sorted by node_id. With this the flow rates can be updated over time. In between the given times the flow rate is interpolated linearly, and outside the flow rate is constant given by the nearest time value. Note that a node_id can be either in this table or in the static one, but not both.\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\ntime\nDateTime\n-\nsorted\n\n\nnode_id\nInt\n-\nsorted per time\n\n\nflow_rate\nFloat64\n\\(m^3 s^{-1}\\)\nnon-negative" + "section": "12.1 FlowBoundary / time", + "text": "12.1 FlowBoundary / time\nThis table is the transient form of the FlowBoundary table. The only differences are that a time column is added and the nodes are assumed to be active so this column is removed. The table must by sorted by time, and per time it must be sorted by node_id. With this the flow rates can be updated over time. In between the given times the flow rate is interpolated linearly, and outside the flow rate is constant given by the nearest time value. Note that a node_id can be either in this table or in the static one, but not both.\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\ntime\nDateTime\n-\nsorted\n\n\nnode_id\nInt\n-\nsorted per time\n\n\nflow_rate\nFloat64\n\\(m^3 s^{-1}\\)\nnon-negative" }, { "objectID": "core/usage.html#sec-condition", "href": "core/usage.html#sec-condition", "title": "Usage", - "section": "15.1 DiscreteControl / condition", - "text": "15.1 DiscreteControl / condition\nThe condition schema defines conditions of the form ‘the discrete_control node with this node id listens to whether the given variable of the node with the given listen feature id is grater than the given value’. If the condition variable comes from a time-series, a look ahead \\(\\Delta t\\) can be supplied.\n\n\n\n\n\n\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\nnode_id\nInt\n-\nsorted\n\n\nlisten_feature_id\nInt\n-\n-\n\n\nvariable\nString\n-\nmust be “level” or “flow_rate”\n\n\ngreater_than\nFloat64\nvarious\n-\n\n\nlook_ahead\nFloat64\n\\(s\\)\nOnly on transient boundary conditions, non-negative (optional, default 0)" + "section": "16.1 DiscreteControl / condition", + "text": "16.1 DiscreteControl / condition\nThe condition schema defines conditions of the form ‘the discrete_control node with this node id listens to whether the given variable of the node with the given listen feature id is grater than the given value’. If the condition variable comes from a time-series, a look ahead \\(\\Delta t\\) can be supplied.\n\n\n\n\n\n\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\nnode_id\nInt\n-\nsorted\n\n\nlisten_feature_id\nInt\n-\n-\n\n\nvariable\nString\n-\nmust be “level” or “flow_rate”\n\n\ngreater_than\nFloat64\nvarious\n-\n\n\nlook_ahead\nFloat64\n\\(s\\)\nOnly on transient boundary conditions, non-negative (optional, default 0)" }, { "objectID": "core/usage.html#discretecontrol-logic", "href": "core/usage.html#discretecontrol-logic", "title": "Usage", - "section": "15.2 DiscreteControl / logic", - "text": "15.2 DiscreteControl / logic\nThe logic schema defines which control states are triggered based on the truth of the conditions a discrete_control node listens to. DiscreteControl is applied in the Julia core as follows:\n\nDuring the simulation it is checked whether the truth of any of the conditions changes.\nWhen a condition changes, the corresponding discrrete_control node id is retrieved (node_id in the condition schema above).\nThe truth value of all the conditions this discrete_control node lisens to are retrieved, in the order as they are specified in the condition schema. This is then converted into a string of “T” for true and “F” for false. This string we call the truth state.*\nThe table below determines for the given discrete_control node ID and truth state what the corresponding control state is.\nFor all the nodes this discrete_control node affects (as given by the “control” edges in Edges / static), their parameters are set to those parameters in NodeType / static corresponding to the determined control state.\n\n*. There is also a second truth state created in which for the last condition that changed it is specified whether it was an upcrossing (“U”) or downcrossing (“D”) of the threshold (greater than) value. If a control state is specified for a truth state that is crossing-specific, this takes precedence over the control state for the truth state that contains only “T” and “F”.\n\n\n\n\n\n\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\nnode_id\nInt\n-\nsorted\n\n\ntruth_state\nString\n-\nConsists of the characters “T” (true), “F” (false), “U” (upcrossing), “D” (downcrossing) and “*” (any)\n\n\ncontrol_state\nString\n-" + "section": "16.2 DiscreteControl / logic", + "text": "16.2 DiscreteControl / logic\nThe logic schema defines which control states are triggered based on the truth of the conditions a discrete_control node listens to. DiscreteControl is applied in the Julia core as follows:\n\nDuring the simulation it is checked whether the truth of any of the conditions changes.\nWhen a condition changes, the corresponding discrrete_control node id is retrieved (node_id in the condition schema above).\nThe truth value of all the conditions this discrete_control node lisens to are retrieved, in the order as they are specified in the condition schema. This is then converted into a string of “T” for true and “F” for false. This string we call the truth state.*\nThe table below determines for the given discrete_control node ID and truth state what the corresponding control state is.\nFor all the nodes this discrete_control node affects (as given by the “control” edges in Edges / static), their parameters are set to those parameters in NodeType / static corresponding to the determined control state.\n\n*. There is also a second truth state created in which for the last condition that changed it is specified whether it was an upcrossing (“U”) or downcrossing (“D”) of the threshold (greater than) value. If a control state is specified for a truth state that is crossing-specific, this takes precedence over the control state for the truth state that contains only “T” and “F”.\n\n\n\n\n\n\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\nnode_id\nInt\n-\nsorted\n\n\ntruth_state\nString\n-\nConsists of the characters “T” (true), “F” (false), “U” (upcrossing), “D” (downcrossing) and “*” (any)\n\n\ncontrol_state\nString\n-" }, { "objectID": "core/usage.html#discretecontrol-output", "href": "core/usage.html#discretecontrol-output", "title": "Usage", - "section": "15.3 DiscreteControl output", - "text": "15.3 DiscreteControl output\nThe control table contains a record of each change of control state: when it happened, which control node was involved, to which control state it changed and based on which truth state.\n\n\n\ncolumn\ntype\n\n\n\n\ntime\nDateTime\n\n\ncontrol_node_id\nInt\n\n\ntruth_state\nString\n\n\ncontrol_state\nString" + "section": "16.3 DiscreteControl output", + "text": "16.3 DiscreteControl output\nThe control table contains a record of each change of control state: when it happened, which control node was involved, to which control state it changed and based on which truth state.\n\n\n\ncolumn\ntype\n\n\n\n\ntime\nDateTime\n\n\ncontrol_node_id\nInt\n\n\ntruth_state\nString\n\n\ncontrol_state\nString" }, { "objectID": "core/usage.html#pidcontrol-time", "href": "core/usage.html#pidcontrol-time", "title": "Usage", - "section": "16.1 PidControl / time", - "text": "16.1 PidControl / time\nThis table is the transient form of the PidControl table. The differences are that a time column is added and the nodes are assumed to be active so this column is removed. The table must by sorted by time, and per time it must be sorted by node_id. With this the target level and PID coefficients can be updated over time. In between the given times the these values interpolated linearly, and outside these values area constant given by the nearest time value. Note that a node_id can be either in this table or in the static one, but not both.\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\nnode_id\nInt\n-\nsorted per time\n\n\ntime\nDateTime\n-\nsorted\n\n\nlisten_node_id\nInt\n-\n-\n\n\ntarget\nFloat64\n\\(m\\)\n-\n\n\nproportional\nFloat64\n\\(s^{-1}\\)\n-\n\n\nintegral\nFloat64\n\\(s^{-2}\\)\n-\n\n\nderivative\nFloat64\n-\n-" + "section": "17.1 PidControl / time", + "text": "17.1 PidControl / time\nThis table is the transient form of the PidControl table. The differences are that a time column is added and the nodes are assumed to be active so this column is removed. The table must by sorted by time, and per time it must be sorted by node_id. With this the target level and PID coefficients can be updated over time. In between the given times the these values interpolated linearly, and outside these values area constant given by the nearest time value. Note that a node_id can be either in this table or in the static one, but not both.\n\n\n\ncolumn\ntype\nunit\nrestriction\n\n\n\n\nnode_id\nInt\n-\nsorted per time\n\n\ntime\nDateTime\n-\nsorted\n\n\nlisten_node_id\nInt\n-\n-\n\n\ntarget\nFloat64\n\\(m\\)\n-\n\n\nproportional\nFloat64\n\\(s^{-1}\\)\n-\n\n\nintegral\nFloat64\n\\(s^{-2}\\)\n-\n\n\nderivative\nFloat64\n-\n-" }, { "objectID": "qgis/index.html", @@ -501,7 +508,7 @@ "href": "contribute/addnode.html#reading-from-configuration", "title": "Adding node types", "section": "1.2 Reading from configuration", - "text": "1.2 Reading from configuration\nThere can be several schemas associated with a single node type. To define a schema for the new node type, add the following to validation.jl:\n@schema \"ribasim.newnodetype.static\" NewNodeTypeStatic\n\n\"\"\"\nnode_id: node ID of the NewNodeType node\n\"\"\"\n@version NewNodeTypeStaticV1 begin\n node_id::Int\n # Other fields\nend\nHere Static refers to data that does not change over time. For naming conventions of these schemas see Node usage.\nvalidation.jl also deals with checking and applying a specific sorting order for the tabular data (default is sorting by node ID only), see sort_by_function and sorted_table!.\nNow we define the function that is called in the second bullet above, in create.jl:\nfunction NewNodeType(db::DB, config::Config)::NewNodeType\n static = load_structvector(db, config, NewNodeTypeStaticV1)\n defaults = (; active = true)\n # Process potential control states in the static data\n static_parsed = parse_static(static, db, \"Outlet\", defaults)\n\n # Unpack the fields of static as inputs for the NewNodeType constructor\n return NewNodeType(\n static_parsed.node_id,\n static_parsed.some_property,\n static_parsed.control_mapping)\nend" + "text": "1.2 Reading from configuration\nThere can be several schemas associated with a single node type. To define a schema for the new node type, add the following to validation.jl:\n@schema \"ribasim.newnodetype.static\" NewNodeTypeStatic\n\n\"\"\"\nnode_id: node ID of the NewNodeType node\n\"\"\"\n@version NewNodeTypeStaticV1 begin\n node_id::Int\n # Other fields\nend\nHere Static refers to data that does not change over time. For naming conventions of these schemas see Node usage.\nvalidation.jl also deals with checking and applying a specific sorting order for the tabular data (default is sorting by node ID only), see sort_by_function and sorted_table!.\nNow we define the function that is called in the second bullet above, in create.jl:\nfunction NewNodeType(db::DB, config::Config)::NewNodeType\n static = load_structvector(db, config, NewNodeTypeStaticV1)\n defaults = (; foo = 1, bar = false)\n # Process potential control states in the static data\n parsed_parameters, valid = parse_static_and_time(db, config, \"Outlet\"; static, defaults)\n\n if !valid\n error(\"Errors occurred when parsing NewNodeType data.\")\n end\n\n # Unpack the fields of static as inputs for the NewNodeType constructor\n return NewNodeType(\n parsed_parameters.node_id,\n parsed_parameters.some_property,\n parsed_parameters.control_mapping)\nend" }, { "objectID": "contribute/addnode.html#node-behavior", @@ -515,14 +522,14 @@ "href": "contribute/addnode.html#the-jacobian", "title": "Adding node types", "section": "1.4 The Jacobian", - "text": "1.4 The Jacobian\nSee Equations for a mathematical description of the Jacobian.\nBefore the Julia core runs its simulation, the sparsity structure jac_prototype of \\(J\\) is determined with get_jac_prototype in utils.jl. This function runs trough all node types and looks for nodes that create dependencies between states. It creates a sparse matrix of zeros and ones, where the ones denote locations of possible non-zeros in \\(J\\).\nWe divide the various node types in groups based on what type of state dependencies they yield, and these groups are discussed below. Each group has its own method update_jac_prototype! in utils.jl for the sparsity structure induced by nodes of that group. NewNodeType should be added to the signature of one these methods, or to the list of node types that do not contribute to the Jacobian in the method of update_jac_prototype! whose signature contains node::AbstractParameterNode. Of course it is also possible that a new method of update_jac_prototype! has to be introduced.\nThe current dependency groups are:\n\nOut-neighbor dependencies: examples are TabulatedRatingCurve, Pump (the latter only in the reduction factor regime and not PID controlled). If the in-neighbor of a node of this group is a basin, then the storage of this basin affects itself and the storage of the outneighbor (or the basin one node further if it is connected with a FractionalFlow in between) if that is also a basin;\nEither-neighbor dependencies: examples are LinearResistance, ManningResistance. If either the in-neighbor or out-neighbor of a node of this group is a basin, the storage of this basin depends on itself. If both the in-neighbor and the out-neighbor are basins, their storages also depend on eachother.\nThe PidControl node is a special case which is discussed in equations.\n\nIn the methods formulate_jac! in jac.jl the analytical expressions for the partial derivatives \\(\\frac{\\partial Q_{i',j'}}{\\partial u_i}\\) (and the ones related to PID integral states) are hardcoded. For NewNodeType either a new method of formulate_jac! has to be introduced, or it has to be added to the list of node types that do not contribute to the Jacobian in the method of formulate_jac! whose signature contains node::AbstractParameterNode." + "text": "1.4 The Jacobian\nSee Equations for a mathematical description of the Jacobian.\nBefore the Julia core runs its simulation, the sparsity structure jac_prototype of \\(J\\) is determined with get_jac_prototype in utils.jl. This function runs trough all node types and looks for nodes that create dependencies between states. It creates a sparse matrix of zeros and ones, where the ones denote locations of possible non-zeros in \\(J\\).\nWe divide the various node types in groups based on what type of state dependencies they yield, and these groups are discussed below. Each group has its own method update_jac_prototype! in utils.jl for the sparsity structure induced by nodes of that group. NewNodeType should be added to the signature of one these methods, or to the list of node types that do not contribute to the Jacobian in the method of update_jac_prototype! whose signature contains node::AbstractParameterNode. Of course it is also possible that a new method of update_jac_prototype! has to be introduced.\nThe current dependency groups are:\n\nOut-neighbor dependencies: examples are TabulatedRatingCurve, Pump (the latter only in the reduction factor regime and not PID controlled). If the in-neighbor of a node of this group is a basin, then the storage of this basin affects itself and the storage of the outneighbor (or the basin one node further if it is connected with a FractionalFlow in between) if that is also a basin;\nEither-neighbor dependencies: examples are LinearResistance, ManningResistance. If either the in-neighbor or out-neighbor of a node of this group is a basin, the storage of this basin depends on itself. If both the in-neighbor and the out-neighbor are basins, their storages also depend on eachother.\nThe PidControl node is a special case which is discussed in equations.\n\nUsing jac_prototype the Jacobian of water_balance! is computed automatically using ForwardDiff.jl with memory management provided by PreallocationTools.jl. These computations make use of DiffCache and dual numbers." }, { "objectID": "contribute/addnode.html#python-class", "href": "contribute/addnode.html#python-class", "title": "Adding node types", "section": "2.1 Python class", - "text": "2.1 Python class\nCreate a new file python/ribasim/ribasim/node_types/new_node_type.py which is structured as follows:\nimport pandas as pd\nimport pandera as pa\nfrom pandera.engines.pandas_engine import PydanticModel\nfrom pandera.typing import DataFrame\n\nfrom ribasim import models\nfrom ribasim.input_base import TableModel\n\n__all__ = (\"NewNodeType\",)\n\nclass StaticSchema(pa.SchemaModel):\n class Config:\n \"\"\"Config with dataframe-level data type.\"\"\"\n\n dtype = PydanticModel(models.NewNodeTypeStatic)\n\n# Possible other schemas\n\n\nclass NewNodeType(TableModel):\n \"\"\"\n Description of this node type.\n\n Parameters\n ----------\n static: pandas.DataFrame\n table with data for this node type.\n\n possible other schemas\n \"\"\"\n\n static: Optional[DataFrame[StaticSchema]] = None\n # possible other schemas\n\n class Config:\n validate_assignment = True\n\n\n\n def sort(self):\n self.static = self.static.sort_values(\"node_id\", ignore_index=True)\nThe sort method should implement the same sorting as in validation.jl.\nNow in both python/ribasim/ribasim/__init__.py and python/ribasim/ribasim/node_types/__init__.py add\n\nfrom ribasim.node_types.new_node_type import NewNodeType;\n\"NewNodeType\" to __all__.\n\nIn python/ribasim/ribasim/model.py, add\n\nfrom ribasim.new_node_type import NewNodeType;\nnew_node_type as a parameter and in the docstring of the Model class.\n\nIn python/ribasim/ribasim/geometry/node.py add a color and shape description in the MARKERS and COLORS dictionaries." + "text": "2.1 Python class\nCreate a new file python/ribasim/ribasim/node_types/new_node_type.py which is structured as follows:\nfrom typing import Optional\n\nimport pandera as pa\nfrom pandera.engines.pandas_engine import PydanticModel\nfrom pandera.typing import DataFrame\n\nfrom ribasim import models\nfrom ribasim.input_base import TableModel\n\n__all__ = (\"NewNodeType\",)\n\nclass StaticSchema(pa.SchemaModel):\n class Config:\n \"\"\"Config with dataframe-level data type.\"\"\"\n\n dtype = PydanticModel(models.NewNodeTypeStatic)\n\n# Possible other schemas\n\n\nclass NewNodeType(TableModel):\n \"\"\"\n Description of this node type.\n\n Parameters\n ----------\n static: pandas.DataFrame\n table with data for this node type.\n\n possible other schemas\n \"\"\"\n\n static: Optional[DataFrame[StaticSchema]] = None\n # possible other schemas\n\n class Config:\n validate_assignment = True\n\n def sort(self):\n self.static.sort_values(\"node_id\", ignore_index=True, inplace=True)\nThe sort method should implement the same sorting as in validation.jl.\nNow in both python/ribasim/ribasim/__init__.py and python/ribasim/ribasim/node_types/__init__.py add\n\nfrom ribasim.node_types.new_node_type import NewNodeType;\n\"NewNodeType\" to __all__.\n\nIn python/ribasim/ribasim/model.py, add\n\nfrom ribasim.new_node_type import NewNodeType;\nnew_node_type as a parameter and in the docstring of the Model class.\n\nIn python/ribasim/ribasim/geometry/node.py add a color and shape description in the MARKERS and COLORS dictionaries." }, { "objectID": "src/index.html", @@ -676,7 +683,7 @@ "href": "python/examples.html", "title": "Examples", "section": "", - "text": "1 Basic model with static forcing\n\nimport geopandas as gpd\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nimport ribasim\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1, 3, 3, 6, 6, 9, 9],\n \"area\": [0.01, 1000.0] * 4,\n \"level\": [0.0, 1.0] * 4,\n }\n)\n\n# Convert steady forcing to m/s\n# 2 mm/d precipitation, 1 mm/d evaporation\nseconds_in_day = 24 * 3600\nprecipitation = 0.002 / seconds_in_day\nevaporation = 0.001 / seconds_in_day\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [0],\n \"drainage\": [0.0],\n \"potential_evaporation\": [evaporation],\n \"infiltration\": [0.0],\n \"precipitation\": [precipitation],\n \"urban_runoff\": [0.0],\n }\n)\nstatic = static.iloc[[0, 0, 0, 0]]\nstatic[\"node_id\"] = [1, 3, 6, 9]\n\nbasin = ribasim.Basin(profile=profile, static=static)\n\nSetup linear resistance:\n\nlinear_resistance = ribasim.LinearResistance(\n static=pd.DataFrame(\n data={\"node_id\": [10, 12], \"resistance\": [5e3, (3600.0 * 24) / 100.0]}\n )\n)\n\nSetup Manning resistance:\n\nmanning_resistance = ribasim.ManningResistance(\n static=pd.DataFrame(\n data={\n \"node_id\": [2],\n \"length\": [900.0],\n \"manning_n\": [0.04],\n \"profile_width\": [6.0],\n \"profile_slope\": [3.0],\n }\n )\n)\n\nSet up a rating curve node:\n\n# Discharge: lose 1% of storage volume per day at storage = 1000.0.\nq1000 = 1000.0 * 0.01 / seconds_in_day\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\n \"node_id\": [4, 4],\n \"level\": [0.0, 1.0],\n \"discharge\": [0.0, q1000],\n }\n )\n)\n\nSetup fractional flows:\n\nfractional_flow = ribasim.FractionalFlow(\n static=pd.DataFrame(\n data={\n \"node_id\": [5, 8, 13],\n \"fraction\": [0.3, 0.6, 0.1],\n }\n )\n)\n\nSetup pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [7],\n \"flow_rate\": [0.5 / 3600],\n }\n )\n)\n\nSetup level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [11, 17],\n \"level\": [0.5, 1.5],\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [15, 16],\n \"flow_rate\": [1e-4, 1e-4],\n }\n )\n)\n\nSetup terminal:\n\nterminal = ribasim.Terminal(\n static=pd.DataFrame(\n data={\n \"node_id\": [14],\n }\n )\n)\n\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin,\n (1.0, 0.0), # 2: ManningResistance\n (2.0, 0.0), # 3: Basin\n (3.0, 0.0), # 4: TabulatedRatingCurve\n (3.0, 1.0), # 5: FractionalFlow\n (3.0, 2.0), # 6: Basin\n (4.0, 1.0), # 7: Pump\n (4.0, 0.0), # 8: FractionalFlow\n (5.0, 0.0), # 9: Basin\n (6.0, 0.0), # 10: LinearResistance\n (2.0, 2.0), # 11: LevelBoundary\n (2.0, 1.0), # 12: LinearResistance\n (3.0, -1.0), # 13: FractionalFlow\n (3.0, -2.0), # 14: Terminal\n (3.0, 3.0), # 15: FlowBoundary\n (0.0, 1.0), # 16: FlowBoundary\n (6.0, 1.0), # 17: LevelBoundary\n ]\n)\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_id, node_type = ribasim.Node.get_node_ids_and_types(\n basin,\n manning_resistance,\n rating_curve,\n pump,\n fractional_flow,\n linear_resistance,\n level_boundary,\n flow_boundary,\n terminal,\n)\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(node_id, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array(\n [1, 2, 3, 4, 4, 5, 6, 8, 7, 9, 11, 12, 4, 13, 15, 16, 10], dtype=np.int64\n)\nto_id = np.array(\n [2, 3, 4, 5, 8, 6, 7, 9, 9, 10, 12, 3, 13, 14, 6, 1, 17], dtype=np.int64\n)\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": len(from_id) * [\"flow\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"basic\",\n node=node,\n edge=edge,\n basin=basin,\n level_boundary=level_boundary,\n flow_boundary=flow_boundary,\n pump=pump,\n linear_resistance=linear_resistance,\n manning_resistance=manning_resistance,\n tabulated_rating_curve=rating_curve,\n fractional_flow=fractional_flow,\n terminal=terminal,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"basic\")\n\n\n\n2 Update the basic model with transient forcing\nThis assumes you have already created the basic model with static forcing.\n\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport ribasim\n\n\nmodel = ribasim.Model.from_toml(datadir / \"basic/basic.toml\")\n\n\ntime = pd.date_range(model.starttime, model.endtime)\nday_of_year = time.day_of_year.to_numpy()\nseconds_per_day = 24 * 60 * 60\nevaporation = (\n (-1.0 * np.cos(day_of_year / 365.0 * 2 * np.pi) + 1.0) * 0.0025 / seconds_per_day\n)\nrng = np.random.default_rng(seed=0)\nprecipitation = (\n rng.lognormal(mean=-1.0, sigma=1.7, size=time.size) * 0.001 / seconds_per_day\n)\n\nWe’ll use xarray to easily broadcast the values.\n\ntimeseries = (\n pd.DataFrame(\n data={\n \"node_id\": 1,\n \"time\": time,\n \"drainage\": 0.0,\n \"potential_evaporation\": evaporation,\n \"infiltration\": 0.0,\n \"precipitation\": precipitation,\n \"urban_runoff\": 0.0,\n }\n )\n .set_index(\"time\")\n .to_xarray()\n)\n\nbasin_ids = model.basin.static[\"node_id\"].to_numpy()\nbasin_nodes = xr.DataArray(\n np.ones(len(basin_ids)), coords={\"node_id\": basin_ids}, dims=[\"node_id\"]\n)\nforcing = (timeseries * basin_nodes).to_dataframe().reset_index()\n\n\nstate = pd.DataFrame(\n data={\n \"node_id\": basin_ids,\n \"level\": 1.4,\n \"concentration\": 0.0,\n }\n)\n\n\nmodel.basin.forcing = forcing\nmodel.basin.state = state\n\n\nmodel.modelname = \"basic_transient\"\nmodel.write(datadir / \"basic_transient\")\n\nNow run the model with ribasim basic-transient/basic.toml. After running the model, read back the output:\n\ndf_basin = pd.read_feather(datadir / \"basic_transient/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\ndf_basin_wide[\"level\"].plot()\n\n<Axes: xlabel='time'>\n\n\n\n\n\n\ndf_flow = pd.read_feather(datadir / \"basic_transient/output/flow.arrow\")\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow * 86400\nax = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\").plot()\nax.legend(bbox_to_anchor=(1.3, 1), title=\"Edge\")\n\n<matplotlib.legend.Legend at 0x7fe11670bf10>\n\n\n\n\n\n\ntype(df_flow)\n\npandas.core.frame.DataFrame\n\n\n\n\n3 Model with discrete control\nThe model constructed below consists of a single basin which slowly drains trough a TabulatedRatingCurve, but is held within a range around a target level (setpoint) by two connected pumps. These two pumps behave like a reversible pump. When pumping can be done in only one direction, and the other direction is only possible under gravity, use an Outlet for that direction.\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin\n (1.0, 0.5), # 2: Pump\n (1.0, -0.5), # 3: Pump\n (2.0, 0.0), # 4: LevelBoundary\n (-1.0, 0.0), # 5: TabulatedRatingCurve\n (-2.0, 0.0), # 6: Terminal\n (1.0, 0.0), # 7: DiscreteControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"Basin\",\n \"Pump\",\n \"Pump\",\n \"LevelBoundary\",\n \"TabulatedRatingCurve\",\n \"Terminal\",\n \"DiscreteControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 3, 4, 2, 1, 5, 7, 7], dtype=np.int64)\nto_id = np.array([3, 4, 2, 1, 5, 6, 2, 3], dtype=np.int64)\n\nedge_type = 6 * [\"flow\"] + 2 * [\"control\"]\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\"from_node_id\": from_id, \"to_node_id\": to_id, \"edge_type\": edge_type},\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1],\n \"area\": [1000.0, 1000.0],\n \"level\": [0.0, 1.0],\n }\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [1],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(data={\"node_id\": [1], \"level\": [20.0]})\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the discrete control:\n\ncondition = pd.DataFrame(\n data={\n \"node_id\": 3 * [7],\n \"listen_feature_id\": 3 * [1],\n \"variable\": 3 * [\"level\"],\n \"greater_than\": [5.0, 10.0, 15.0], # min, setpoint, max\n }\n)\n\nlogic = pd.DataFrame(\n data={\n \"node_id\": 5 * [7],\n \"truth_state\": [\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n \"control_state\": [\"in\", \"in\", \"none\", \"out\", \"out\"],\n }\n)\n\ndiscrete_control = ribasim.DiscreteControl(condition=condition, logic=logic)\n\nThe above control logic can be summarized as follows: - If the level gets above the maximum, activate the control state “out” until the setpoint is reached; - If the level gets below the minimum, active the control state “in” until the setpoint is reached; - Otherwise activate the control state “none”.\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": 3 * [2] + 3 * [3],\n \"control_state\": 2 * [\"none\", \"in\", \"out\"],\n \"flow_rate\": [0.0, 2e-3, 0.0, 0.0, 0.0, 2e-3],\n }\n )\n)\n\nThe pump data defines the following:\n\n\n\nControl state\nPump #2 flow rate (m/s)\nPump #3 flow rate (m/s)\n\n\n\n\n“none”\n0.0\n0.0\n\n\n“in”\n2e-3\n0.0\n\n\n“out”\n0.0\n2e-3\n\n\n\nSetup the level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(data={\"node_id\": [4], \"level\": [10.0]})\n)\n\nSetup the rating curve:\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\"node_id\": 2 * [5], \"level\": [2.0, 15.0], \"discharge\": [0.0, 1e-3]}\n )\n)\n\nSetup the terminal:\n\nterminal = ribasim.Terminal(static=pd.DataFrame(data={\"node_id\": [6]}))\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"level_setpoint_with_minmax\",\n node=node,\n edge=edge,\n basin=basin,\n pump=pump,\n level_boundary=level_boundary,\n tabulated_rating_curve=rating_curve,\n terminal=terminal,\n discrete_control=discrete_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nListen edges are plotted with a dashed line since they are not present in the “Edge / static” schema but only in the “Control / condition” schema.\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"level_setpoint_with_minmax\")\n\nNow run the model with level_setpoint_with_minmax/level_setpoint_with_minmax.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"level_setpoint_with_minmax/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\n\nax = df_basin_wide[\"level\"].plot()\n\ngreater_than = model.discrete_control.condition.greater_than\n\nax.hlines(\n greater_than,\n df_basin.time[0],\n df_basin.time.max(),\n lw=1,\n ls=\"--\",\n color=\"k\",\n)\n\ndf_control = pd.read_feather(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\ny_min, y_max = ax.get_ybound()\nax.fill_between(df_control.time[:2], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\nax.fill_between(df_control.time[2:4], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\n\nax.set_xticks(\n date2num(df_control.time).tolist(),\n df_control.control_state.tolist(),\n rotation=50,\n)\n\nax.set_yticks(greater_than, [\"min\", \"setpoint\", \"max\"])\nax.set_ylabel(\"level\")\nplt.show()\n\n\n\n\nThe highlighted regions show where a pump is active.\nLet’s print an overview of what happened with control:\n\nmodel.print_discrete_control_record(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\n0. At 2020-01-01 00:00:00 the control node with ID 7 reached truth state TTT:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level > 15.0\n\n This yielded control state \"out\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.002\n\n1. At 2020-02-09 01:17:29.324000 the control node with ID 7 reached truth state TFF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n2. At 2020-07-05 13:24:51.165000 the control node with ID 7 reached truth state FFF:\n For node ID 1 (Basin): level < 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"in\":\n For node ID 2 (Pump): flow_rate = 0.002\n For node ID 3 (Pump): flow_rate = 0.0\n\n3. At 2020-08-11 11:49:59.015000 the control node with ID 7 reached truth state TTF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n\n\nNote that crossing direction specific truth states (containing “U”, “D”) are not present in this overview even though they are part of the control logic. This is because in the control logic for this model these truth states are only used to sustain control states, while the overview only shows changes in control states.\n\n\n4 Model with PID control\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: FlowBoundary\n (1.0, 0.0), # 2: Basin\n (2.0, 0.5), # 3: Pump\n (3.0, 0.0), # 4: LevelBoundary\n (1.5, 1.0), # 5: PidControl\n (2.0, -0.5), # 6: outlet\n (1.5, -1.0), # 7: PidControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"FlowBoundary\",\n \"Basin\",\n \"Pump\",\n \"LevelBoundary\",\n \"PidControl\",\n \"Outlet\",\n \"PidControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 2, 3, 4, 6, 5, 7], dtype=np.int64)\nto_id = np.array([2, 3, 4, 6, 2, 3, 6], dtype=np.int64)\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": 5 * [\"flow\"] + 2 * [\"control\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\"node_id\": [2, 2], \"level\": [0.0, 1.0], \"area\": [1000.0, 1000.0]}\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"level\": [6.0],\n }\n)\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [3],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup the outlet:\n\noutlet = ribasim.Outlet(\n static=pd.DataFrame(\n data={\n \"node_id\": [6],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(data={\"node_id\": [1], \"flow_rate\": [1e-3]})\n)\n\nSetup flow boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [4],\n \"level\": [1.0], # Not relevant\n }\n )\n)\n\nSetup PID control:\n\npid_control = ribasim.PidControl(\n time=pd.DataFrame(\n data={\n \"node_id\": 4 * [5, 7],\n \"time\": [\n \"2020-01-01 00:00:00\",\n \"2020-01-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n ],\n \"listen_node_id\": 4 * [2, 2],\n \"target\": [5.0, 5.0, 5.0, 5.0, 7.5, 7.5, 7.5, 7.5],\n \"proportional\": 4 * [-1e-3, 1e-3],\n \"integral\": 4 * [-1e-7, 1e-7],\n \"derivative\": 4 * [0.0, 0.0],\n }\n )\n)\n\nNote that the coefficients for the pump and the outlet are equal in magnitude but opposite in sign. This way the pump and the outlet equally work towards the same goal, while having opposite effects on the controlled basin due to their connectivity to this basin.\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"pid_control\",\n node=node,\n edge=edge,\n basin=basin,\n flow_boundary=flow_boundary,\n level_boundary=level_boundary,\n pump=pump,\n outlet=outlet,\n pid_control=pid_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2020-12-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"pid_control\")\n\nNow run the model with ribasim pid_control/pid_control.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"pid_control/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\nax = df_basin_wide[\"level\"].plot()\nax.set_ylabel(\"level [m]\")\n\n# Plot target level\ntarget_levels = model.pid_control.time.target.to_numpy()[::2]\ntimes = date2num(model.pid_control.time.time)[::2]\nax.plot(times, target_levels, color=\"k\", ls=\":\", label=\"target level\");" + "text": "1 Basic model with static forcing\n\nimport geopandas as gpd\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nimport ribasim\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1, 3, 3, 6, 6, 9, 9],\n \"area\": [0.01, 1000.0] * 4,\n \"level\": [0.0, 1.0] * 4,\n }\n)\n\n# Convert steady forcing to m/s\n# 2 mm/d precipitation, 1 mm/d evaporation\nseconds_in_day = 24 * 3600\nprecipitation = 0.002 / seconds_in_day\nevaporation = 0.001 / seconds_in_day\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [0],\n \"drainage\": [0.0],\n \"potential_evaporation\": [evaporation],\n \"infiltration\": [0.0],\n \"precipitation\": [precipitation],\n \"urban_runoff\": [0.0],\n }\n)\nstatic = static.iloc[[0, 0, 0, 0]]\nstatic[\"node_id\"] = [1, 3, 6, 9]\n\nbasin = ribasim.Basin(profile=profile, static=static)\n\nSetup linear resistance:\n\nlinear_resistance = ribasim.LinearResistance(\n static=pd.DataFrame(\n data={\"node_id\": [10, 12], \"resistance\": [5e3, (3600.0 * 24) / 100.0]}\n )\n)\n\nSetup Manning resistance:\n\nmanning_resistance = ribasim.ManningResistance(\n static=pd.DataFrame(\n data={\n \"node_id\": [2],\n \"length\": [900.0],\n \"manning_n\": [0.04],\n \"profile_width\": [6.0],\n \"profile_slope\": [3.0],\n }\n )\n)\n\nSet up a rating curve node:\n\n# Discharge: lose 1% of storage volume per day at storage = 1000.0.\nq1000 = 1000.0 * 0.01 / seconds_in_day\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\n \"node_id\": [4, 4],\n \"level\": [0.0, 1.0],\n \"discharge\": [0.0, q1000],\n }\n )\n)\n\nSetup fractional flows:\n\nfractional_flow = ribasim.FractionalFlow(\n static=pd.DataFrame(\n data={\n \"node_id\": [5, 8, 13],\n \"fraction\": [0.3, 0.6, 0.1],\n }\n )\n)\n\nSetup pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [7],\n \"flow_rate\": [0.5 / 3600],\n }\n )\n)\n\nSetup level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [11, 17],\n \"level\": [0.5, 1.5],\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [15, 16],\n \"flow_rate\": [1e-4, 1e-4],\n }\n )\n)\n\nSetup terminal:\n\nterminal = ribasim.Terminal(\n static=pd.DataFrame(\n data={\n \"node_id\": [14],\n }\n )\n)\n\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin,\n (1.0, 0.0), # 2: ManningResistance\n (2.0, 0.0), # 3: Basin\n (3.0, 0.0), # 4: TabulatedRatingCurve\n (3.0, 1.0), # 5: FractionalFlow\n (3.0, 2.0), # 6: Basin\n (4.0, 1.0), # 7: Pump\n (4.0, 0.0), # 8: FractionalFlow\n (5.0, 0.0), # 9: Basin\n (6.0, 0.0), # 10: LinearResistance\n (2.0, 2.0), # 11: LevelBoundary\n (2.0, 1.0), # 12: LinearResistance\n (3.0, -1.0), # 13: FractionalFlow\n (3.0, -2.0), # 14: Terminal\n (3.0, 3.0), # 15: FlowBoundary\n (0.0, 1.0), # 16: FlowBoundary\n (6.0, 1.0), # 17: LevelBoundary\n ]\n)\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_id, node_type = ribasim.Node.get_node_ids_and_types(\n basin,\n manning_resistance,\n rating_curve,\n pump,\n fractional_flow,\n linear_resistance,\n level_boundary,\n flow_boundary,\n terminal,\n)\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(node_id, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array(\n [1, 2, 3, 4, 4, 5, 6, 8, 7, 9, 11, 12, 4, 13, 15, 16, 10], dtype=np.int64\n)\nto_id = np.array(\n [2, 3, 4, 5, 8, 6, 7, 9, 9, 10, 12, 3, 13, 14, 6, 1, 17], dtype=np.int64\n)\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": len(from_id) * [\"flow\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"basic\",\n node=node,\n edge=edge,\n basin=basin,\n level_boundary=level_boundary,\n flow_boundary=flow_boundary,\n pump=pump,\n linear_resistance=linear_resistance,\n manning_resistance=manning_resistance,\n tabulated_rating_curve=rating_curve,\n fractional_flow=fractional_flow,\n terminal=terminal,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"basic\")\n\n\n\n2 Update the basic model with transient forcing\nThis assumes you have already created the basic model with static forcing.\n\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport ribasim\n\n\nmodel = ribasim.Model.from_toml(datadir / \"basic/basic.toml\")\n\n\ntime = pd.date_range(model.starttime, model.endtime)\nday_of_year = time.day_of_year.to_numpy()\nseconds_per_day = 24 * 60 * 60\nevaporation = (\n (-1.0 * np.cos(day_of_year / 365.0 * 2 * np.pi) + 1.0) * 0.0025 / seconds_per_day\n)\nrng = np.random.default_rng(seed=0)\nprecipitation = (\n rng.lognormal(mean=-1.0, sigma=1.7, size=time.size) * 0.001 / seconds_per_day\n)\n\nWe’ll use xarray to easily broadcast the values.\n\ntimeseries = (\n pd.DataFrame(\n data={\n \"node_id\": 1,\n \"time\": time,\n \"drainage\": 0.0,\n \"potential_evaporation\": evaporation,\n \"infiltration\": 0.0,\n \"precipitation\": precipitation,\n \"urban_runoff\": 0.0,\n }\n )\n .set_index(\"time\")\n .to_xarray()\n)\n\nbasin_ids = model.basin.static[\"node_id\"].to_numpy()\nbasin_nodes = xr.DataArray(\n np.ones(len(basin_ids)), coords={\"node_id\": basin_ids}, dims=[\"node_id\"]\n)\nforcing = (timeseries * basin_nodes).to_dataframe().reset_index()\n\n\nstate = pd.DataFrame(\n data={\n \"node_id\": basin_ids,\n \"level\": 1.4,\n \"concentration\": 0.0,\n }\n)\n\n\nmodel.basin.forcing = forcing\nmodel.basin.state = state\n\n\nmodel.modelname = \"basic_transient\"\nmodel.write(datadir / \"basic_transient\")\n\nNow run the model with ribasim basic-transient/basic.toml. After running the model, read back the output:\n\ndf_basin = pd.read_feather(datadir / \"basic_transient/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\ndf_basin_wide[\"level\"].plot()\n\n<Axes: xlabel='time'>\n\n\n\n\n\n\ndf_flow = pd.read_feather(datadir / \"basic_transient/output/flow.arrow\")\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow * 86400\nax = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\").plot()\nax.legend(bbox_to_anchor=(1.3, 1), title=\"Edge\")\n\n<matplotlib.legend.Legend at 0x7fc2f5d90250>\n\n\n\n\n\n\ntype(df_flow)\n\npandas.core.frame.DataFrame\n\n\n\n\n3 Model with discrete control\nThe model constructed below consists of a single basin which slowly drains trough a TabulatedRatingCurve, but is held within a range around a target level (setpoint) by two connected pumps. These two pumps behave like a reversible pump. When pumping can be done in only one direction, and the other direction is only possible under gravity, use an Outlet for that direction.\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin\n (1.0, 0.5), # 2: Pump\n (1.0, -0.5), # 3: Pump\n (2.0, 0.0), # 4: LevelBoundary\n (-1.0, 0.0), # 5: TabulatedRatingCurve\n (-2.0, 0.0), # 6: Terminal\n (1.0, 0.0), # 7: DiscreteControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"Basin\",\n \"Pump\",\n \"Pump\",\n \"LevelBoundary\",\n \"TabulatedRatingCurve\",\n \"Terminal\",\n \"DiscreteControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 3, 4, 2, 1, 5, 7, 7], dtype=np.int64)\nto_id = np.array([3, 4, 2, 1, 5, 6, 2, 3], dtype=np.int64)\n\nedge_type = 6 * [\"flow\"] + 2 * [\"control\"]\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\"from_node_id\": from_id, \"to_node_id\": to_id, \"edge_type\": edge_type},\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1],\n \"area\": [1000.0, 1000.0],\n \"level\": [0.0, 1.0],\n }\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [1],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(data={\"node_id\": [1], \"level\": [20.0]})\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the discrete control:\n\ncondition = pd.DataFrame(\n data={\n \"node_id\": 3 * [7],\n \"listen_feature_id\": 3 * [1],\n \"variable\": 3 * [\"level\"],\n \"greater_than\": [5.0, 10.0, 15.0], # min, setpoint, max\n }\n)\n\nlogic = pd.DataFrame(\n data={\n \"node_id\": 5 * [7],\n \"truth_state\": [\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n \"control_state\": [\"in\", \"in\", \"none\", \"out\", \"out\"],\n }\n)\n\ndiscrete_control = ribasim.DiscreteControl(condition=condition, logic=logic)\n\nThe above control logic can be summarized as follows: - If the level gets above the maximum, activate the control state “out” until the setpoint is reached; - If the level gets below the minimum, active the control state “in” until the setpoint is reached; - Otherwise activate the control state “none”.\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": 3 * [2] + 3 * [3],\n \"control_state\": 2 * [\"none\", \"in\", \"out\"],\n \"flow_rate\": [0.0, 2e-3, 0.0, 0.0, 0.0, 2e-3],\n }\n )\n)\n\nThe pump data defines the following:\n\n\n\nControl state\nPump #2 flow rate (m/s)\nPump #3 flow rate (m/s)\n\n\n\n\n“none”\n0.0\n0.0\n\n\n“in”\n2e-3\n0.0\n\n\n“out”\n0.0\n2e-3\n\n\n\nSetup the level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(data={\"node_id\": [4], \"level\": [10.0]})\n)\n\nSetup the rating curve:\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\"node_id\": 2 * [5], \"level\": [2.0, 15.0], \"discharge\": [0.0, 1e-3]}\n )\n)\n\nSetup the terminal:\n\nterminal = ribasim.Terminal(static=pd.DataFrame(data={\"node_id\": [6]}))\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"level_setpoint_with_minmax\",\n node=node,\n edge=edge,\n basin=basin,\n pump=pump,\n level_boundary=level_boundary,\n tabulated_rating_curve=rating_curve,\n terminal=terminal,\n discrete_control=discrete_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nListen edges are plotted with a dashed line since they are not present in the “Edge / static” schema but only in the “Control / condition” schema.\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"level_setpoint_with_minmax\")\n\nNow run the model with level_setpoint_with_minmax/level_setpoint_with_minmax.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"level_setpoint_with_minmax/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\n\nax = df_basin_wide[\"level\"].plot()\n\ngreater_than = model.discrete_control.condition.greater_than\n\nax.hlines(\n greater_than,\n df_basin.time[0],\n df_basin.time.max(),\n lw=1,\n ls=\"--\",\n color=\"k\",\n)\n\ndf_control = pd.read_feather(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\ny_min, y_max = ax.get_ybound()\nax.fill_between(df_control.time[:2], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\nax.fill_between(df_control.time[2:4], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\n\nax.set_xticks(\n date2num(df_control.time).tolist(),\n df_control.control_state.tolist(),\n rotation=50,\n)\n\nax.set_yticks(greater_than, [\"min\", \"setpoint\", \"max\"])\nax.set_ylabel(\"level\")\nplt.show()\n\n\n\n\nThe highlighted regions show where a pump is active.\nLet’s print an overview of what happened with control:\n\nmodel.print_discrete_control_record(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\n0. At 2020-01-01 00:00:00 the control node with ID 7 reached truth state TTT:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level > 15.0\n\n This yielded control state \"out\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.002\n\n1. At 2020-02-09 01:17:29.324000 the control node with ID 7 reached truth state TFF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n2. At 2020-07-05 13:24:51.165000 the control node with ID 7 reached truth state FFF:\n For node ID 1 (Basin): level < 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"in\":\n For node ID 2 (Pump): flow_rate = 0.002\n For node ID 3 (Pump): flow_rate = 0.0\n\n3. At 2020-08-11 11:49:59.015000 the control node with ID 7 reached truth state TTF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n\n\nNote that crossing direction specific truth states (containing “U”, “D”) are not present in this overview even though they are part of the control logic. This is because in the control logic for this model these truth states are only used to sustain control states, while the overview only shows changes in control states.\n\n\n4 Model with PID control\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: FlowBoundary\n (1.0, 0.0), # 2: Basin\n (2.0, 0.5), # 3: Pump\n (3.0, 0.0), # 4: LevelBoundary\n (1.5, 1.0), # 5: PidControl\n (2.0, -0.5), # 6: outlet\n (1.5, -1.0), # 7: PidControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"FlowBoundary\",\n \"Basin\",\n \"Pump\",\n \"LevelBoundary\",\n \"PidControl\",\n \"Outlet\",\n \"PidControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 2, 3, 4, 6, 5, 7], dtype=np.int64)\nto_id = np.array([2, 3, 4, 6, 2, 3, 6], dtype=np.int64)\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": 5 * [\"flow\"] + 2 * [\"control\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\"node_id\": [2, 2], \"level\": [0.0, 1.0], \"area\": [1000.0, 1000.0]}\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"level\": [6.0],\n }\n)\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [3],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup the outlet:\n\noutlet = ribasim.Outlet(\n static=pd.DataFrame(\n data={\n \"node_id\": [6],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(data={\"node_id\": [1], \"flow_rate\": [1e-3]})\n)\n\nSetup flow boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [4],\n \"level\": [1.0], # Not relevant\n }\n )\n)\n\nSetup PID control:\n\npid_control = ribasim.PidControl(\n time=pd.DataFrame(\n data={\n \"node_id\": 4 * [5, 7],\n \"time\": [\n \"2020-01-01 00:00:00\",\n \"2020-01-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n ],\n \"listen_node_id\": 4 * [2, 2],\n \"target\": [5.0, 5.0, 5.0, 5.0, 7.5, 7.5, 7.5, 7.5],\n \"proportional\": 4 * [-1e-3, 1e-3],\n \"integral\": 4 * [-1e-7, 1e-7],\n \"derivative\": 4 * [0.0, 0.0],\n }\n )\n)\n\nNote that the coefficients for the pump and the outlet are equal in magnitude but opposite in sign. This way the pump and the outlet equally work towards the same goal, while having opposite effects on the controlled basin due to their connectivity to this basin.\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"pid_control\",\n node=node,\n edge=edge,\n basin=basin,\n flow_boundary=flow_boundary,\n level_boundary=level_boundary,\n pump=pump,\n outlet=outlet,\n pid_control=pid_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2020-12-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"pid_control\")\n\nNow run the model with ribasim pid_control/pid_control.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"pid_control/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\nax = df_basin_wide[\"level\"].plot()\nax.set_ylabel(\"level [m]\")\n\n# Plot target level\ntarget_levels = model.pid_control.time.target.to_numpy()[::2]\ntimes = date2num(model.pid_control.time.time)[::2]\nax.plot(times, target_levels, color=\"k\", ls=\":\", label=\"target level\");" }, { "objectID": "python/reference/Pump.html",