From 79151fbe40e60fcae4b77a797cea3636fdddbe5c Mon Sep 17 00:00:00 2001 From: visr Date: Wed, 17 Apr 2024 12:15:52 +0000 Subject: [PATCH] deploy: 6facfda2ee2d984d6c57490b283b051102234637 --- build/index.html | 388 ++++++++++++++++++++-------------------- core/allocation.html | 38 ++-- core/equations.html | 4 +- core/validation.html | 4 +- python/examples.html | 2 +- python/test-models.html | 2 +- search.json | 6 +- 7 files changed, 222 insertions(+), 222 deletions(-) diff --git a/build/index.html b/build/index.html index 6f0600c20..b3f098b1f 100644 --- a/build/index.html +++ b/build/index.html @@ -278,11 +278,11 @@

solve!
  • write_results
  • -

    source

    +

    source

    # Ribasim.configModule.

    module config

    Ribasim.config is a submodule of Ribasim to handle the configuration of a Ribasim model. It is implemented using the Configurations package. A full configuration is represented by Config, which is the main API. Ribasim.config is a submodule mainly to avoid name clashes between the configuration sections and the rest of Ribasim.

    -

    source

    +

    source

    @@ -290,18 +290,18 @@

    1.2 Types

    # Ribasim.AllocationType.

    Object for all information about allocation allocationnetworkids: The unique sorted allocation network IDs allocation models: The allocation models for the main network and subnetworks corresponding to allocationnetworkids mainnetworkconnections: (fromid, toid) from the main network to the subnetwork per subnetwork priorities: All used priority values. subnetworkdemands: The demand of an edge from the main network to a subnetwork recorddemand: A record of demands and allocated flows for nodes that have these. record_flow: A record of all flows computed by allocation optimization, eventually saved to output file

    -

    source

    +

    source

    # Ribasim.AllocationModelType.

    Store information for a subnetwork used for allocation.

    allocationnetworkid: The ID of this allocation network capacity: The capacity per edge of the allocation network, as constrained by nodes that have a maxflowrate problem: The JuMP.jl model for solving the allocation problem Δt_allocation: The time interval between consecutive allocation solves

    -

    source

    +

    source

    # Ribasim.AllocationModelMethod.

    Construct the JuMP.jl problem for allocation.

    Inputs

    allocationnetworkid: the ID of this allocation network p: Ribasim problem parameters Δt_allocation: The timestep between successive allocation solves

    Outputs

    An AllocationModel object.

    -

    source

    +

    source

    # Ribasim.BasinType.

    Requirements:

      @@ -311,25 +311,25 @@

      source

      +

      source

      # Ribasim.DiscreteControlType.

      nodeid: node ID of the DiscreteControl node per compound variable (can contain repeats) listennodeid: the IDs of the nodes being condition on per compound variable variable: the names of the variables in the condition per compound variable weight: the weight of the variables in the condition per compound variable lookahead: the look ahead of variables in the condition in seconds per compoundvariable greaterthan: The threshold values per compound variable conditionvalue: The current truth value of each condition per compoundvariable per greaterthan 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 results

      -

      source

      +

      source

      # Ribasim.EdgeMetadataType.

      Type for storing metadata of edges in the graph: id: ID of the edge (only used for labeling flow output) type: type of the edge allocationnetworkidsource: ID of allocation network where this edge is a source (0 if not a source) fromid: the node ID of the source node toid: the node ID of the destination node allocationflow: whether this edge has a flow in an allocation network node_ids: if this edge has allocation flow, these are all the nodes from the physical layer this edge consists of

      -

      source

      +

      source

      # Ribasim.FlatVectorType.

      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.

      -

      source

      +

      source

      # Ribasim.FlatVectorMethod.

      Construct a FlatVector from one of the fields of SavedFlow.

      -

      source

      +

      source

      # Ribasim.FlowBoundaryType.

      nodeid: node ID of the FlowBoundary node active: whether this node is active and thus contributes flow flowrate: target flow rate

      -

      source

      +

      source

      # Ribasim.FractionalFlowType.

      Requirements:

        @@ -338,16 +338,16 @@

        source

        +

        source

        # Ribasim.InNeighborsType.

        Iterate over incoming neighbors of a given label in a MetaGraph, only for edges of edge_type

        -

        source

        +

        source

        # Ribasim.LevelBoundaryType.

        node_id: node ID of the LevelBoundary node active: whether this node is active level: the fixed level of this ‘infinitely big basin’

        -

        source

        +

        source

        # Ribasim.LevelDemandType.

        nodeid: node ID of the LevelDemand node minlevel: The minimum target level of the connected basin(s) max_level: The maximum target level of the connected basin(s) priority: If in a shortage state, the priority of the demand of the connected basin(s)

        -

        source

        +

        source

        # Ribasim.LinearResistanceType.

        Requirements:

          @@ -355,7 +355,7 @@

          source

          +

          source

          # Ribasim.ManningResistanceType.

          This is a simple Manning-Gauckler reach connection.

            @@ -385,29 +385,29 @@

            source

            +

            source

            # Ribasim.ModelType.

            Model(config_path::AbstractString)
             Model(config::Config)

            Initialize a Model.

            The Model struct is an initialized model, combined with the Config used to create it and saved results. The Basic Model Interface (BMI) is implemented on the Model. A Model can be created from the path to a TOML configuration file, or a Config object.

            -

            source

            +

            source

            # Ribasim.NodeMetadataType.

            Type for storing metadata of nodes in the graph type: type of the node allocationnetworkid: Allocation network ID (0 if not in subnetwork)

            -

            source

            +

            source

            # Ribasim.OutNeighborsType.

            Iterate over outgoing neighbors of a given label in a MetaGraph, only for edges of edge_type

            -

            source

            +

            source

            # Ribasim.OutletType.

            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

            -

            source

            +

            source

            # Ribasim.PidControlType.

            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

            -

            source

            +

            source

            # Ribasim.PumpType.

            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

            -

            source

            +

            source

            # Ribasim.SavedFlowType.

            In-memory storage of saved mean flows for writing to results.

              @@ -415,26 +415,26 @@

              source

              +

              source

              # Ribasim.SubgridType.

              Subgrid linearly interpolates basin levels.

              -

              source

              +

              source

              # Ribasim.TabulatedRatingCurveType.

              struct TabulatedRatingCurve{C}

              Rating curve from level to flow rate. 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

              -

              source

              +

              source

              # Ribasim.TerminalType.

              node_id: node ID of the Terminal node

              -

              source

              +

              source

              # Ribasim.UserDemandType.

              active: whether this node is active and thus demands water realizedbmi: Cumulative inflow volume, for read or reset by BMI only demand: water flux demand of UserDemand per priority over time Each UserDemand has a demand for all priorities, which is 0.0 if it is not provided explicitly. demandreduced: the total demand reduced by allocated flows. This is used for goal programming, and requires separate memory from demand since demands can come from the BMI demanditp: Timeseries interpolation objects for demands demandfromtimeseries: If false the demand comes from the BMI or is fixed allocated: water flux currently allocated to UserDemand per priority returnfactor: the factor in [0,1] of how much of the abstracted water is given back to the system min_level: The level of the source basin below which the UserDemand does not abstract

              -

              source

              +

              source

              # Ribasim.config.ConfigMethod.

              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.

              -

              source

              +

              source

              @@ -443,212 +443,212 @@

              # BasicModelInterface.finalizeMethod.

              BMI.finalize(model::Model)::Model

              Write all results to the configured files.

              -

              source

              +

              source

              # BasicModelInterface.initializeMethod.

              BMI.initialize(T::Type{Model}, config_path::AbstractString)::Model

              Initialize a Model from the path to the TOML configuration file.

              -

              source

              +

              source

              # CommonSolve.solve!Method.

              solve!(model::Model)::Model

              Solve a Model until the configured endtime.

              -

              source

              +

              source

              # CommonSolve.step!Method.

              step!(model::Model, dt::Float64)::Model

              Take Model timesteps until t + dt is reached exactly.

              -

              source

              +

              source

              # Ribasim.add_basin_term!Method.

              Add a term to the expression of the objective function corresponding to the demand of a basin.

              -

              source

              +

              source

              # Ribasim.add_constraints_absolute_value!Method.

              Minimizing |expr| can be achieved by introducing a new variable exprabs and posing the following constraints: exprabs >= expr expr_abs >= -expr

              -

              source

              +

              source

              # Ribasim.add_constraints_absolute_value_flow_demand!Method.

              Add constraints so that variables Fabsflow_demand act as the absolute value of the expression comparing flow to a flow buffer to the flow demand.

              -

              source

              +

              source

              # Ribasim.add_constraints_absolute_value_level_demand!Method.

              Add constraints so that variables Fabslevel_demand act as the absolute value of the expression comparing flow to a basin to its demand.

              -

              source

              +

              source

              # Ribasim.add_constraints_absolute_value_user_demand!Method.

              Add constraints so that variables Fabsuser_demand act as the absolute value of the expression comparing flow to a UserDemand to its demand.

              -

              source

              +

              source

              # Ribasim.add_constraints_basin_flow!Method.

              Add the Basin flow constraints to the allocation problem. The constraint indices are the Basin node IDs.

              Constraint: flow out of basin <= basin capacity

              -

              source

              +

              source

              # Ribasim.add_constraints_buffer!Method.

              Add the buffer outflow constraints to the allocation problem. The constraint indices are the node IDs of the nodes that have a flow demand.

              Constraint: flow out of buffer <= flow buffer capacity

              -

              source

              +

              source

              # Ribasim.add_constraints_capacity!Method.

              Add the flow capacity constraints to the allocation problem. Only finite capacities get a constraint. The constraint indices are (edgesourceid, edgedstid).

              Constraint: flow over edge <= edge capacity

              -

              source

              +

              source

              # Ribasim.add_constraints_conservation_basin!Method.

              Add the basin flow conservation constraints to the allocation problem. The constraint indices are Basin node IDs.

              Constraint: sum(flows out of basin) == sum(flows into basin) + flow from storage and vertical fluxes

              -

              source

              +

              source

              # Ribasim.add_constraints_conservation_flow_demand!Method.

              Add the conservation constraints for connector nodes with a flow demand to the allocation problem. The constraint indices are node IDs of the nodes with the flow demand (so not the IDs of the FlowDemand nodes).

              Constraint: flow into node + flow out of buffer = flow out of node + flow into buffer

              -

              source

              +

              source

              # Ribasim.add_constraints_conservation_subnetwork!Method.

              Add the subnetwork inlet flow conservation constraints to the allocation problem. The constraint indices are node IDs subnetwork inlet edge dst IDs.

              Constraint: sum(flows into node) == sum(flows out of node)

              -

              source

              +

              source

              # Ribasim.add_constraints_flow_demand_outflow!Method.

              Add the flow demand node outflow constraints to the allocation problem. The constraint indices are the node IDs of the nodes that have a flow demand.

              Constraint: flow out of node with flow demand <= ∞ if not at flow demand priority, 0.0 otherwise

              -

              source

              +

              source

              # Ribasim.add_constraints_fractional_flow!Method.

              Add the fractional flow constraints to the allocation problem. The constraint indices are allocation edges over a fractional flow node.

              Constraint: flow after fractional_flow node <= fraction * inflow

              -

              source

              +

              source

              # Ribasim.add_constraints_source!Method.

              Add the source constraints to the allocation problem. The actual threshold values will be set before each allocation solve. The constraint indices are (edgesourceid, edgedstid).

              Constraint: flow over source edge <= source flow in subnetwork

              -

              source

              +

              source

              # Ribasim.add_constraints_user_source!Method.

              Add capacity constraints to the outflow edge of UserDemand nodes. The constraint indices are the UserDemand node IDs.

              Constraint: flow over UserDemand edge outflow edge <= cumulative return flow from previous priorities

              -

              source

              +

              source

              # Ribasim.add_flow_demand_term!Method.

              Add a term to the expression of the objective function corresponding to the demand of a node with a a flow demand.

              -

              source

              +

              source

              # Ribasim.add_objective_term!Function.

              Add a term to the objective function given by the objective type, depending in the provided flow variable and the associated demand.

              -

              source

              +

              source

              # Ribasim.add_subnetwork_connections!Method.

              Add the edges connecting the main network work to a subnetwork to both the main network and subnetwork allocation network.

              -

              source

              +

              source

              # Ribasim.add_user_demand_term!Method.

              Add a term to the expression of the objective function corresponding to the demand of a UserDemand.

              -

              source

              +

              source

              # Ribasim.add_variables_absolute_value!Method.

              Certain allocation distribution types use absolute values in the objective function. Since most optimization packages do not support the absolute value function directly, New variables are introduced that act as the absolute value of an expression by posing the appropriate constraints.

              -

              source

              +

              source

              # Ribasim.add_variables_basin!Method.

              Add the variables for supply/demand of a basin to the problem. The variable indices are the node_ids of the basins with a level demand in the subnetwork.

              -

              source

              +

              source

              # Ribasim.add_variables_flow!Method.

              Add the flow variables F to the allocation problem. The variable indices are (edgesourceid, edgedstid). Non-negativivity constraints are also immediately added to the flow variables.

              -

              source

              +

              source

              # Ribasim.add_variables_flow_buffer!Method.

              Add the variables for supply/demand of a node with a flow demand to the problem. The variable indices are the node_ids of the nodes with a flow demand in the subnetwork.

              -

              source

              +

              source

              # Ribasim.adjust_capacities_basin!Method.

              Set the values of the basin outflows. 2 cases:

              • Before the first allocation solve, set the capacities to their full capacity if there is surplus storage;
              • Before an allocation solve, subtract the flow used by allocation for the previous priority from the capacities.
              -

              source

              +

              source

              # Ribasim.adjust_capacities_buffer!Method.

              Increase the capacities of the flow buffers of nodes with a flow demand by the inflow to the respective buffers.

              -

              source

              +

              source

              # Ribasim.adjust_capacities_edge!Method.

              Set the values of the edge capacities. 2 cases:

              • Before the first allocation solve, set the edge capacities to their full capacity;
              • Before an allocation solve, subtract the flow used by allocation for the previous priority from the edge capacities.
              -

              source

              +

              source

              # Ribasim.adjust_capacities_returnflow!Method.

              Add the return flow fraction of the inflow to the UserDemand nodes to the capacity of the outflow source.

              -

              source

              +

              source

              # Ribasim.adjust_capacities_source!Method.

              Adjust the source capacities by the flow used from the sources.

              -

              source

              +

              source

              # Ribasim.adjust_demands_flow!Method.

              Reduce the flow demand based on flow trough the node with the demand. Flow from any priority counts.

              -

              source

              +

              source

              # Ribasim.adjust_demands_level!Method.

              Subtract the allocated flow to the basin from its demand, to obtain the reduced demand used for goal programming

              -

              source

              +

              source

              # Ribasim.adjust_demands_user!Method.

              Set the demand of the flow demand nodes. 2 cases:

              • Before the first allocation solve, set the demands to their full value;
              • Before an allocation solve, subtract the flow trough the node with a flow demand from the total flow demand (which will be used at the priority of the flow demand only).
              -

              source

              +

              source

              # Ribasim.all_neighbor_labels_typeMethod.

              Get the in- and outneighbor node IDs of the given node ID (label) over the given edge type in the graph.

              -

              source

              +

              source

              # Ribasim.allocate!Method.

              Update the allocation optimization problem for the given subnetwork with the problem state and flows, solve the allocation problem and assign the results to the UserDemand.

              -

              source

              +

              source

              # Ribasim.allocation_graphMethod.

              Build the graph used for the allocation problem.

              -

              source

              +

              source

              # Ribasim.allocation_graph_used_nodes!Method.

              Find all nodes in the subnetwork which will be used in the allocation network. Some nodes are skipped to optimize allocation optimization.

              -

              source

              +

              source

              # Ribasim.allocation_problemMethod.

              Construct the allocation problem for the current subnetwork as a JuMP.jl model.

              -

              source

              +

              source

              # Ribasim.allocation_tableMethod.

              Create an allocation result table for the saved data

              -

              source

              +

              source

              # Ribasim.assign_allocations!Method.

              Assign the allocations to the UserDemand as determined by the solution of the allocation problem.

              -

              source

              +

              source

              # Ribasim.basin_bottomMethod.

              Return the bottom elevation of the basin with index i, or nothing if it doesn’t exist

              -

              source

              +

              source

              # Ribasim.basin_tableMethod.

              Create the basin result table from the saved data

              -

              source

              +

              source

              # Ribasim.create_callbacksMethod.

              Create the different callbacks that are used to store results 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.

              -

              source

              +

              source

              # Ribasim.create_graphMethod.

              Return a directed metagraph with data of nodes (NodeMetadata): NodeMetadata

              and data of edges (EdgeMetadata): EdgeMetadata

              -

              source

              +

              source

              # Ribasim.create_storage_tablesMethod.

              Read the Basin / profile table and return all area and level and computed storage values

              -

              source

              +

              source

              # Ribasim.datetime_sinceMethod.

              datetime_since(t::Real, t0::DateTime)::DateTime

              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.

              -

              source

              +

              source

              # Ribasim.datetimesMethod.

              Get all saved times as a Vector{DateTime}

              -

              source

              +

              source

              # Ribasim.discrete_control_affect!Method.

              Change parameters based on the control logic.

              -

              source

              +

              source

              # Ribasim.discrete_control_condition!Method.

              Update discrete control condition truths.

              -

              source

              +

              source

              # Ribasim.discrete_control_tableMethod.

              Create a discrete control result table from the saved data

              -

              source

              +

              source

              # Ribasim.expand_logic_mappingMethod.

              Replace the truth states in the logic mapping which contain wildcards with all possible explicit truth states.

              -

              source

              +

              source

              # Ribasim.find_allocation_graph_edges!Method.

              This loop finds allocation network edges in several ways:

              • Between allocation network nodes whose equivalent in the subnetwork are directly connected
              • Between allocation network nodes whose equivalent in the subnetwork are connected with one or more allocation network nodes in between
              -

              source

              +

              source

              # Ribasim.find_subnetwork_connections!Method.

              Find the edges from the main network to a subnetwork.

              -

              source

              +

              source

              # Ribasim.findlastgroupMethod.

              For an element 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.

              -

              source

              +

              source

              # Ribasim.findsortedMethod.

              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.

              -

              source

              +

              source

              # Ribasim.flow_tableMethod.

              Create a flow result table from the saved data

              -

              source

              +

              source

              # Ribasim.formulate_flow!Method.

              Directed graph: outflow is positive!

              -

              source

              +

              source

              # Ribasim.formulate_flow!Method.

              Conservation of energy for two basins, a and b:

              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)
              @@ -676,16 +676,16 @@

              source

              +

              source

              # Ribasim.formulate_flow!Method.

              Directed graph: outflow is positive!

              -

              source

              +

              source

              # Ribasim.get_area_and_levelMethod.

              Compute the area and level of a basin given its storage. Also returns darea/dlevel as it is needed for the Jacobian.

              -

              source

              +

              source

              # Ribasim.get_basin_capacityMethod.

              Get the capacity of the basin, i.e. the maximum flow that can be abstracted from the basin if it is in a state of surplus storage (0 if no reference levels are provided by a level_demand node). Storages are converted to flows by dividing by the allocation timestep.

              -

              source

              +

              source

              # Ribasim.get_basin_dataMethod.

              Get several variables associated with a basin:

                @@ -695,135 +695,135 @@

                source

                +

                source

                # Ribasim.get_basin_demandMethod.

                Get the demand of the basin, i.e. how large a flow the basin needs to get to its minimum target level (0 if no reference levels are provided by a level_demand node). Storages are converted to flows by dividing by the allocation timestep.

                -

                source

                +

                source

                # Ribasim.get_chunk_sizesMethod.

                Get the chunk sizes for DiffCache; differentiation w.r.t. u and t (the latter only if a Rosenbrock algorithm is used).

                -

                source

                +

                source

                # Ribasim.get_compressorMethod.

                Get the compressor based on the Results section

                -

                source

                +

                source

                # Ribasim.get_flowMethod.

                Get the flow over the given edge (val is needed for get_tmp from ForwardDiff.jl).

                -

                source

                +

                source

                # Ribasim.get_fractional_flow_connected_basinsMethod.

                Get the node type specific indices of the fractional flows and basins, that are consecutively connected to a node of given id.

                -

                source

                +

                source

                # Ribasim.get_jac_prototypeMethod.

                Get a sparse matrix whose sparsity matches (with some false positives) 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.

                -

                source

                +

                source

                # Ribasim.get_levelMethod.

                Get the current water level of a node ID. The ID can belong to either a Basin or a LevelBoundary. storage: tells ForwardDiff whether this call is for differentiation or not

                -

                source

                +

                source

                # Ribasim.get_scalar_interpolationMethod.

                Linear interpolation of a scalar with constant extrapolation.

                -

                source

                +

                source

                # Ribasim.get_storage_from_levelMethod.

                Get the storage of a basin from its level.

                -

                source

                +

                source

                # Ribasim.get_storages_and_levelsMethod.

                Get the storage and level of all basins as matrices of nbasin × ntime

                -

                source

                +

                source

                # Ribasim.get_storages_from_levelsMethod.

                Compute the storages of the basins based on the water level of the basins.

                -

                source

                +

                source

                # Ribasim.get_tstopsMethod.

                From an iterable of DateTimes, find the times the solver needs to stop

                -

                source

                +

                source

                # Ribasim.get_valueMethod.

                Get a value for a condition. Currently supports getting levels from basins and flows from flow boundaries.

                -

                source

                +

                source

                # Ribasim.get_ΔtMethod.

                Get the time interval between (flow) saves

                -

                source

                +

                source

                # Ribasim.id_indexMethod.

                Get the index of an ID in a set of indices.

                -

                source

                +

                source

                # Ribasim.indicate_allocation_flow!Method.

                Add to the edge metadata that the given edge is used for allocation flow. If the edge does not exist, it is created.

                -

                source

                +

                source

                # Ribasim.inflow_idMethod.

                Get the unique inneighbor over a flow edge.

                -

                source

                +

                source

                # Ribasim.inflow_idsMethod.

                Get the inneighbors over flow edges.

                -

                source

                +

                source

                # Ribasim.inflow_ids_allocationMethod.

                Get the inneighbors of the given ID such that the connecting edge is an allocation flow edge.

                -

                source

                +

                source

                # Ribasim.inneighbor_labels_typeMethod.

                Get the inneighbor node IDs of the given node ID (label) over the given edge type in the graph.

                -

                source

                +

                source

                # Ribasim.inoutflow_idsMethod.

                Get the in- and outneighbors over flow edges.

                -

                source

                +

                source

                # Ribasim.integrate_flows!Method.

                Integrate flows over the last timestep

                -

                source

                +

                source

                # Ribasim.is_allocation_sourceMethod.

                Find out whether the given edge is a source for an allocation network.

                -

                source

                +

                source

                # Ribasim.is_current_moduleMethod.

                is_current_module(log::LogMessageType)::Bool
                 Returns true if the log message is from the current module or a submodule.
                 
                 See https://github.com/JuliaLogging/LoggingExtras.jl/blob/d35e7c8cfc197853ee336ace17182e6ed36dca24/src/CompositionalLoggers/earlyfiltered.jl#L39
                 for the information available in log.
                -

                source

                +

                source

                # Ribasim.is_flow_constrainingMethod.

                Whether the given node node is flow constraining by having a maximum flow rate.

                -

                source

                +

                source

                # Ribasim.is_flow_direction_constrainingMethod.

                Whether the given node is flow direction constraining (only in direction of edges).

                -

                source

                +

                source

                # Ribasim.load_dataMethod.

                load_data(db::DB, config::Config, nodetype::Symbol, kind::Symbol)::Union{Table, Query, Nothing}

                Load data from Arrow files if available, otherwise the database. Returns either an Arrow.Table, SQLite.Query or nothing if the data is not present.

                -

                source

                +

                source

                # Ribasim.load_structvectorMethod.

                load_structvector(db::DB, config::Config, ::Type{T})::StructVector{T}

                Load data from Arrow files if available, otherwise the database. 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.

                -

                source

                +

                source

                # Ribasim.low_storage_factorMethod.

                If id is a Basin with storage below the threshold, return a reduction factor != 1

                -

                source

                +

                source

                # Ribasim.mainMethod.

                main(toml_path::AbstractString)::Cint
                 main(ARGS::Vector{String})::Cint
                 main()::Cint

                This is the main entry point of the application. Performs argument parsing and sets up logging for both terminal and file. Calls Ribasim.run() and handles exceptions to convert to exit codes.

                -

                source

                +

                source

                # Ribasim.metadata_from_edgeMethod.

                Get the metadata of an edge in the graph from an edge of the underlying DiGraph.

                -

                source

                +

                source

                # Ribasim.nodefieldsMethod.

                Get all node fieldnames of the parameter object.

                -

                source

                +

                source

                # Ribasim.nodetypeMethod.

                From a SchemaVersion(“ribasim.flowboundary.static”, 1) return (:FlowBoundary, :static)

                -

                source

                +

                source

                # Ribasim.outflow_idMethod.

                Get the unique outneighbor over a flow edge.

                -

                source

                +

                source

                # Ribasim.outflow_idsMethod.

                Get the outneighbors over flow edges.

                -

                source

                +

                source

                # Ribasim.outflow_ids_allocationMethod.

                Get the outneighbors of the given ID such that the connecting edge is an allocation flow edge.

                -

                source

                +

                source

                # Ribasim.outneighbor_labels_typeMethod.

                Get the outneighbor node IDs of the given node ID (label) over the given edge type in the graph.

                -

                source

                +

                source

                # Ribasim.parse_static_and_timeMethod.

                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.

                -

                source

                +

                source

                # Ribasim.pkgversionMethod.

                Get the package version of a given module

                -

                source

                +

                source

                # Ribasim.process_allocation_graph_edges!Method.

                For the composite allocation network edges:

                  @@ -831,133 +831,133 @@

                  source

                  +

                  source

                  # Ribasim.profile_storageMethod.

                  Calculate a profile storage by integrating the areas over the levels

                  -

                  source

                  +

                  source

                  # Ribasim.qh_interpolationMethod.

                  From a table with columns nodeid, flowrate (Q) and level (h), create a LinearInterpolation from level to flow rate for a given node_id.

                  -

                  source

                  +

                  source

                  # Ribasim.reduction_factorMethod.

                  Function that goes smoothly from 0 to 1 in the interval [0,threshold], and is constant outside this interval.

                  -

                  source

                  +

                  source

                  # Ribasim.runMethod.

                  run(config_file::AbstractString)::Model
                   run(config::Config)::Model

                  Run a Model, given a path to a TOML configuration file, or a Config object. Running a model includes initialization, solving to the end with [solve!](@ref) and writing results with write_results.

                  -

                  source

                  +

                  source

                  # Ribasim.save_allocation_flows!Method.

                  Save the allocation flows per basin and physical edge.

                  -

                  source

                  +

                  source

                  # Ribasim.save_demands_and_allocations!Method.

                  Save the demands and allocated flows for UserDemand and Basin. Note: Basin supply (negative demand) is only saved for the first priority.

                  -

                  source

                  +

                  source

                  # Ribasim.save_flowMethod.

                  Compute the average flows over the last saveat interval and write them to SavedValues

                  -

                  source

                  +

                  source

                  # Ribasim.save_subgrid_levelMethod.

                  Interpolate the levels and save them to SavedValues

                  -

                  source

                  +

                  source

                  # Ribasim.save_vertical_fluxMethod.

                  Compute the average vertical fluxes over the last saveat interval and write them to SavedValues

                  -

                  source

                  +

                  source

                  # Ribasim.scalar_interpolation_derivativeMethod.

                  Derivative of scalar interpolation.

                  -

                  source

                  +

                  source

                  # Ribasim.secondsMethod.

                  seconds(period::Millisecond)::Float64

                  Convert a period of type Millisecond to a Float64 in seconds. You get Millisecond objects when subtracting two DateTime objects. Dates.value returns the number of milliseconds.

                  -

                  source

                  +

                  source

                  # Ribasim.seconds_sinceMethod.

                  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.

                  -

                  source

                  +

                  source

                  # Ribasim.set_capacities_flow_demand_outflow!Method.

                  Set the capacity of the outflow edge from a node with a flow demand:

                  • To Inf if the current priority is other than the priority of the flow demand
                  • To 0.0 if the current priority is equal to the priority of the flow demand
                  -

                  source

                  +

                  source

                  # 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.

                  -

                  source

                  +

                  source

                  # Ribasim.set_flow!Method.

                  Set the given flow q over the edge between the given nodes.

                  -

                  source

                  +

                  source

                  # Ribasim.set_fractional_flow_in_allocation!Method.

                  Update the fractional flow fractions in an allocation problem.

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_capacities_basin!Method.

                  Set the initial capacity of each basin in the subnetwork as vertical fluxes + the disk of storage above the maximum level / Δt_allocation

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_capacities_buffer!Method.

                  Set the flow buffer of nodes with a flow demand to 0.0

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_capacities_edge!Method.

                  Set the capacities of the allocation flow edges as determined by the smallest maxflowrate of a node on this edge

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_capacities_inlet!Method.

                  Set the capacities of the main network to subnetwork inlets. Per optimization type: internalsources: 0.0 collectdemands: Inf allocate: the total flow allocated to this inlet from the main network

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_capacities_returnflow!Method.

                  Set the initial capacities of the UserDemand return flow sources to 0.

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_capacities_source!Method.

                  Set the capacities of the sources in the subnetwork as the latest instantaneous flow out of the source in the physical layer

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_demands_flow!Method.

                  Set the initial demands of the nodes with a flow demand to the interpolated value from the given timeseries.

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_demands_level!Method.

                  Set the initial demand of each basin in the subnetwork as

                  • vertical fluxes + the disk of missing storage below the minimum level / Δt_allocation
                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_demands_user!Method.

                  Set the demands of the user demand nodes as given by either a coupled model or a timeseries

                  -

                  source

                  +

                  source

                  # Ribasim.set_initial_values!Method.

                  Set the initial capacities and demands which are recudes by usage in the adjust*capacities**! and adjust*demands**! functions respectively.

                  -

                  source

                  +

                  source

                  # Ribasim.set_is_pid_controlled!Method.

                  Set ispidcontrolled to true for those pumps and outlets that are PID controlled

                  -

                  source

                  +

                  source

                  # Ribasim.set_objective_priority!Method.

                  Set the objective for the given priority. For an objective with absolute values this also involves adjusting constraints.

                  -

                  source

                  +

                  source

                  # 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.

                  -

                  source

                  +

                  source

                  # 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 missing, it is not set.

                  -

                  source

                  +

                  source

                  # Ribasim.sorted_table!Method.

                  Depending on if a table can be sorted, either sort it or assert that it is sorted.

                  Tables loaded from the database into memory can be sorted. Tables loaded from Arrow files are memory mapped and can therefore not be sorted.

                  -

                  source

                  +

                  source

                  # Ribasim.tsavesMethod.

                  Get all saved times in seconds since start

                  -

                  source

                  +

                  source

                  # Ribasim.update_allocation!Method.

                  Solve the allocation problem for all demands and assign allocated abstractions.

                  -

                  source

                  +

                  source

                  # Ribasim.update_basinMethod.

                  Load updates from ‘Basin / time’ into the parameters

                  -

                  source

                  +

                  source

                  # Ribasim.update_jac_prototype!Method.

                  Add nonzeros for basins connected to eachother via 1 node and possibly a fractional flow node Basins are also assumed to depend on themselves (main diagonal terms)

                  -

                  source

                  +

                  source

                  # Ribasim.update_jac_prototype!Method.

                  Add nonzeros for the integral term and the basins on either side of the controlled node

                  -

                  source

                  +

                  source

                  # Ribasim.update_tabulated_rating_curve!Method.

                  Load updates from ‘TabulatedRatingCurve / time’ into the parameters

                  -

                  source

                  +

                  source

                  # Ribasim.update_vertical_flux!Method.

                  Smoothly let the evaporation flux go to 0 when at small water depths Currently at less than 0.1 m.

                  -

                  source

                  +

                  source

                  # Ribasim.valid_discrete_controlMethod.

                  Check:

                    @@ -965,60 +965,60 @@

                    source

                    +

                    source

                    # Ribasim.valid_edge_typesMethod.

                    Check that only supported edge types are declared.

                    -

                    source

                    +

                    source

                    # Ribasim.valid_edgesMethod.

                    Test for each node given its node type whether the nodes that

                    are downstream (‘down-edge’) of this node are of an allowed type

                    -

                    source

                    +

                    source

                    # Ribasim.valid_flow_ratesMethod.

                    Test whether static or discrete controlled flow rates are indeed non-negative.

                    -

                    source

                    +

                    source

                    # Ribasim.valid_fractional_flowMethod.

                    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.

                    -

                    source

                    +

                    source

                    # Ribasim.valid_n_neighborsMethod.

                    Test for each node given its node type whether it has an allowed number of flow/control inneighbors and outneighbors

                    -

                    source

                    +

                    source

                    # Ribasim.valid_profilesMethod.

                    Check whether the profile data has no repeats in the levels and the areas start positive.

                    -

                    source

                    +

                    source

                    # Ribasim.valid_sourcesMethod.

                    The source nodes must only have one allocation outneighbor and no allocation inneighbors.

                    -

                    source

                    +

                    source

                    # Ribasim.valid_subgridMethod.

                    Validate the entries for a single subgrid element.

                    -

                    source

                    +

                    source

                    # Ribasim.water_balance!Method.

                    The right hand side function of the system of ODEs set up by Ribasim.

                    -

                    source

                    +

                    source

                    # Ribasim.write_arrowMethod.

                    Write a result table to disk as an Arrow file

                    -

                    source

                    +

                    source

                    # Ribasim.write_resultsMethod.

                    write_results(model::Model)::Model

                    Write all results to the Arrow files as specified in the model configuration.

                    -

                    source

                    +

                    source

                    # Ribasim.config.algorithmMethod.

                    Create an OrdinaryDiffEqAlgorithm from solver config

                    -

                    source

                    +

                    source

                    # Ribasim.config.convert_dtMethod.

                    Convert the dt from our Config to SciML stepsize control arguments

                    -

                    source

                    +

                    source

                    # Ribasim.config.convert_saveatMethod.

                    Convert the saveat Float64 from our Config to SciML’s saveat

                    -

                    source

                    +

                    source

                    # Ribasim.config.input_pathMethod.

                    Construct a path relative to both the TOML directory and the optional input_dir

                    -

                    source

                    +

                    source

                    # Ribasim.config.results_pathMethod.

                    Construct a path relative to both the TOML directory and the optional results_dir

                    -

                    source

                    +

                    source

                    # Ribasim.config.snake_caseMethod.

                    Convert a string from CamelCase to snake_case.

                    -

                    source

                    +

                    source

                    @@ -1039,7 +1039,7 @@

                    source

                    +

                    source

                    @@ -1047,10 +1047,10 @@

                    1.5 Macros

                    # Ribasim.config.@addfieldsMacro.

                    Add fieldnames with Union{String, Nothing} type to struct expression. Requires (option?) use before it.

                    -

                    source

                    +

                    source

                    # Ribasim.config.@addnodetypesMacro.

                    Add all TableOption subtypes as fields to struct expression. Requires (option?) use before it.

                    -

                    source

                    +

                    source

                    @@ -1066,8 +1066,8 @@

                    Ribasim.Basin
                  • Ribasim.DiscreteControl
                  • Ribasim.EdgeMetadata
                  • -
                  • Ribasim.FlatVector
                  • Ribasim.FlatVector
                  • +
                  • Ribasim.FlatVector
                  • Ribasim.FlowBoundary
                  • Ribasim.FractionalFlow
                  • Ribasim.InNeighbors
                  • @@ -1152,8 +1152,8 @@

                    Ribasim.findsorted
                  • Ribasim.flow_table
                  • Ribasim.formulate_flow!
                  • -
                  • Ribasim.formulate_flow!
                  • Ribasim.formulate_flow!
                  • +
                  • Ribasim.formulate_flow!
                  • Ribasim.get_area_and_level
                  • Ribasim.get_basin_capacity
                  • Ribasim.get_basin_data
                  • diff --git a/core/allocation.html b/core/allocation.html index 46816b282..439c1eb35 100644 --- a/core/allocation.html +++ b/core/allocation.html @@ -567,7 +567,7 @@

                    4.4 Example

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

                    -
                    +
                    Code
                    using Ribasim
                    @@ -590,32 +590,32 @@ 

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

                    -
                    Min F_abs_user_demand[UserDemand #6] + F_abs_user_demand[UserDemand #3] + F_abs_user_demand[UserDemand #13] + F_abs_level_demand[Basin #12] + F_abs_level_demand[Basin #5] + F_abs_level_demand[Basin #2]
                    +
                    Min F_abs_user_demand[UserDemand #13] + F_abs_user_demand[UserDemand #3] + F_abs_user_demand[UserDemand #6] + F_abs_level_demand[Basin #12] + F_abs_level_demand[Basin #5] + F_abs_level_demand[Basin #2]
                     Subject to
                    - abs_positive_user_demand[UserDemand #6] : -F[(Basin #5, UserDemand #6)] + F_abs_user_demand[UserDemand #6] ≥ 0
                    - abs_positive_user_demand[UserDemand #3] : -F[(Basin #2, UserDemand #3)] + F_abs_user_demand[UserDemand #3] ≥ 0
                      abs_positive_user_demand[UserDemand #13] : -F[(Basin #12, UserDemand #13)] + F_abs_user_demand[UserDemand #13] ≥ 0
                    - abs_negative_user_demand[UserDemand #6] : F[(Basin #5, UserDemand #6)] + F_abs_user_demand[UserDemand #6] ≥ 0
                    - abs_negative_user_demand[UserDemand #3] : F[(Basin #2, UserDemand #3)] + F_abs_user_demand[UserDemand #3] ≥ 0
                    + abs_positive_user_demand[UserDemand #3] : -F[(Basin #2, UserDemand #3)] + F_abs_user_demand[UserDemand #3] ≥ 0
                    + abs_positive_user_demand[UserDemand #6] : -F[(Basin #5, UserDemand #6)] + F_abs_user_demand[UserDemand #6] ≥ 0
                      abs_negative_user_demand[UserDemand #13] : F[(Basin #12, UserDemand #13)] + F_abs_user_demand[UserDemand #13] ≥ 0
                    + abs_negative_user_demand[UserDemand #3] : F[(Basin #2, UserDemand #3)] + F_abs_user_demand[UserDemand #3] ≥ 0
                    + abs_negative_user_demand[UserDemand #6] : F[(Basin #5, UserDemand #6)] + F_abs_user_demand[UserDemand #6] ≥ 0
                      abs_positive_basin[Basin #12] : -F_basin_in[Basin #12] + F_abs_level_demand[Basin #12] ≥ 0
                      abs_positive_basin[Basin #5] : -F_basin_in[Basin #5] + F_abs_level_demand[Basin #5] ≥ 0
                      abs_positive_basin[Basin #2] : -F_basin_in[Basin #2] + F_abs_level_demand[Basin #2] ≥ 0
                      abs_negative_basin[Basin #12] : F_basin_in[Basin #12] + F_abs_level_demand[Basin #12] ≥ 0
                      abs_negative_basin[Basin #5] : F_basin_in[Basin #5] + F_abs_level_demand[Basin #5] ≥ 0
                      abs_negative_basin[Basin #2] : F_basin_in[Basin #2] + F_abs_level_demand[Basin #2] ≥ 0
                    - F[(TabulatedRatingCurve #7, Basin #12)] ≥ 0
                    - F[(Basin #2, UserDemand #3)] ≥ 0
                    - F[(UserDemand #3, Basin #2)] ≥ 0
                    - F[(TabulatedRatingCurve #7, Terminal #10)] ≥ 0
                    + F[(UserDemand #13, Terminal #10)] ≥ 0
                      F[(Basin #2, Basin #5)] ≥ 0
                    - F[(Basin #12, UserDemand #13)] ≥ 0
                    + F[(Basin #2, UserDemand #3)] ≥ 0
                      F[(Basin #5, UserDemand #6)] ≥ 0
                    - F[(UserDemand #13, Terminal #10)] ≥ 0
                      F[(Basin #5, Basin #2)] ≥ 0
                    + F[(UserDemand #6, Basin #5)] ≥ 0
                      F[(Basin #5, TabulatedRatingCurve #7)] ≥ 0
                    + F[(Basin #12, UserDemand #13)] ≥ 0
                      F[(FlowBoundary #1, Basin #2)] ≥ 0
                    - F[(UserDemand #6, Basin #5)] ≥ 0
                    + F[(TabulatedRatingCurve #7, Terminal #10)] ≥ 0
                    + F[(UserDemand #3, Basin #2)] ≥ 0
                    + F[(TabulatedRatingCurve #7, Basin #12)] ≥ 0
                      F_basin_in[Basin #12] ≥ 0
                      F_basin_in[Basin #5] ≥ 0
                      F_basin_in[Basin #2] ≥ 0
                    @@ -623,16 +623,16 @@ 

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

                    -
                    +
                    Code
                    import numpy as np
                    @@ -475,7 +475,7 @@ 

                    diff --git a/core/validation.html b/core/validation.html index 591ed919e..ce82cbbf7 100644 --- a/core/validation.html +++ b/core/validation.html @@ -262,7 +262,7 @@

                    Validation

                    1 Connectivity

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

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

                    1 Connectivity

                    2 Neighbor amounts

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

                    -
                    +
                    Code
                    flow_in_min = Vector{String}()
                    diff --git a/python/examples.html b/python/examples.html
                    index b828a168a..2ade74c18 100644
                    --- a/python/examples.html
                    +++ b/python/examples.html
                    @@ -640,7 +640,7 @@ 

                    2 Model with disc

                    -

                    We see that in Januari the level of the basin is too high and thus water is pumped out until the maximum level of the desired range is reached. Then until May water flows out of the basin freely through the tabulated rating curve until the minimum level is reached. From this point until the start of July water is pumped into the basin in short bursts to stay within the desired range. At the start of July rain starts falling on the basin, which causes the basin level to rise until the maximum level. From this point onward water is pumped out of the basin in short bursts to stay within the desired range.

                    +

                    We see that in January the level of the basin is too high and thus water is pumped out until the maximum level of the desired range is reached. Then until May water flows out of the basin freely through the tabulated rating curve until the minimum level is reached. From this point until the start of July water is pumped into the basin in short bursts to stay within the desired range. At the start of July rain starts falling on the basin, which causes the basin level to rise until the maximum level. From this point onward water is pumped out of the basin in short bursts to stay within the desired range.

                    3 Model with PID control

                    diff --git a/python/test-models.html b/python/test-models.html index eb2b8c456..fee643524 100644 --- a/python/test-models.html +++ b/python/test-models.html @@ -227,7 +227,7 @@

                    Test models

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

                    -
                    +
                    Code
                    import ribasim_testmodels
                    diff --git a/search.json b/search.json
                    index 917599517..3f0c3d372 100644
                    --- a/search.json
                    +++ b/search.json
                    @@ -199,7 +199,7 @@
                         "href": "core/allocation.html#example",
                         "title": "Allocation",
                         "section": "4.4 Example",
                    -    "text": "4.4 Example\nThe following is an example of an optimization problem for the example shown here:\n\n\nCode\nusing Ribasim\nusing Ribasim: NodeID\nusing SQLite\nusing ComponentArrays: ComponentVector\n\ntoml_path = normpath(@__DIR__, \"../../generated_testmodels/allocation_example/ribasim.toml\")\np = Ribasim.Model(toml_path).integrator.p\nu = ComponentVector(; storage = zeros(length(p.basin.node_id)))\n\nallocation_model = p.allocation.allocation_models[1]\nt = 0.0\npriority_idx = 1\n\nRibasim.set_flow!(p.graph, NodeID(:FlowBoundary, 1), NodeID(:Basin, 2), 1.0)\nRibasim.set_objective_priority!(allocation_model, p, u, t, priority_idx)\nRibasim.set_initial_values!(allocation_model, p, u, t)\n\nprintln(p.allocation.allocation_models[1].problem)\n\n\nMin F_abs_user_demand[UserDemand #6] + F_abs_user_demand[UserDemand #3] + F_abs_user_demand[UserDemand #13] + F_abs_level_demand[Basin #12] + F_abs_level_demand[Basin #5] + F_abs_level_demand[Basin #2]\nSubject to\n abs_positive_user_demand[UserDemand #6] : -F[(Basin #5, UserDemand #6)] + F_abs_user_demand[UserDemand #6] ≥ 0\n abs_positive_user_demand[UserDemand #3] : -F[(Basin #2, UserDemand #3)] + F_abs_user_demand[UserDemand #3] ≥ 0\n abs_positive_user_demand[UserDemand #13] : -F[(Basin #12, UserDemand #13)] + F_abs_user_demand[UserDemand #13] ≥ 0\n abs_negative_user_demand[UserDemand #6] : F[(Basin #5, UserDemand #6)] + F_abs_user_demand[UserDemand #6] ≥ 0\n abs_negative_user_demand[UserDemand #3] : F[(Basin #2, UserDemand #3)] + F_abs_user_demand[UserDemand #3] ≥ 0\n abs_negative_user_demand[UserDemand #13] : F[(Basin #12, UserDemand #13)] + F_abs_user_demand[UserDemand #13] ≥ 0\n abs_positive_basin[Basin #12] : -F_basin_in[Basin #12] + F_abs_level_demand[Basin #12] ≥ 0\n abs_positive_basin[Basin #5] : -F_basin_in[Basin #5] + F_abs_level_demand[Basin #5] ≥ 0\n abs_positive_basin[Basin #2] : -F_basin_in[Basin #2] + F_abs_level_demand[Basin #2] ≥ 0\n abs_negative_basin[Basin #12] : F_basin_in[Basin #12] + F_abs_level_demand[Basin #12] ≥ 0\n abs_negative_basin[Basin #5] : F_basin_in[Basin #5] + F_abs_level_demand[Basin #5] ≥ 0\n abs_negative_basin[Basin #2] : F_basin_in[Basin #2] + F_abs_level_demand[Basin #2] ≥ 0\n F[(TabulatedRatingCurve #7, Basin #12)] ≥ 0\n F[(Basin #2, UserDemand #3)] ≥ 0\n F[(UserDemand #3, Basin #2)] ≥ 0\n F[(TabulatedRatingCurve #7, Terminal #10)] ≥ 0\n F[(Basin #2, Basin #5)] ≥ 0\n F[(Basin #12, UserDemand #13)] ≥ 0\n F[(Basin #5, UserDemand #6)] ≥ 0\n F[(UserDemand #13, Terminal #10)] ≥ 0\n F[(Basin #5, Basin #2)] ≥ 0\n F[(Basin #5, TabulatedRatingCurve #7)] ≥ 0\n F[(FlowBoundary #1, Basin #2)] ≥ 0\n F[(UserDemand #6, Basin #5)] ≥ 0\n F_basin_in[Basin #12] ≥ 0\n F_basin_in[Basin #5] ≥ 0\n F_basin_in[Basin #2] ≥ 0\n F_basin_out[Basin #12] ≥ 0\n F_basin_out[Basin #5] ≥ 0\n F_basin_out[Basin #2] ≥ 0\n source[(FlowBoundary #1, Basin #2)] : F[(FlowBoundary #1, Basin #2)] ≤ 1\n F[(UserDemand #6, Basin #5)] ≤ 0\n F[(UserDemand #3, Basin #2)] ≤ 0\n F[(UserDemand #13, Terminal #10)] ≤ 0\n fractional_flow[(TabulatedRatingCurve #7, Basin #12)] : F[(TabulatedRatingCurve #7, Basin #12)] - 0.4 F[(Basin #5, TabulatedRatingCurve #7)] ≤ 0\n basin_outflow[Basin #12] : F_basin_out[Basin #12] ≤ 0\n basin_outflow[Basin #5] : F_basin_out[Basin #5] ≤ 0\n basin_outflow[Basin #2] : F_basin_out[Basin #2] ≤ 0\n flow_conservation_basin[Basin #12] : -F[(TabulatedRatingCurve #7, Basin #12)] + F[(Basin #12, UserDemand #13)] + F_basin_in[Basin #12] - F_basin_out[Basin #12] = 0\n flow_conservation_basin[Basin #5] : -F[(Basin #2, Basin #5)] + F[(Basin #5, UserDemand #6)] + F[(Basin #5, Basin #2)] + F[(Basin #5, TabulatedRatingCurve #7)] - F[(UserDemand #6, Basin #5)] + F_basin_in[Basin #5] - F_basin_out[Basin #5] = 0\n flow_conservation_basin[Basin #2] : F[(Basin #2, UserDemand #3)] - F[(UserDemand #3, Basin #2)] + F[(Basin #2, Basin #5)] - F[(Basin #5, Basin #2)] - F[(FlowBoundary #1, Basin #2)] + F_basin_in[Basin #2] - F_basin_out[Basin #2] = 0",
                    +    "text": "4.4 Example\nThe following is an example of an optimization problem for the example shown here:\n\n\nCode\nusing Ribasim\nusing Ribasim: NodeID\nusing SQLite\nusing ComponentArrays: ComponentVector\n\ntoml_path = normpath(@__DIR__, \"../../generated_testmodels/allocation_example/ribasim.toml\")\np = Ribasim.Model(toml_path).integrator.p\nu = ComponentVector(; storage = zeros(length(p.basin.node_id)))\n\nallocation_model = p.allocation.allocation_models[1]\nt = 0.0\npriority_idx = 1\n\nRibasim.set_flow!(p.graph, NodeID(:FlowBoundary, 1), NodeID(:Basin, 2), 1.0)\nRibasim.set_objective_priority!(allocation_model, p, u, t, priority_idx)\nRibasim.set_initial_values!(allocation_model, p, u, t)\n\nprintln(p.allocation.allocation_models[1].problem)\n\n\nMin F_abs_user_demand[UserDemand #13] + F_abs_user_demand[UserDemand #3] + F_abs_user_demand[UserDemand #6] + F_abs_level_demand[Basin #12] + F_abs_level_demand[Basin #5] + F_abs_level_demand[Basin #2]\nSubject to\n abs_positive_user_demand[UserDemand #13] : -F[(Basin #12, UserDemand #13)] + F_abs_user_demand[UserDemand #13] ≥ 0\n abs_positive_user_demand[UserDemand #3] : -F[(Basin #2, UserDemand #3)] + F_abs_user_demand[UserDemand #3] ≥ 0\n abs_positive_user_demand[UserDemand #6] : -F[(Basin #5, UserDemand #6)] + F_abs_user_demand[UserDemand #6] ≥ 0\n abs_negative_user_demand[UserDemand #13] : F[(Basin #12, UserDemand #13)] + F_abs_user_demand[UserDemand #13] ≥ 0\n abs_negative_user_demand[UserDemand #3] : F[(Basin #2, UserDemand #3)] + F_abs_user_demand[UserDemand #3] ≥ 0\n abs_negative_user_demand[UserDemand #6] : F[(Basin #5, UserDemand #6)] + F_abs_user_demand[UserDemand #6] ≥ 0\n abs_positive_basin[Basin #12] : -F_basin_in[Basin #12] + F_abs_level_demand[Basin #12] ≥ 0\n abs_positive_basin[Basin #5] : -F_basin_in[Basin #5] + F_abs_level_demand[Basin #5] ≥ 0\n abs_positive_basin[Basin #2] : -F_basin_in[Basin #2] + F_abs_level_demand[Basin #2] ≥ 0\n abs_negative_basin[Basin #12] : F_basin_in[Basin #12] + F_abs_level_demand[Basin #12] ≥ 0\n abs_negative_basin[Basin #5] : F_basin_in[Basin #5] + F_abs_level_demand[Basin #5] ≥ 0\n abs_negative_basin[Basin #2] : F_basin_in[Basin #2] + F_abs_level_demand[Basin #2] ≥ 0\n F[(UserDemand #13, Terminal #10)] ≥ 0\n F[(Basin #2, Basin #5)] ≥ 0\n F[(Basin #2, UserDemand #3)] ≥ 0\n F[(Basin #5, UserDemand #6)] ≥ 0\n F[(Basin #5, Basin #2)] ≥ 0\n F[(UserDemand #6, Basin #5)] ≥ 0\n F[(Basin #5, TabulatedRatingCurve #7)] ≥ 0\n F[(Basin #12, UserDemand #13)] ≥ 0\n F[(FlowBoundary #1, Basin #2)] ≥ 0\n F[(TabulatedRatingCurve #7, Terminal #10)] ≥ 0\n F[(UserDemand #3, Basin #2)] ≥ 0\n F[(TabulatedRatingCurve #7, Basin #12)] ≥ 0\n F_basin_in[Basin #12] ≥ 0\n F_basin_in[Basin #5] ≥ 0\n F_basin_in[Basin #2] ≥ 0\n F_basin_out[Basin #12] ≥ 0\n F_basin_out[Basin #5] ≥ 0\n F_basin_out[Basin #2] ≥ 0\n source[(FlowBoundary #1, Basin #2)] : F[(FlowBoundary #1, Basin #2)] ≤ 1\n F[(UserDemand #13, Terminal #10)] ≤ 0\n F[(UserDemand #3, Basin #2)] ≤ 0\n F[(UserDemand #6, Basin #5)] ≤ 0\n fractional_flow[(TabulatedRatingCurve #7, Basin #12)] : -0.4 F[(Basin #5, TabulatedRatingCurve #7)] + F[(TabulatedRatingCurve #7, Basin #12)] ≤ 0\n basin_outflow[Basin #12] : F_basin_out[Basin #12] ≤ 0\n basin_outflow[Basin #5] : F_basin_out[Basin #5] ≤ 0\n basin_outflow[Basin #2] : F_basin_out[Basin #2] ≤ 0\n flow_conservation_basin[Basin #12] : F[(Basin #12, UserDemand #13)] - F[(TabulatedRatingCurve #7, Basin #12)] + F_basin_in[Basin #12] - F_basin_out[Basin #12] = 0\n flow_conservation_basin[Basin #5] : -F[(Basin #2, Basin #5)] + F[(Basin #5, UserDemand #6)] + F[(Basin #5, Basin #2)] - F[(UserDemand #6, Basin #5)] + F[(Basin #5, TabulatedRatingCurve #7)] + F_basin_in[Basin #5] - F_basin_out[Basin #5] = 0\n flow_conservation_basin[Basin #2] : F[(Basin #2, Basin #5)] + F[(Basin #2, UserDemand #3)] - F[(Basin #5, Basin #2)] - F[(FlowBoundary #1, Basin #2)] - F[(UserDemand #3, Basin #2)] + F_basin_in[Basin #2] - F_basin_out[Basin #2] = 0",
                         "crumbs": [
                           "Julia core",
                           "Allocation"
                    @@ -283,7 +283,7 @@
                         "href": "python/examples.html",
                         "title": "Examples",
                         "section": "",
                    -    "text": "1 Basic model with static forcing\n\nimport shutil\nfrom pathlib import Path\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom ribasim import Allocation, Model, Node\nfrom ribasim.nodes import (\n    basin,\n    discrete_control,\n    flow_boundary,\n    fractional_flow,\n    level_boundary,\n    level_demand,\n    linear_resistance,\n    manning_resistance,\n    outlet,\n    pid_control,\n    pump,\n    tabulated_rating_curve,\n    user_demand,\n)\nfrom shapely.geometry import Point\n\n\ndatadir = Path(\"data\")\nshutil.rmtree(datadir, ignore_errors=True)\n\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2021-01-01\", crs=\"EPSG:4326\")\n\nSetup the basins:\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\n# Convert steady forcing to m/s\n# 2 mm/d precipitation, 1 mm/d evaporation\n\nbasin_data = [\n    basin.Profile(area=[0.01, 1000.0], level=[0.0, 1.0]),\n    basin.Time(\n        time=pd.date_range(model.starttime, model.endtime),\n        drainage=0.0,\n        potential_evaporation=evaporation,\n        infiltration=0.0,\n        precipitation=precipitation,\n        urban_runoff=0.0,\n    ),\n    basin.State(level=[1.4]),\n]\n\nmodel.basin.add(Node(1, Point(0.0, 0.0)), basin_data)\nmodel.basin.add(Node(3, Point(2.0, 0.0)), basin_data)\nmodel.basin.add(Node(6, Point(3.0, 2.0)), basin_data)\nmodel.basin.add(Node(9, Point(5.0, 0.0)), basin_data)\n\nSetup linear resistance:\n\nmodel.linear_resistance.add(\n    Node(10, Point(6.0, 0.0)),\n    [linear_resistance.Static(resistance=[5e3])],\n)\nmodel.linear_resistance.add(\n    Node(12, Point(2.0, 1.0)),\n    [linear_resistance.Static(resistance=[3600.0 * 24.0 / 100.0])],\n)\n\nSetup Manning resistance:\n\nmodel.manning_resistance.add(\n    Node(2, Point(1.0, 0.0)),\n    [\n        manning_resistance.Static(\n            length=[900], manning_n=[0.04], profile_width=[6.0], profile_slope=[3.0]\n        )\n    ],\n)\n\nSet up a rating curve node:\n\nmodel.tabulated_rating_curve.add(\n    Node(4, Point(3.0, 0.0)),\n    [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 10 / 86400])],\n)\n\nSetup fractional flows:\n\nmodel.fractional_flow.add(\n    Node(5, Point(3.0, 1.0)), [fractional_flow.Static(fraction=[0.3])]\n)\nmodel.fractional_flow.add(\n    Node(8, Point(4.0, 0.0)), [fractional_flow.Static(fraction=[0.6])]\n)\nmodel.fractional_flow.add(\n    Node(13, Point(3.0, -1.0)),\n    [fractional_flow.Static(fraction=[0.1])],\n)\n\nSetup pump:\n\nmodel.pump.add(Node(7, Point(4.0, 1.0)), [pump.Static(flow_rate=[0.5 / 3600])])\n\nSetup level boundary:\n\nmodel.level_boundary.add(\n    Node(11, Point(2.0, 2.0)), [level_boundary.Static(level=[0.5])]\n)\nmodel.level_boundary.add(\n    Node(17, Point(6.0, 1.0)), [level_boundary.Static(level=[1.5])]\n)\n\nSetup flow boundary:\n\nmodel.flow_boundary.add(\n    Node(15, Point(3.0, 3.0)), [flow_boundary.Static(flow_rate=[1e-4])]\n)\nmodel.flow_boundary.add(\n    Node(16, Point(0.0, 1.0)), [flow_boundary.Static(flow_rate=[1e-4])]\n)\n\nSetup terminal:\n\nmodel.terminal.add(Node(14, Point(3.0, -2.0)))\n\nSetup the edges:\n\nmodel.edge.add(model.basin[1], model.manning_resistance[2])\nmodel.edge.add(model.manning_resistance[2], model.basin[3])\nmodel.edge.add(model.basin[3], model.tabulated_rating_curve[4])\nmodel.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[5])\nmodel.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[8])\nmodel.edge.add(model.fractional_flow[5], model.basin[6])\nmodel.edge.add(model.basin[6], model.pump[7])\nmodel.edge.add(model.fractional_flow[8], model.basin[9])\nmodel.edge.add(model.pump[7], model.basin[9])\nmodel.edge.add(model.basin[9], model.linear_resistance[10])\nmodel.edge.add(model.level_boundary[11], model.linear_resistance[12])\nmodel.edge.add(model.linear_resistance[12], model.basin[3])\nmodel.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[13])\nmodel.edge.add(model.fractional_flow[13], model.terminal[14])\nmodel.edge.add(model.flow_boundary[15], model.basin[6])\nmodel.edge.add(model.flow_boundary[16], model.basin[1])\nmodel.edge.add(model.linear_resistance[10], model.level_boundary[17])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ntoml_path = datadir / \"basic/ribasim.toml\"\nmodel.write(toml_path)\n\nPosixPath('data/basic/ribasim.toml')\n\n\nNow run the model. From Python you can run it with:\nimport subprocess\nsubprocess.call([cli_path, toml_path], check=True)\nWindows users should note that if you put the ribasim_cli folder in your Path, cli_path needs to have the cmd suffix; ribasim.cmd.\nOr similarly you can from the terminal with:\nribasim basic/ribasim.toml\nAfter running the model, read back the results:\n\ndf_basin = pd.read_feather(datadir / \"basic/results/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\n\n\n\n\n\n\n\ndf_flow = pd.read_feather(datadir / \"basic/results/flow.arrow\")\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow_rate * 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\n\n\n\n\n\n\n\n\n2 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 by two connected pumps. These two pumps together 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.\nSetup the basins:\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2021-01-01\", crs=\"EPSG:4326\")\n\n\nmodel.basin.add(\n    Node(1, Point(0.0, 0.0)),\n    [\n        basin.Profile(area=[1000.0, 1000.0], level=[0.0, 1.0]),\n        basin.State(level=[20.0]),\n        basin.Time(time=[\"2020-01-01\", \"2020-07-01\"], precipitation=[0.0, 3e-6]),\n    ],\n)\n\nSetup the discrete control:\n\nmodel.discrete_control.add(\n    Node(7, Point(1.0, 0.0)),\n    [\n        discrete_control.Variable(\n            compound_variable_id=1,\n            listen_node_id=1,\n            listen_node_type=[\"Basin\"],\n            variable=[\"level\"],\n        ),\n        discrete_control.Condition(\n            compound_variable_id=1,\n            # min, max\n            greater_than=[5.0, 15.0],\n        ),\n        discrete_control.Logic(\n            truth_state=[\"FF\", \"TF\", \"TT\"],\n            control_state=[\"in\", \"none\", \"out\"],\n        ),\n    ],\n)\n\nThe above control logic can be summarized as follows:\n\nIf the level is above the maximum, activate the control state “out”;\nIf the level is below the minimum, active the control state “in”;\nOtherwise activate the control state “none”.\n\nSetup the pump:\n\nmodel.pump.add(\n    Node(2, Point(1.0, 1.0)),\n    [pump.Static(control_state=[\"none\", \"in\", \"out\"], flow_rate=[0.0, 2e-3, 0.0])],\n)\nmodel.pump.add(\n    Node(3, Point(1.0, -1.0)),\n    [pump.Static(control_state=[\"none\", \"in\", \"out\"], flow_rate=[0.0, 0.0, 2e-3])],\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\nmodel.level_boundary.add(\n    Node(4, Point(2.0, 0.0)), [level_boundary.Static(level=[10.0])]\n)\n\nSetup the rating curve:\n\nmodel.tabulated_rating_curve.add(\n    Node(5, Point(-1.0, 0.0)),\n    [tabulated_rating_curve.Static(level=[2.0, 15.0], flow_rate=[0.0, 2e-3])],\n)\n\nSetup the terminal:\n\nmodel.terminal.add(Node(6, Point(-2.0, 0.0)))\n\nSetup edges:\n\nmodel.edge.add(model.basin[1], model.pump[3])\nmodel.edge.add(model.pump[3], model.level_boundary[4])\nmodel.edge.add(model.level_boundary[4], model.pump[2])\nmodel.edge.add(model.pump[2], model.basin[1])\nmodel.edge.add(model.basin[1], model.tabulated_rating_curve[5])\nmodel.edge.add(model.tabulated_rating_curve[5], model.terminal[6])\nmodel.edge.add(model.discrete_control[7], model.pump[2])\nmodel.edge.add(model.discrete_control[7], model.pump[3])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\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_range/ribasim.toml\")\n\nPosixPath('data/level_range/ribasim.toml')\n\n\nNow run the model with ribasim level_range/ribasim.toml. After running the model, read back the results:\n\ndf_basin = pd.read_feather(datadir / \"level_range/results/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.df.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\nax.set_yticks(greater_than, [\"min\", \"max\"])\nax.set_ylabel(\"level\")\nplt.show()\n\n\n\n\n\n\n\n\nWe see that in Januari the level of the basin is too high and thus water is pumped out until the maximum level of the desired range is reached. Then until May water flows out of the basin freely through the tabulated rating curve until the minimum level is reached. From this point until the start of July water is pumped into the basin in short bursts to stay within the desired range. At the start of July rain starts falling on the basin, which causes the basin level to rise until the maximum level. From this point onward water is pumped out of the basin in short bursts to stay within the desired range.\n\n\n3 Model with PID control\nSet up the model:\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2020-12-01\", crs=\"EPSG:4326\")\n\nSetup the basins:\n\nmodel.basin.add(\n    Node(2, Point(1.0, 0.0)),\n    [basin.Profile(area=[1000.0, 1000.0], level=[0.0, 1.0]), basin.State(level=[6.0])],\n)\n\nSetup the pump:\n\nmodel.pump.add(\n    Node(3, Point(2.0, 0.5)),\n    [pump.Static(flow_rate=[0.0])],  # Will be overwritten by PID controller\n)\n\nSetup the outlet:\n\nmodel.outlet.add(\n    Node(6, Point(2.0, -0.5)),\n    [outlet.Static(flow_rate=[0.0])],  # Will be overwritten by PID controller\n)\n\nSetup flow boundary:\n\nmodel.flow_boundary.add(\n    Node(1, Point(0.0, 0.0)),\n    [flow_boundary.Static(flow_rate=[1e-3])],\n)\n\nSetup level boundary:\n\nmodel.level_boundary.add(\n    Node(4, Point(3.0, 0.0)),\n    [level_boundary.Static(level=[5.0])],\n)\n\nSetup PID control:\n\nfor node, proportional, integral in [\n    (Node(5, Point(1.5, 1.0)), -1e-3, -1e-7),\n    (Node(7, Point(1.5, -1.0)), 1e-3, 1e-7),\n]:\n    pid_control_data = [\n        pid_control.Time(\n            time=[\n                \"2020-01-01\",\n                \"2020-05-01\",\n                \"2020-07-01\",\n                \"2020-12-01\",\n            ],\n            listen_node_id=2,\n            listen_node_type=\"Basin\",\n            target=[5.0, 5.0, 7.5, 7.5],\n            proportional=proportional,\n            integral=integral,\n            derivative=0.0,\n        )\n    ]\n    model.pid_control.add(node, pid_control_data)\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 the edges:\n\nmodel.edge.add(model.flow_boundary[1], model.basin[2])\nmodel.edge.add(model.basin[2], model.pump[3])\nmodel.edge.add(model.pump[3], model.level_boundary[4])\nmodel.edge.add(model.level_boundary[4], model.outlet[6])\nmodel.edge.add(model.outlet[6], model.basin[2])\nmodel.edge.add(model.pid_control[5], model.pump[3])\nmodel.edge.add(model.pid_control[7], model.outlet[6])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"pid_control/ribasim.toml\")\n\nPosixPath('data/pid_control/ribasim.toml')\n\n\nNow run the model with ribasim pid_control/ribasim.toml. After running the model, read back the results:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"pid_control/results/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\nlevel_demands = model.pid_control.time.df.target.to_numpy()[:4]\ntimes = date2num(model.pid_control.time.df.time)[:4]\nax.plot(times, level_demands, color=\"k\", ls=\":\", label=\"target level\")\npass\n\n\n\n\n\n\n\n\n\n\n4 Model with allocation (user demand)\nSetup a model:\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2020-01-20\", crs=\"EPSG:4326\")\n\nSetup the basins:\n\nbasin_data = [\n    basin.Profile(area=[300_000.0, 300_000.0], level=[0.0, 1.0]),\n    basin.State(level=[1.0]),\n]\n\nmodel.basin.add(\n    Node(2, Point(1.0, 0.0), subnetwork_id=1),\n    basin_data,\n)\nmodel.basin.add(\n    Node(5, Point(3.0, 0.0), subnetwork_id=1),\n    basin_data,\n)\nmodel.basin.add(\n    Node(12, Point(4.5, 1.0), subnetwork_id=1),\n    basin_data,\n)\n\nSetup the flow boundary:\n\nmodel.flow_boundary.add(\n    Node(1, Point(0.0, 0.0), subnetwork_id=1), [flow_boundary.Static(flow_rate=[2.0])]\n)\n\nSetup the linear resistance:\n\nmodel.linear_resistance.add(\n    Node(4, Point(2.0, 0.0), subnetwork_id=1),\n    [linear_resistance.Static(resistance=[0.06])],\n)\n\nSetup the tabulated rating curve:\n\nmodel.tabulated_rating_curve.add(\n    Node(7, Point(4.0, 0.0), subnetwork_id=1),\n    [tabulated_rating_curve.Static(level=[0.0, 0.5, 1.0], flow_rate=[0.0, 0.0, 2.0])],\n)\n\nSetup the fractional flow:\n\nmodel.fractional_flow.add(\n    Node(8, Point(4.5, 0.0), subnetwork_id=1),\n    [fractional_flow.Static(fraction=[0.6, 0.9], control_state=[\"divert\", \"close\"])],\n)\nmodel.fractional_flow.add(\n    Node(9, Point(4.5, 0.5), subnetwork_id=1),\n    [fractional_flow.Static(fraction=[0.4, 0.1], control_state=[\"divert\", \"close\"])],\n)\n\nSetup the terminal:\n\nmodel.terminal.add(Node(10, Point(5.0, 0.0), subnetwork_id=1))\n\nSetup the discrete control:\n\nmodel.discrete_control.add(\n    Node(11, Point(4.5, 0.25), subnetwork_id=1),\n    [\n        discrete_control.Variable(\n            compound_variable_id=1,\n            listen_node_id=[5],\n            listen_node_type=[\"Basin\"],\n            variable=[\"level\"],\n        ),\n        discrete_control.Condition(\n            compound_variable_id=1,\n            greater_than=[0.52],\n        ),\n        discrete_control.Logic(\n            truth_state=[\"T\", \"F\"], control_state=[\"divert\", \"close\"]\n        ),\n    ],\n)\n\nSetup the users:\n\nmodel.user_demand.add(\n    Node(6, Point(3.0, 1.0), subnetwork_id=1),\n    [\n        user_demand.Static(\n            demand=[1.5], return_factor=[0.0], min_level=[-1.0], priority=[1]\n        )\n    ],\n)\nmodel.user_demand.add(\n    Node(13, Point(5.0, 1.0), subnetwork_id=1),\n    [\n        user_demand.Static(\n            demand=[1.0], return_factor=[0.0], min_level=[-1.0], priority=[3]\n        )\n    ],\n)\nmodel.user_demand.add(\n    Node(3, Point(1.0, 1.0), subnetwork_id=1),\n    [\n        user_demand.Time(\n            demand=[0.0, 1.0, 1.2, 1.2],\n            return_factor=[0.0, 0.0, 0.0, 0.0],\n            min_level=[-1.0, -1.0, -1.0, -1.0],\n            priority=[1, 1, 2, 2],\n            time=2 * [\"2020-01-01\", \"2020-01-20\"],\n        )\n    ],\n)\n\nSetup the allocation:\n\nmodel.allocation = Allocation(use_allocation=True, timestep=86400)\n\nSetup the edges:\n\nmodel.edge.add(model.flow_boundary[1], model.basin[2], subnetwork_id=1)\nmodel.edge.add(model.basin[2], model.user_demand[3])\nmodel.edge.add(model.basin[2], model.linear_resistance[4])\nmodel.edge.add(model.linear_resistance[4], model.basin[5])\nmodel.edge.add(model.basin[5], model.user_demand[6])\nmodel.edge.add(model.basin[5], model.tabulated_rating_curve[7])\nmodel.edge.add(model.tabulated_rating_curve[7], model.fractional_flow[8])\nmodel.edge.add(model.user_demand[3], model.basin[2])\nmodel.edge.add(model.user_demand[6], model.basin[5])\nmodel.edge.add(model.tabulated_rating_curve[7], model.fractional_flow[9])\nmodel.edge.add(model.fractional_flow[8], model.terminal[10])\nmodel.edge.add(model.fractional_flow[9], model.basin[12])\nmodel.edge.add(model.basin[12], model.user_demand[13])\nmodel.edge.add(model.user_demand[13], model.terminal[10])\nmodel.edge.add(model.discrete_control[11], model.fractional_flow[8])\nmodel.edge.add(model.discrete_control[11], model.fractional_flow[9])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"allocation_example/ribasim.toml\")\n\nPosixPath('data/allocation_example/ribasim.toml')\n\n\nNow run the model with ribasim allocation_example/ribasim.toml. After running the model, read back the results:\n\nimport matplotlib.ticker as plticker\n\ndf_allocation = pd.read_feather(datadir / \"allocation_example/results/allocation.arrow\")\ndf_allocation_wide = df_allocation.pivot_table(\n    index=\"time\",\n    columns=[\"node_type\", \"node_id\", \"priority\"],\n    values=[\"demand\", \"allocated\", \"realized\"],\n)\ndf_allocation_wide = df_allocation_wide.loc[:, (df_allocation_wide != 0).any(axis=0)]\n\nfig, axs = plt.subplots(1, 3, figsize=(8, 5))\n\ndf_allocation_wide[\"demand\"].plot(ax=axs[0], ls=\":\")\ndf_allocation_wide[\"allocated\"].plot(ax=axs[1], ls=\"--\")\ndf_allocation_wide[\"realized\"].plot(ax=axs[2])\n\nfig.tight_layout()\nloc = plticker.MultipleLocator(2)\n\naxs[0].set_ylabel(\"level [m]\")\n\nfor ax, title in zip(axs, [\"Demand\", \"Allocated\", \"Abstracted\"]):\n    ax.set_title(title)\n    ax.set_ylim(0.0, 1.6)\n    ax.xaxis.set_major_locator(loc)\n\n\n\n\n\n\n\n\nSome things to note about this plot:\n\nAbstraction behaves somewhat erratically at the start of the simulation. This is because allocation is based on flows computed in the physical layer, and at the start of the simulation these are not known yet.\nAlthough there is a plotted line for abstraction per priority, abstraction is actually accumulated over all priorities per user.\n\n\ndf_basin = pd.read_feather(datadir / \"allocation_example/results/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()\nax.set_title(\"Basin levels\")\nax.set_ylabel(\"level [m]\")\n\nText(0, 0.5, 'level [m]')\n\n\n\n\n\n\n\n\n\n\n\n5 Model with allocation (basin supply/demand)\nSetup a model:\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2020-02-01\", crs=\"EPSG:4326\")\n\nSetup the basins:\n\nbasin_data = [\n    basin.Profile(area=[1e3, 1e3], level=[0.0, 1.0]),\n    basin.State(level=[0.5]),\n]\nmodel.basin.add(\n    Node(2, Point(1.0, 0.0), subnetwork_id=2),\n    [\n        *basin_data,\n        basin.Time(\n            time=[\"2020-01-01\", \"2020-01-16\"],\n            drainage=[0.0, 0.0],\n            potential_evaporation=[0.0, 0.0],\n            infiltration=[0.0, 0.0],\n            precipitation=[1e-6, 0.0],\n            urban_runoff=[0.0, 0.0],\n        ),\n    ],\n)\nmodel.basin.add(\n    Node(5, Point(2.0, -1.0), subnetwork_id=2),\n    [\n        *basin_data,\n        basin.Static(\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)\n\nSetup the flow boundary:\n\nmodel.flow_boundary.add(\n    Node(1, Point(0.0, 0.0), subnetwork_id=2), [flow_boundary.Static(flow_rate=[1e-3])]\n)\n\nSetup level demand:\n\nmodel.level_demand.add(\n    Node(4, Point(1.0, -1.0), subnetwork_id=2),\n    [level_demand.Static(priority=[1], min_level=[1.0], max_level=[1.5])],\n)\n\nSetup the users:\n\nmodel.user_demand.add(\n    Node(3, Point(2.0, 0.0), subnetwork_id=2),\n    [\n        user_demand.Static(\n            priority=[2], demand=[1.5e-3], return_factor=[0.2], min_level=[0.2]\n        )\n    ],\n)\n\nSetup the allocation:\n\nmodel.allocation = Allocation(use_allocation=True, timestep=1e5)\n\nSetup the edges:\n\nmodel.edge.add(model.flow_boundary[1], model.basin[2], subnetwork_id=2)\nmodel.edge.add(model.basin[2], model.user_demand[3])\nmodel.edge.add(model.level_demand[4], model.basin[2])\nmodel.edge.add(model.user_demand[3], model.basin[5])\nmodel.edge.add(model.level_demand[4], model.basin[5])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\nmodel.write(datadir / \"level_demand/ribasim.toml\")\n\nPosixPath('data/level_demand/ribasim.toml')\n\n\nNow run the model with ribasim level_demand/ribasim.toml. After running the model, read back the results:\n\ndf_basin = pd.read_feather(datadir / \"level_demand/results/basin.arrow\")\ndf_basin = df_basin[df_basin.node_id == 2]\ndf_basin_wide = df_basin.pivot_table(\n    index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\nax = df_basin_wide[\"level\"].plot(ylabel=\"level [m]\")\n\n\n\n\n\n\n\n\nIn the plot above, the line denotes the level of Basin #2 over time. The Basin level is a piecewise linear function of time, with several stages explained below.\nConstants:\n\n\\(d\\): UserDemand #3 demand,\n\\(\\phi\\): Basin #2 precipitation rate,\n\\(q\\): LevelBoundary flow.\n\nStages:\n\nIn the first stage the UserDemand abstracts fully, so the net change of Basin #2 is \\(q + \\phi - d\\);\nIn the second stage the Basin takes precedence so the UserDemand doesn’t abstract, hence the net change of Basin #2 is \\(q + \\phi\\);\nIn the third stage (and following stages) the Basin no longer has a positive demand, since precipitation provides enough water to get the Basin to its target level. The FlowBoundary flow gets fully allocated to the UserDemand, hence the net change of Basin #2 is \\(\\phi\\);\nIn the fourth stage the Basin enters its surplus stage, even though initially the level is below the maximum level. This is because the simulation anticipates that the current precipitation is going to bring the Basin level over its maximum level. The net change of Basin #2 is now \\(q + \\phi - d\\);\nAt the start of the fifth stage the precipitation stops, and so the UserDemand partly uses surplus water from the Basin to fulfill its demand. The net change of Basin #2 becomes \\(q - d\\).\nIn the final stage the Basin is in a dynamical equilibrium, since the Basin has no supply so the user abstracts precisely the flow from the LevelBoundary.\n\n\n\n6 Guidance of modelling a cascade of polder basins\nSituation description: This example shows how to make a model for a given practical water system, which consists of a cascade of level control polder basins with inlet and outlet to the main systems. Note that alternative model layouts are feasible for the same water system, each having its positive items and drawbacks.\n\nThe polder system is composed of a sequence of level controlled polder basins with weirs inbetween each basin and an inlet and outlet to main system\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2021-01-01\", crs=\"EPSG:28992\")\n\nAll the polder basins are exposed to time varying forcings (precipitation, evaporation, drainage, infiltration) to mimic situations of water excess and water shortage.\nIn case of water excess, a pump in the most downstream polder will need to pump the surplus water to the main water system. In case of water shortage, an inlet at the most upstream polder will need to bring water into the cascase of polders. The main water system acts as a water source.\nModel approach: All polder basins as well as the main water system are modelled with basin nodes. To let the system experience all 4 excess/shortage situation, forcing time series are made in a way that is adapting to them. Overall, assume that in one year, the system will experience precipitation (situation 1) in winter and early spring, precipitation shortage (situation 2) from late spring until early autumn. During situation 2, polder basin 4 will experience additional seepage (compoensating its shortage), and later polder basin 3 will also receive more seepage.\nSetting up the basins:\n\ntime = pd.date_range(model.starttime, model.endtime)\nday_of_year = time.day_of_year.to_numpy()\n\nprecipitation = np.zeros(day_of_year.size)\nprecipitation[0:90] = 1.72e-8\nprecipitation[330:366] = 1.72e-8\n\nevaporation = np.zeros(day_of_year.size)\nevaporation[130:270] = 2.87e-8\n\ndrainage = np.zeros(day_of_year.size)\ndrainage[120:270] = 0.4 * 2.87e-8\ndrainage_3 = drainage.copy()\ndrainage_3[210:240] = 17 * 2.87e-8\ndrainage_4 = drainage.copy()\ndrainage_4[160:240] = 13 * 2.87e-8\n\ninfiltration = np.zeros(day_of_year.size)\ninfiltration[0:90] = 5e-8\n\npolder_profile = basin.Profile(area=[100, 100], level=[0.0, 3.0])\n\nbasin_time = [\n    basin.Time(\n        time=pd.date_range(model.starttime, model.endtime),\n        drainage=drainage,\n        potential_evaporation=evaporation,\n        infiltration=0.0,\n        precipitation=precipitation,\n        urban_runoff=0.0,\n    ),\n]\n\nbasin_time4 = [\n    basin.Time(\n        time=pd.date_range(model.starttime, model.endtime),\n        drainage=drainage_4,\n        potential_evaporation=evaporation,\n        infiltration=0.0,\n        precipitation=precipitation,\n        urban_runoff=0.0,\n    ),\n]\nbasin_time3 = [\n    basin.Time(\n        time=pd.date_range(model.starttime, model.endtime),\n        drainage=drainage_3,\n        potential_evaporation=evaporation,\n        infiltration=0.0,\n        precipitation=precipitation,\n        urban_runoff=0.0,\n    ),\n]\n\nmodel.basin.add(\n    Node(1, Point(2.0, 0.0)),\n    [\n        basin.State(level=[2.5]),\n        basin.Profile(area=[1000, 1000], level=[0.0, 3.0]),\n        basin.Time(\n            time=pd.date_range(model.starttime, model.endtime),\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)\nmodel.basin.add(\n    Node(4, Point(0.0, -2.0)),\n    [basin.State(level=[1.5]), polder_profile, *basin_time],\n)\nmodel.basin.add(\n    Node(6, Point(0.0, -4.0)),\n    [basin.State(level=[1.0]), polder_profile, *basin_time],\n)\nmodel.basin.add(\n    Node(8, Point(2.0, -4.0)),\n    [basin.State(level=[1.5]), polder_profile, *basin_time3],\n)\nmodel.basin.add(\n    Node(10, Point(4.0, -4.0)),\n    [basin.State(level=[1.3]), polder_profile, *basin_time4],\n)\nmodel.basin.add(\n    Node(12, Point(4.0, -2.0)),\n    [basin.State(level=[0.1]), polder_profile, *basin_time],\n)\n\nAfter all the basins are defined the connecting component inbetween the basins needs to be determined. For polder basin 5 (node 12), the water level needs to be maintain at 0.0 meter. This means that either there should be no water in this basin, or the basin bottom is lower than the reference level, and the water level should be maintained at the reference level.\nSince the water level of the main system is at 2.5 meter above the reference level a pump is needed to remove the water from polder basin 5.\nSetup the pumps:\n\nmodel.pump.add(\n    Node(13, Point(4.0, -1.0)),\n    [pump.Static(flow_rate=[0.5 / 3600])],\n)\n\nAccording to the description of situation 1 and 2, the water in one polder basin needs to be able to flow to the downstream basin if the current basin has too much water (i.e. the water level is above the setpoint) or if the downstream basin is below setpoint and needs more water. This could be modelled with an uncontrolled TabulatedRatingCurve node with Q=0 at the setpoint level (and Q rising when the level rises above setpoint) , or with an Outlet node where the minimum crest is specified at or just below the setpoint. In this example, we’ve chosen for the Outlet where we specify the minimum crest level 5 cm below the setpoint. For example: the Outlet of polder basin 1 (node 4) is specified with a minimum crest level of 1.95 meter.\nSetup the outlets:\n\n# Set up outlet\nmodel.outlet.add(\n    Node(2, Point(0.0, -1.0)),\n    [outlet.Static(flow_rate=[2 * 0.5 / 3600], min_crest_level=[0.0])],\n)\nmodel.outlet.add(\n    Node(5, Point(0.0, -3.0)),\n    [outlet.Static(flow_rate=[0.5 / 3600], min_crest_level=[1.95])],\n)\nmodel.outlet.add(\n    Node(7, Point(1.0, -4.0)),\n    [outlet.Static(flow_rate=[0.5 / 3600], min_crest_level=[1.45])],\n)\nmodel.outlet.add(\n    Node(9, Point(3.0, -4.0)),\n    [outlet.Static(flow_rate=[0.5 / 3600], min_crest_level=[0.95])],\n)\nmodel.outlet.add(\n    Node(11, Point(4.0, -3.0)),\n    [outlet.Static(flow_rate=[0.5 / 3600], min_crest_level=[0.45])],\n)\n\nWhen using Outlets as connecting nodes, the flow over the Outlet needs to be controlled to maintain the water level at the setpoint. For this purpose we introduce local PidControllers, where the targets of the PidControllers are set to the setpoints. Disadvantage of this local control approach is the delay that is introduced to transport the ‘basin X has a shortage’ message upstream through the cascade to the inlet. Current functionality does not offer the capability for PidControl to take multiple observations into account when controlling the inlet. Combining multiple observations in one control is feasible with DiscreteControl. This could be an alternative approach to controlling the inlet for the cascading water system.\nSetup the PID control:\n\npid_control_data = {\n    \"listen_node_type\": \"Basin\",\n    \"proportional\": [0.05],\n    \"integral\": [0.00],\n    \"derivative\": [0.0],\n}\nmodel.pid_control.add(\n    Node(3, Point(-1.0, -1.0)),\n    [pid_control.Static(listen_node_id=[4], target=[2.0], **pid_control_data)],\n)\nmodel.pid_control.add(\n    Node(14, Point(-1.0, -3.0)),\n    [pid_control.Static(listen_node_id=[6], target=[1.5], **pid_control_data)],\n)\nmodel.pid_control.add(\n    Node(15, Point(1.0, -3.0)),\n    [pid_control.Static(listen_node_id=[8], target=[1.0], **pid_control_data)],\n)\nmodel.pid_control.add(\n    Node(16, Point(3.0, -3.0)),\n    [pid_control.Static(listen_node_id=[10], target=[0.5], **pid_control_data)],\n)\n\nSetup the edges:\n\nmodel.edge.add(model.basin[1], model.outlet[2])\nmodel.edge.add(model.pid_control[3], model.outlet[2])\nmodel.edge.add(model.outlet[2], model.basin[4])\nmodel.edge.add(model.basin[4], model.outlet[5])\nmodel.edge.add(model.outlet[5], model.basin[6])\nmodel.edge.add(model.basin[6], model.outlet[7])\nmodel.edge.add(model.outlet[7], model.basin[8])\nmodel.edge.add(model.basin[8], model.outlet[9])\nmodel.edge.add(model.outlet[9], model.basin[10])\nmodel.edge.add(model.basin[10], model.outlet[11])\nmodel.edge.add(model.outlet[11], model.basin[12])\nmodel.edge.add(model.basin[12], model.pump[13])\nmodel.edge.add(model.pump[13], model.basin[1])\nmodel.edge.add(model.pid_control[14], model.outlet[5])\nmodel.edge.add(model.pid_control[15], model.outlet[7])\nmodel.edge.add(model.pid_control[16], model.outlet[9])\n\nTo plot the model\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML file and run it in the Julia.\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"local_pidcontrolled_cascade/ribasim.toml\")\n\nPosixPath('data/local_pidcontrolled_cascade/ribasim.toml')\n\n\nAfter running the model, read back the result to plot the flow of each polder basin.\n\ndatadir_flow = datadir / \"local_pidcontrolled_cascade/results/flow.arrow\"\ndf_flow = pd.read_feather(datadir_flow)\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow_rate * 86400\n\ndf_pivot = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\")\n\nBelow graphs show the flow exchanged with the mainsystem (i.e. the inlet and the pump), and the flow of weirs inbetween the polder basins.\n\ndf_input = df_pivot.loc[:, [(1, 2), (13, 1)]]\ndf_input.plot(ylim=[-1.0, 20.0])\ndf_weirs = df_pivot.loc[:, [(4, 5), (6, 7), (8, 9), (10, 11)]]\ndf_weirs.plot(ylim=[-1.0, 15.0])\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nBelow graph shows the vertical flux on each basin.\n\ndatadir_basin = datadir / \"local_pidcontrolled_cascade/results/basin.arrow\"\ndf_basin = pd.read_feather(datadir_basin)\ndf_basin[\"vertical_flux\"] = (\n    df_basin[\"precipitation\"]\n    - df_basin[\"evaporation\"]\n    + df_basin[\"drainage\"]\n    + df_basin[\"infiltration\"]\n)\ndf_basin_wide = df_basin.pivot_table(\n    index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\", \"vertical_flux\"]\n)\ndf_basin_wide[\"vertical_flux\"].plot()\n\n\n\n\n\n\n\n\nIn the following graph, the water level of basins are shown. The five polder basins are given starting levels that are different from their setpoints. It can be observed that in the beginning, the water level are changing and approaching to the set points. Later when the water levels are stable, they will not be affected by the forcing.\n\ndf_basin_wide[\"level\"].plot()",
                    +    "text": "1 Basic model with static forcing\n\nimport shutil\nfrom pathlib import Path\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom ribasim import Allocation, Model, Node\nfrom ribasim.nodes import (\n    basin,\n    discrete_control,\n    flow_boundary,\n    fractional_flow,\n    level_boundary,\n    level_demand,\n    linear_resistance,\n    manning_resistance,\n    outlet,\n    pid_control,\n    pump,\n    tabulated_rating_curve,\n    user_demand,\n)\nfrom shapely.geometry import Point\n\n\ndatadir = Path(\"data\")\nshutil.rmtree(datadir, ignore_errors=True)\n\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2021-01-01\", crs=\"EPSG:4326\")\n\nSetup the basins:\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\n# Convert steady forcing to m/s\n# 2 mm/d precipitation, 1 mm/d evaporation\n\nbasin_data = [\n    basin.Profile(area=[0.01, 1000.0], level=[0.0, 1.0]),\n    basin.Time(\n        time=pd.date_range(model.starttime, model.endtime),\n        drainage=0.0,\n        potential_evaporation=evaporation,\n        infiltration=0.0,\n        precipitation=precipitation,\n        urban_runoff=0.0,\n    ),\n    basin.State(level=[1.4]),\n]\n\nmodel.basin.add(Node(1, Point(0.0, 0.0)), basin_data)\nmodel.basin.add(Node(3, Point(2.0, 0.0)), basin_data)\nmodel.basin.add(Node(6, Point(3.0, 2.0)), basin_data)\nmodel.basin.add(Node(9, Point(5.0, 0.0)), basin_data)\n\nSetup linear resistance:\n\nmodel.linear_resistance.add(\n    Node(10, Point(6.0, 0.0)),\n    [linear_resistance.Static(resistance=[5e3])],\n)\nmodel.linear_resistance.add(\n    Node(12, Point(2.0, 1.0)),\n    [linear_resistance.Static(resistance=[3600.0 * 24.0 / 100.0])],\n)\n\nSetup Manning resistance:\n\nmodel.manning_resistance.add(\n    Node(2, Point(1.0, 0.0)),\n    [\n        manning_resistance.Static(\n            length=[900], manning_n=[0.04], profile_width=[6.0], profile_slope=[3.0]\n        )\n    ],\n)\n\nSet up a rating curve node:\n\nmodel.tabulated_rating_curve.add(\n    Node(4, Point(3.0, 0.0)),\n    [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 10 / 86400])],\n)\n\nSetup fractional flows:\n\nmodel.fractional_flow.add(\n    Node(5, Point(3.0, 1.0)), [fractional_flow.Static(fraction=[0.3])]\n)\nmodel.fractional_flow.add(\n    Node(8, Point(4.0, 0.0)), [fractional_flow.Static(fraction=[0.6])]\n)\nmodel.fractional_flow.add(\n    Node(13, Point(3.0, -1.0)),\n    [fractional_flow.Static(fraction=[0.1])],\n)\n\nSetup pump:\n\nmodel.pump.add(Node(7, Point(4.0, 1.0)), [pump.Static(flow_rate=[0.5 / 3600])])\n\nSetup level boundary:\n\nmodel.level_boundary.add(\n    Node(11, Point(2.0, 2.0)), [level_boundary.Static(level=[0.5])]\n)\nmodel.level_boundary.add(\n    Node(17, Point(6.0, 1.0)), [level_boundary.Static(level=[1.5])]\n)\n\nSetup flow boundary:\n\nmodel.flow_boundary.add(\n    Node(15, Point(3.0, 3.0)), [flow_boundary.Static(flow_rate=[1e-4])]\n)\nmodel.flow_boundary.add(\n    Node(16, Point(0.0, 1.0)), [flow_boundary.Static(flow_rate=[1e-4])]\n)\n\nSetup terminal:\n\nmodel.terminal.add(Node(14, Point(3.0, -2.0)))\n\nSetup the edges:\n\nmodel.edge.add(model.basin[1], model.manning_resistance[2])\nmodel.edge.add(model.manning_resistance[2], model.basin[3])\nmodel.edge.add(model.basin[3], model.tabulated_rating_curve[4])\nmodel.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[5])\nmodel.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[8])\nmodel.edge.add(model.fractional_flow[5], model.basin[6])\nmodel.edge.add(model.basin[6], model.pump[7])\nmodel.edge.add(model.fractional_flow[8], model.basin[9])\nmodel.edge.add(model.pump[7], model.basin[9])\nmodel.edge.add(model.basin[9], model.linear_resistance[10])\nmodel.edge.add(model.level_boundary[11], model.linear_resistance[12])\nmodel.edge.add(model.linear_resistance[12], model.basin[3])\nmodel.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[13])\nmodel.edge.add(model.fractional_flow[13], model.terminal[14])\nmodel.edge.add(model.flow_boundary[15], model.basin[6])\nmodel.edge.add(model.flow_boundary[16], model.basin[1])\nmodel.edge.add(model.linear_resistance[10], model.level_boundary[17])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ntoml_path = datadir / \"basic/ribasim.toml\"\nmodel.write(toml_path)\n\nPosixPath('data/basic/ribasim.toml')\n\n\nNow run the model. From Python you can run it with:\nimport subprocess\nsubprocess.call([cli_path, toml_path], check=True)\nWindows users should note that if you put the ribasim_cli folder in your Path, cli_path needs to have the cmd suffix; ribasim.cmd.\nOr similarly you can from the terminal with:\nribasim basic/ribasim.toml\nAfter running the model, read back the results:\n\ndf_basin = pd.read_feather(datadir / \"basic/results/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\n\n\n\n\n\n\n\ndf_flow = pd.read_feather(datadir / \"basic/results/flow.arrow\")\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow_rate * 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\n\n\n\n\n\n\n\n\n2 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 by two connected pumps. These two pumps together 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.\nSetup the basins:\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2021-01-01\", crs=\"EPSG:4326\")\n\n\nmodel.basin.add(\n    Node(1, Point(0.0, 0.0)),\n    [\n        basin.Profile(area=[1000.0, 1000.0], level=[0.0, 1.0]),\n        basin.State(level=[20.0]),\n        basin.Time(time=[\"2020-01-01\", \"2020-07-01\"], precipitation=[0.0, 3e-6]),\n    ],\n)\n\nSetup the discrete control:\n\nmodel.discrete_control.add(\n    Node(7, Point(1.0, 0.0)),\n    [\n        discrete_control.Variable(\n            compound_variable_id=1,\n            listen_node_id=1,\n            listen_node_type=[\"Basin\"],\n            variable=[\"level\"],\n        ),\n        discrete_control.Condition(\n            compound_variable_id=1,\n            # min, max\n            greater_than=[5.0, 15.0],\n        ),\n        discrete_control.Logic(\n            truth_state=[\"FF\", \"TF\", \"TT\"],\n            control_state=[\"in\", \"none\", \"out\"],\n        ),\n    ],\n)\n\nThe above control logic can be summarized as follows:\n\nIf the level is above the maximum, activate the control state “out”;\nIf the level is below the minimum, active the control state “in”;\nOtherwise activate the control state “none”.\n\nSetup the pump:\n\nmodel.pump.add(\n    Node(2, Point(1.0, 1.0)),\n    [pump.Static(control_state=[\"none\", \"in\", \"out\"], flow_rate=[0.0, 2e-3, 0.0])],\n)\nmodel.pump.add(\n    Node(3, Point(1.0, -1.0)),\n    [pump.Static(control_state=[\"none\", \"in\", \"out\"], flow_rate=[0.0, 0.0, 2e-3])],\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\nmodel.level_boundary.add(\n    Node(4, Point(2.0, 0.0)), [level_boundary.Static(level=[10.0])]\n)\n\nSetup the rating curve:\n\nmodel.tabulated_rating_curve.add(\n    Node(5, Point(-1.0, 0.0)),\n    [tabulated_rating_curve.Static(level=[2.0, 15.0], flow_rate=[0.0, 2e-3])],\n)\n\nSetup the terminal:\n\nmodel.terminal.add(Node(6, Point(-2.0, 0.0)))\n\nSetup edges:\n\nmodel.edge.add(model.basin[1], model.pump[3])\nmodel.edge.add(model.pump[3], model.level_boundary[4])\nmodel.edge.add(model.level_boundary[4], model.pump[2])\nmodel.edge.add(model.pump[2], model.basin[1])\nmodel.edge.add(model.basin[1], model.tabulated_rating_curve[5])\nmodel.edge.add(model.tabulated_rating_curve[5], model.terminal[6])\nmodel.edge.add(model.discrete_control[7], model.pump[2])\nmodel.edge.add(model.discrete_control[7], model.pump[3])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\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_range/ribasim.toml\")\n\nPosixPath('data/level_range/ribasim.toml')\n\n\nNow run the model with ribasim level_range/ribasim.toml. After running the model, read back the results:\n\ndf_basin = pd.read_feather(datadir / \"level_range/results/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.df.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\nax.set_yticks(greater_than, [\"min\", \"max\"])\nax.set_ylabel(\"level\")\nplt.show()\n\n\n\n\n\n\n\n\nWe see that in January the level of the basin is too high and thus water is pumped out until the maximum level of the desired range is reached. Then until May water flows out of the basin freely through the tabulated rating curve until the minimum level is reached. From this point until the start of July water is pumped into the basin in short bursts to stay within the desired range. At the start of July rain starts falling on the basin, which causes the basin level to rise until the maximum level. From this point onward water is pumped out of the basin in short bursts to stay within the desired range.\n\n\n3 Model with PID control\nSet up the model:\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2020-12-01\", crs=\"EPSG:4326\")\n\nSetup the basins:\n\nmodel.basin.add(\n    Node(2, Point(1.0, 0.0)),\n    [basin.Profile(area=[1000.0, 1000.0], level=[0.0, 1.0]), basin.State(level=[6.0])],\n)\n\nSetup the pump:\n\nmodel.pump.add(\n    Node(3, Point(2.0, 0.5)),\n    [pump.Static(flow_rate=[0.0])],  # Will be overwritten by PID controller\n)\n\nSetup the outlet:\n\nmodel.outlet.add(\n    Node(6, Point(2.0, -0.5)),\n    [outlet.Static(flow_rate=[0.0])],  # Will be overwritten by PID controller\n)\n\nSetup flow boundary:\n\nmodel.flow_boundary.add(\n    Node(1, Point(0.0, 0.0)),\n    [flow_boundary.Static(flow_rate=[1e-3])],\n)\n\nSetup level boundary:\n\nmodel.level_boundary.add(\n    Node(4, Point(3.0, 0.0)),\n    [level_boundary.Static(level=[5.0])],\n)\n\nSetup PID control:\n\nfor node, proportional, integral in [\n    (Node(5, Point(1.5, 1.0)), -1e-3, -1e-7),\n    (Node(7, Point(1.5, -1.0)), 1e-3, 1e-7),\n]:\n    pid_control_data = [\n        pid_control.Time(\n            time=[\n                \"2020-01-01\",\n                \"2020-05-01\",\n                \"2020-07-01\",\n                \"2020-12-01\",\n            ],\n            listen_node_id=2,\n            listen_node_type=\"Basin\",\n            target=[5.0, 5.0, 7.5, 7.5],\n            proportional=proportional,\n            integral=integral,\n            derivative=0.0,\n        )\n    ]\n    model.pid_control.add(node, pid_control_data)\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 the edges:\n\nmodel.edge.add(model.flow_boundary[1], model.basin[2])\nmodel.edge.add(model.basin[2], model.pump[3])\nmodel.edge.add(model.pump[3], model.level_boundary[4])\nmodel.edge.add(model.level_boundary[4], model.outlet[6])\nmodel.edge.add(model.outlet[6], model.basin[2])\nmodel.edge.add(model.pid_control[5], model.pump[3])\nmodel.edge.add(model.pid_control[7], model.outlet[6])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"pid_control/ribasim.toml\")\n\nPosixPath('data/pid_control/ribasim.toml')\n\n\nNow run the model with ribasim pid_control/ribasim.toml. After running the model, read back the results:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"pid_control/results/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\nlevel_demands = model.pid_control.time.df.target.to_numpy()[:4]\ntimes = date2num(model.pid_control.time.df.time)[:4]\nax.plot(times, level_demands, color=\"k\", ls=\":\", label=\"target level\")\npass\n\n\n\n\n\n\n\n\n\n\n4 Model with allocation (user demand)\nSetup a model:\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2020-01-20\", crs=\"EPSG:4326\")\n\nSetup the basins:\n\nbasin_data = [\n    basin.Profile(area=[300_000.0, 300_000.0], level=[0.0, 1.0]),\n    basin.State(level=[1.0]),\n]\n\nmodel.basin.add(\n    Node(2, Point(1.0, 0.0), subnetwork_id=1),\n    basin_data,\n)\nmodel.basin.add(\n    Node(5, Point(3.0, 0.0), subnetwork_id=1),\n    basin_data,\n)\nmodel.basin.add(\n    Node(12, Point(4.5, 1.0), subnetwork_id=1),\n    basin_data,\n)\n\nSetup the flow boundary:\n\nmodel.flow_boundary.add(\n    Node(1, Point(0.0, 0.0), subnetwork_id=1), [flow_boundary.Static(flow_rate=[2.0])]\n)\n\nSetup the linear resistance:\n\nmodel.linear_resistance.add(\n    Node(4, Point(2.0, 0.0), subnetwork_id=1),\n    [linear_resistance.Static(resistance=[0.06])],\n)\n\nSetup the tabulated rating curve:\n\nmodel.tabulated_rating_curve.add(\n    Node(7, Point(4.0, 0.0), subnetwork_id=1),\n    [tabulated_rating_curve.Static(level=[0.0, 0.5, 1.0], flow_rate=[0.0, 0.0, 2.0])],\n)\n\nSetup the fractional flow:\n\nmodel.fractional_flow.add(\n    Node(8, Point(4.5, 0.0), subnetwork_id=1),\n    [fractional_flow.Static(fraction=[0.6, 0.9], control_state=[\"divert\", \"close\"])],\n)\nmodel.fractional_flow.add(\n    Node(9, Point(4.5, 0.5), subnetwork_id=1),\n    [fractional_flow.Static(fraction=[0.4, 0.1], control_state=[\"divert\", \"close\"])],\n)\n\nSetup the terminal:\n\nmodel.terminal.add(Node(10, Point(5.0, 0.0), subnetwork_id=1))\n\nSetup the discrete control:\n\nmodel.discrete_control.add(\n    Node(11, Point(4.5, 0.25), subnetwork_id=1),\n    [\n        discrete_control.Variable(\n            compound_variable_id=1,\n            listen_node_id=[5],\n            listen_node_type=[\"Basin\"],\n            variable=[\"level\"],\n        ),\n        discrete_control.Condition(\n            compound_variable_id=1,\n            greater_than=[0.52],\n        ),\n        discrete_control.Logic(\n            truth_state=[\"T\", \"F\"], control_state=[\"divert\", \"close\"]\n        ),\n    ],\n)\n\nSetup the users:\n\nmodel.user_demand.add(\n    Node(6, Point(3.0, 1.0), subnetwork_id=1),\n    [\n        user_demand.Static(\n            demand=[1.5], return_factor=[0.0], min_level=[-1.0], priority=[1]\n        )\n    ],\n)\nmodel.user_demand.add(\n    Node(13, Point(5.0, 1.0), subnetwork_id=1),\n    [\n        user_demand.Static(\n            demand=[1.0], return_factor=[0.0], min_level=[-1.0], priority=[3]\n        )\n    ],\n)\nmodel.user_demand.add(\n    Node(3, Point(1.0, 1.0), subnetwork_id=1),\n    [\n        user_demand.Time(\n            demand=[0.0, 1.0, 1.2, 1.2],\n            return_factor=[0.0, 0.0, 0.0, 0.0],\n            min_level=[-1.0, -1.0, -1.0, -1.0],\n            priority=[1, 1, 2, 2],\n            time=2 * [\"2020-01-01\", \"2020-01-20\"],\n        )\n    ],\n)\n\nSetup the allocation:\n\nmodel.allocation = Allocation(use_allocation=True, timestep=86400)\n\nSetup the edges:\n\nmodel.edge.add(model.flow_boundary[1], model.basin[2], subnetwork_id=1)\nmodel.edge.add(model.basin[2], model.user_demand[3])\nmodel.edge.add(model.basin[2], model.linear_resistance[4])\nmodel.edge.add(model.linear_resistance[4], model.basin[5])\nmodel.edge.add(model.basin[5], model.user_demand[6])\nmodel.edge.add(model.basin[5], model.tabulated_rating_curve[7])\nmodel.edge.add(model.tabulated_rating_curve[7], model.fractional_flow[8])\nmodel.edge.add(model.user_demand[3], model.basin[2])\nmodel.edge.add(model.user_demand[6], model.basin[5])\nmodel.edge.add(model.tabulated_rating_curve[7], model.fractional_flow[9])\nmodel.edge.add(model.fractional_flow[8], model.terminal[10])\nmodel.edge.add(model.fractional_flow[9], model.basin[12])\nmodel.edge.add(model.basin[12], model.user_demand[13])\nmodel.edge.add(model.user_demand[13], model.terminal[10])\nmodel.edge.add(model.discrete_control[11], model.fractional_flow[8])\nmodel.edge.add(model.discrete_control[11], model.fractional_flow[9])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"allocation_example/ribasim.toml\")\n\nPosixPath('data/allocation_example/ribasim.toml')\n\n\nNow run the model with ribasim allocation_example/ribasim.toml. After running the model, read back the results:\n\nimport matplotlib.ticker as plticker\n\ndf_allocation = pd.read_feather(datadir / \"allocation_example/results/allocation.arrow\")\ndf_allocation_wide = df_allocation.pivot_table(\n    index=\"time\",\n    columns=[\"node_type\", \"node_id\", \"priority\"],\n    values=[\"demand\", \"allocated\", \"realized\"],\n)\ndf_allocation_wide = df_allocation_wide.loc[:, (df_allocation_wide != 0).any(axis=0)]\n\nfig, axs = plt.subplots(1, 3, figsize=(8, 5))\n\ndf_allocation_wide[\"demand\"].plot(ax=axs[0], ls=\":\")\ndf_allocation_wide[\"allocated\"].plot(ax=axs[1], ls=\"--\")\ndf_allocation_wide[\"realized\"].plot(ax=axs[2])\n\nfig.tight_layout()\nloc = plticker.MultipleLocator(2)\n\naxs[0].set_ylabel(\"level [m]\")\n\nfor ax, title in zip(axs, [\"Demand\", \"Allocated\", \"Abstracted\"]):\n    ax.set_title(title)\n    ax.set_ylim(0.0, 1.6)\n    ax.xaxis.set_major_locator(loc)\n\n\n\n\n\n\n\n\nSome things to note about this plot:\n\nAbstraction behaves somewhat erratically at the start of the simulation. This is because allocation is based on flows computed in the physical layer, and at the start of the simulation these are not known yet.\nAlthough there is a plotted line for abstraction per priority, abstraction is actually accumulated over all priorities per user.\n\n\ndf_basin = pd.read_feather(datadir / \"allocation_example/results/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()\nax.set_title(\"Basin levels\")\nax.set_ylabel(\"level [m]\")\n\nText(0, 0.5, 'level [m]')\n\n\n\n\n\n\n\n\n\n\n\n5 Model with allocation (basin supply/demand)\nSetup a model:\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2020-02-01\", crs=\"EPSG:4326\")\n\nSetup the basins:\n\nbasin_data = [\n    basin.Profile(area=[1e3, 1e3], level=[0.0, 1.0]),\n    basin.State(level=[0.5]),\n]\nmodel.basin.add(\n    Node(2, Point(1.0, 0.0), subnetwork_id=2),\n    [\n        *basin_data,\n        basin.Time(\n            time=[\"2020-01-01\", \"2020-01-16\"],\n            drainage=[0.0, 0.0],\n            potential_evaporation=[0.0, 0.0],\n            infiltration=[0.0, 0.0],\n            precipitation=[1e-6, 0.0],\n            urban_runoff=[0.0, 0.0],\n        ),\n    ],\n)\nmodel.basin.add(\n    Node(5, Point(2.0, -1.0), subnetwork_id=2),\n    [\n        *basin_data,\n        basin.Static(\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)\n\nSetup the flow boundary:\n\nmodel.flow_boundary.add(\n    Node(1, Point(0.0, 0.0), subnetwork_id=2), [flow_boundary.Static(flow_rate=[1e-3])]\n)\n\nSetup level demand:\n\nmodel.level_demand.add(\n    Node(4, Point(1.0, -1.0), subnetwork_id=2),\n    [level_demand.Static(priority=[1], min_level=[1.0], max_level=[1.5])],\n)\n\nSetup the users:\n\nmodel.user_demand.add(\n    Node(3, Point(2.0, 0.0), subnetwork_id=2),\n    [\n        user_demand.Static(\n            priority=[2], demand=[1.5e-3], return_factor=[0.2], min_level=[0.2]\n        )\n    ],\n)\n\nSetup the allocation:\n\nmodel.allocation = Allocation(use_allocation=True, timestep=1e5)\n\nSetup the edges:\n\nmodel.edge.add(model.flow_boundary[1], model.basin[2], subnetwork_id=2)\nmodel.edge.add(model.basin[2], model.user_demand[3])\nmodel.edge.add(model.level_demand[4], model.basin[2])\nmodel.edge.add(model.user_demand[3], model.basin[5])\nmodel.edge.add(model.level_demand[4], model.basin[5])\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\nmodel.write(datadir / \"level_demand/ribasim.toml\")\n\nPosixPath('data/level_demand/ribasim.toml')\n\n\nNow run the model with ribasim level_demand/ribasim.toml. After running the model, read back the results:\n\ndf_basin = pd.read_feather(datadir / \"level_demand/results/basin.arrow\")\ndf_basin = df_basin[df_basin.node_id == 2]\ndf_basin_wide = df_basin.pivot_table(\n    index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\nax = df_basin_wide[\"level\"].plot(ylabel=\"level [m]\")\n\n\n\n\n\n\n\n\nIn the plot above, the line denotes the level of Basin #2 over time. The Basin level is a piecewise linear function of time, with several stages explained below.\nConstants:\n\n\\(d\\): UserDemand #3 demand,\n\\(\\phi\\): Basin #2 precipitation rate,\n\\(q\\): LevelBoundary flow.\n\nStages:\n\nIn the first stage the UserDemand abstracts fully, so the net change of Basin #2 is \\(q + \\phi - d\\);\nIn the second stage the Basin takes precedence so the UserDemand doesn’t abstract, hence the net change of Basin #2 is \\(q + \\phi\\);\nIn the third stage (and following stages) the Basin no longer has a positive demand, since precipitation provides enough water to get the Basin to its target level. The FlowBoundary flow gets fully allocated to the UserDemand, hence the net change of Basin #2 is \\(\\phi\\);\nIn the fourth stage the Basin enters its surplus stage, even though initially the level is below the maximum level. This is because the simulation anticipates that the current precipitation is going to bring the Basin level over its maximum level. The net change of Basin #2 is now \\(q + \\phi - d\\);\nAt the start of the fifth stage the precipitation stops, and so the UserDemand partly uses surplus water from the Basin to fulfill its demand. The net change of Basin #2 becomes \\(q - d\\).\nIn the final stage the Basin is in a dynamical equilibrium, since the Basin has no supply so the user abstracts precisely the flow from the LevelBoundary.\n\n\n\n6 Guidance of modelling a cascade of polder basins\nSituation description: This example shows how to make a model for a given practical water system, which consists of a cascade of level control polder basins with inlet and outlet to the main systems. Note that alternative model layouts are feasible for the same water system, each having its positive items and drawbacks.\n\nThe polder system is composed of a sequence of level controlled polder basins with weirs inbetween each basin and an inlet and outlet to main system\n\nmodel = Model(starttime=\"2020-01-01\", endtime=\"2021-01-01\", crs=\"EPSG:28992\")\n\nAll the polder basins are exposed to time varying forcings (precipitation, evaporation, drainage, infiltration) to mimic situations of water excess and water shortage.\nIn case of water excess, a pump in the most downstream polder will need to pump the surplus water to the main water system. In case of water shortage, an inlet at the most upstream polder will need to bring water into the cascase of polders. The main water system acts as a water source.\nModel approach: All polder basins as well as the main water system are modelled with basin nodes. To let the system experience all 4 excess/shortage situation, forcing time series are made in a way that is adapting to them. Overall, assume that in one year, the system will experience precipitation (situation 1) in winter and early spring, precipitation shortage (situation 2) from late spring until early autumn. During situation 2, polder basin 4 will experience additional seepage (compoensating its shortage), and later polder basin 3 will also receive more seepage.\nSetting up the basins:\n\ntime = pd.date_range(model.starttime, model.endtime)\nday_of_year = time.day_of_year.to_numpy()\n\nprecipitation = np.zeros(day_of_year.size)\nprecipitation[0:90] = 1.72e-8\nprecipitation[330:366] = 1.72e-8\n\nevaporation = np.zeros(day_of_year.size)\nevaporation[130:270] = 2.87e-8\n\ndrainage = np.zeros(day_of_year.size)\ndrainage[120:270] = 0.4 * 2.87e-8\ndrainage_3 = drainage.copy()\ndrainage_3[210:240] = 17 * 2.87e-8\ndrainage_4 = drainage.copy()\ndrainage_4[160:240] = 13 * 2.87e-8\n\ninfiltration = np.zeros(day_of_year.size)\ninfiltration[0:90] = 5e-8\n\npolder_profile = basin.Profile(area=[100, 100], level=[0.0, 3.0])\n\nbasin_time = [\n    basin.Time(\n        time=pd.date_range(model.starttime, model.endtime),\n        drainage=drainage,\n        potential_evaporation=evaporation,\n        infiltration=0.0,\n        precipitation=precipitation,\n        urban_runoff=0.0,\n    ),\n]\n\nbasin_time4 = [\n    basin.Time(\n        time=pd.date_range(model.starttime, model.endtime),\n        drainage=drainage_4,\n        potential_evaporation=evaporation,\n        infiltration=0.0,\n        precipitation=precipitation,\n        urban_runoff=0.0,\n    ),\n]\nbasin_time3 = [\n    basin.Time(\n        time=pd.date_range(model.starttime, model.endtime),\n        drainage=drainage_3,\n        potential_evaporation=evaporation,\n        infiltration=0.0,\n        precipitation=precipitation,\n        urban_runoff=0.0,\n    ),\n]\n\nmodel.basin.add(\n    Node(1, Point(2.0, 0.0)),\n    [\n        basin.State(level=[2.5]),\n        basin.Profile(area=[1000, 1000], level=[0.0, 3.0]),\n        basin.Time(\n            time=pd.date_range(model.starttime, model.endtime),\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)\nmodel.basin.add(\n    Node(4, Point(0.0, -2.0)),\n    [basin.State(level=[1.5]), polder_profile, *basin_time],\n)\nmodel.basin.add(\n    Node(6, Point(0.0, -4.0)),\n    [basin.State(level=[1.0]), polder_profile, *basin_time],\n)\nmodel.basin.add(\n    Node(8, Point(2.0, -4.0)),\n    [basin.State(level=[1.5]), polder_profile, *basin_time3],\n)\nmodel.basin.add(\n    Node(10, Point(4.0, -4.0)),\n    [basin.State(level=[1.3]), polder_profile, *basin_time4],\n)\nmodel.basin.add(\n    Node(12, Point(4.0, -2.0)),\n    [basin.State(level=[0.1]), polder_profile, *basin_time],\n)\n\nAfter all the basins are defined the connecting component inbetween the basins needs to be determined. For polder basin 5 (node 12), the water level needs to be maintain at 0.0 meter. This means that either there should be no water in this basin, or the basin bottom is lower than the reference level, and the water level should be maintained at the reference level.\nSince the water level of the main system is at 2.5 meter above the reference level a pump is needed to remove the water from polder basin 5.\nSetup the pumps:\n\nmodel.pump.add(\n    Node(13, Point(4.0, -1.0)),\n    [pump.Static(flow_rate=[0.5 / 3600])],\n)\n\nAccording to the description of situation 1 and 2, the water in one polder basin needs to be able to flow to the downstream basin if the current basin has too much water (i.e. the water level is above the setpoint) or if the downstream basin is below setpoint and needs more water. This could be modelled with an uncontrolled TabulatedRatingCurve node with Q=0 at the setpoint level (and Q rising when the level rises above setpoint) , or with an Outlet node where the minimum crest is specified at or just below the setpoint. In this example, we’ve chosen for the Outlet where we specify the minimum crest level 5 cm below the setpoint. For example: the Outlet of polder basin 1 (node 4) is specified with a minimum crest level of 1.95 meter.\nSetup the outlets:\n\n# Set up outlet\nmodel.outlet.add(\n    Node(2, Point(0.0, -1.0)),\n    [outlet.Static(flow_rate=[2 * 0.5 / 3600], min_crest_level=[0.0])],\n)\nmodel.outlet.add(\n    Node(5, Point(0.0, -3.0)),\n    [outlet.Static(flow_rate=[0.5 / 3600], min_crest_level=[1.95])],\n)\nmodel.outlet.add(\n    Node(7, Point(1.0, -4.0)),\n    [outlet.Static(flow_rate=[0.5 / 3600], min_crest_level=[1.45])],\n)\nmodel.outlet.add(\n    Node(9, Point(3.0, -4.0)),\n    [outlet.Static(flow_rate=[0.5 / 3600], min_crest_level=[0.95])],\n)\nmodel.outlet.add(\n    Node(11, Point(4.0, -3.0)),\n    [outlet.Static(flow_rate=[0.5 / 3600], min_crest_level=[0.45])],\n)\n\nWhen using Outlets as connecting nodes, the flow over the Outlet needs to be controlled to maintain the water level at the setpoint. For this purpose we introduce local PidControllers, where the targets of the PidControllers are set to the setpoints. Disadvantage of this local control approach is the delay that is introduced to transport the ‘basin X has a shortage’ message upstream through the cascade to the inlet. Current functionality does not offer the capability for PidControl to take multiple observations into account when controlling the inlet. Combining multiple observations in one control is feasible with DiscreteControl. This could be an alternative approach to controlling the inlet for the cascading water system.\nSetup the PID control:\n\npid_control_data = {\n    \"listen_node_type\": \"Basin\",\n    \"proportional\": [0.05],\n    \"integral\": [0.00],\n    \"derivative\": [0.0],\n}\nmodel.pid_control.add(\n    Node(3, Point(-1.0, -1.0)),\n    [pid_control.Static(listen_node_id=[4], target=[2.0], **pid_control_data)],\n)\nmodel.pid_control.add(\n    Node(14, Point(-1.0, -3.0)),\n    [pid_control.Static(listen_node_id=[6], target=[1.5], **pid_control_data)],\n)\nmodel.pid_control.add(\n    Node(15, Point(1.0, -3.0)),\n    [pid_control.Static(listen_node_id=[8], target=[1.0], **pid_control_data)],\n)\nmodel.pid_control.add(\n    Node(16, Point(3.0, -3.0)),\n    [pid_control.Static(listen_node_id=[10], target=[0.5], **pid_control_data)],\n)\n\nSetup the edges:\n\nmodel.edge.add(model.basin[1], model.outlet[2])\nmodel.edge.add(model.pid_control[3], model.outlet[2])\nmodel.edge.add(model.outlet[2], model.basin[4])\nmodel.edge.add(model.basin[4], model.outlet[5])\nmodel.edge.add(model.outlet[5], model.basin[6])\nmodel.edge.add(model.basin[6], model.outlet[7])\nmodel.edge.add(model.outlet[7], model.basin[8])\nmodel.edge.add(model.basin[8], model.outlet[9])\nmodel.edge.add(model.outlet[9], model.basin[10])\nmodel.edge.add(model.basin[10], model.outlet[11])\nmodel.edge.add(model.outlet[11], model.basin[12])\nmodel.edge.add(model.basin[12], model.pump[13])\nmodel.edge.add(model.pump[13], model.basin[1])\nmodel.edge.add(model.pid_control[14], model.outlet[5])\nmodel.edge.add(model.pid_control[15], model.outlet[7])\nmodel.edge.add(model.pid_control[16], model.outlet[9])\n\nTo plot the model\n\nmodel.plot()\n\n\n\n\n\n\n\n\nWrite the model to a TOML file and run it in the Julia.\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"local_pidcontrolled_cascade/ribasim.toml\")\n\nPosixPath('data/local_pidcontrolled_cascade/ribasim.toml')\n\n\nAfter running the model, read back the result to plot the flow of each polder basin.\n\ndatadir_flow = datadir / \"local_pidcontrolled_cascade/results/flow.arrow\"\ndf_flow = pd.read_feather(datadir_flow)\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow_rate * 86400\n\ndf_pivot = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\")\n\nBelow graphs show the flow exchanged with the mainsystem (i.e. the inlet and the pump), and the flow of weirs inbetween the polder basins.\n\ndf_input = df_pivot.loc[:, [(1, 2), (13, 1)]]\ndf_input.plot(ylim=[-1.0, 20.0])\ndf_weirs = df_pivot.loc[:, [(4, 5), (6, 7), (8, 9), (10, 11)]]\ndf_weirs.plot(ylim=[-1.0, 15.0])\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nBelow graph shows the vertical flux on each basin.\n\ndatadir_basin = datadir / \"local_pidcontrolled_cascade/results/basin.arrow\"\ndf_basin = pd.read_feather(datadir_basin)\ndf_basin[\"vertical_flux\"] = (\n    df_basin[\"precipitation\"]\n    - df_basin[\"evaporation\"]\n    + df_basin[\"drainage\"]\n    + df_basin[\"infiltration\"]\n)\ndf_basin_wide = df_basin.pivot_table(\n    index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\", \"vertical_flux\"]\n)\ndf_basin_wide[\"vertical_flux\"].plot()\n\n\n\n\n\n\n\n\nIn the following graph, the water level of basins are shown. The five polder basins are given starting levels that are different from their setpoints. It can be observed that in the beginning, the water level are changing and approaching to the set points. Later when the water levels are stable, they will not be affected by the forcing.\n\ndf_basin_wide[\"level\"].plot()",
                         "crumbs": [
                           "Python tooling",
                           "Examples"
                    @@ -1244,7 +1244,7 @@
                         "href": "core/equations.html#sec-reduction_factor",
                         "title": "Equations",
                         "section": "2.1 The reduction factor",
                    -    "text": "2.1 The reduction factor\nAt several points in the equations below a reduction factor is used. This is a term that makes certain transitions more smooth, for instance when a pump stops providing water when its source basin dries up. The reduction factor is given by\n\\[\\begin{align}\n    \\phi(x; p) =\n    \\begin{cases}\n    0 &\\text{if}\\quad x < 0 \\\\\n        -2 \\left(\\frac{x}{p}\\right)^3 + 3\\left(\\frac{x}{p}\\right)^2 &\\text{if}\\quad 0 \\le x \\le p \\\\\n        1 &\\text{if}\\quad x > p\n    \\end{cases}\n\\end{align}\\]\nHere \\(p > 0\\) is the threshold value which determines the interval \\([0,p]\\) of the smooth transition between \\(0\\) and \\(1\\), see the plot below.\n\n\nCode\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef f(x, p = 3):\n    x_scaled = x / p\n    phi = (-2 * x_scaled + 3) * x_scaled**2\n    phi = np.where(x < 0, 0, phi)\n    phi = np.where(x > p, 1, phi)\n\n    return phi\n\nfontsize = 15\np = 3\nN = 100\nx_min = -1\nx_max = 4\nx = np.linspace(x_min,x_max,N)\nphi = f(x,p)\n\nfig,ax = plt.subplots(dpi=80)\nax.plot(x,phi)\n\ny_lim = ax.get_ylim()\n\nax.set_xticks([0,p], [0,\"$p$\"], fontsize=fontsize)\nax.set_yticks([0,1], [0,1], fontsize=fontsize)\nax.hlines([0,1],x_min,x_max, color = \"k\", ls = \":\", zorder=-1)\nax.vlines([0,p], *y_lim, color = \"k\", ls = \":\")\nax.set_xlim(x_min,x_max)\nax.set_xlabel(\"$x$\", fontsize=fontsize)\nax.set_ylabel(\"$\\phi(x;p)$\", fontsize=fontsize)\nax.set_ylim(y_lim)\n\nfig.tight_layout()\nplt.show()\n\n\n<>:31: SyntaxWarning:\n\ninvalid escape sequence '\\p'\n\n<>:31: SyntaxWarning:\n\ninvalid escape sequence '\\p'\n\n/tmp/ipykernel_5703/665069857.py:31: SyntaxWarning:\n\ninvalid escape sequence '\\p'",
                    +    "text": "2.1 The reduction factor\nAt several points in the equations below a reduction factor is used. This is a term that makes certain transitions more smooth, for instance when a pump stops providing water when its source basin dries up. The reduction factor is given by\n\\[\\begin{align}\n    \\phi(x; p) =\n    \\begin{cases}\n    0 &\\text{if}\\quad x < 0 \\\\\n        -2 \\left(\\frac{x}{p}\\right)^3 + 3\\left(\\frac{x}{p}\\right)^2 &\\text{if}\\quad 0 \\le x \\le p \\\\\n        1 &\\text{if}\\quad x > p\n    \\end{cases}\n\\end{align}\\]\nHere \\(p > 0\\) is the threshold value which determines the interval \\([0,p]\\) of the smooth transition between \\(0\\) and \\(1\\), see the plot below.\n\n\nCode\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef f(x, p = 3):\n    x_scaled = x / p\n    phi = (-2 * x_scaled + 3) * x_scaled**2\n    phi = np.where(x < 0, 0, phi)\n    phi = np.where(x > p, 1, phi)\n\n    return phi\n\nfontsize = 15\np = 3\nN = 100\nx_min = -1\nx_max = 4\nx = np.linspace(x_min,x_max,N)\nphi = f(x,p)\n\nfig,ax = plt.subplots(dpi=80)\nax.plot(x,phi)\n\ny_lim = ax.get_ylim()\n\nax.set_xticks([0,p], [0,\"$p$\"], fontsize=fontsize)\nax.set_yticks([0,1], [0,1], fontsize=fontsize)\nax.hlines([0,1],x_min,x_max, color = \"k\", ls = \":\", zorder=-1)\nax.vlines([0,p], *y_lim, color = \"k\", ls = \":\")\nax.set_xlim(x_min,x_max)\nax.set_xlabel(\"$x$\", fontsize=fontsize)\nax.set_ylabel(\"$\\phi(x;p)$\", fontsize=fontsize)\nax.set_ylim(y_lim)\n\nfig.tight_layout()\nplt.show()\n\n\n<>:31: SyntaxWarning:\n\ninvalid escape sequence '\\p'\n\n<>:31: SyntaxWarning:\n\ninvalid escape sequence '\\p'\n\n/tmp/ipykernel_5555/665069857.py:31: SyntaxWarning:\n\ninvalid escape sequence '\\p'",
                         "crumbs": [
                           "Julia core",
                           "Equations"