From 1e21905b3689ba5e1481e814e6ec0ba4a1274383 Mon Sep 17 00:00:00 2001 From: Quarto GHA Workflow Runner Date: Mon, 18 Sep 2023 11:07:15 +0000 Subject: [PATCH] Built site for gh-pages --- .nojekyll | 2 +- build/index.html | 166 +++++++++--------- contribute/addnode.html | 5 +- contribute/python.html | 97 +++++----- python/examples.html | 2 +- schema/DiscreteControlCondition.schema.json | 4 +- schema/FlowBoundaryStatic.schema.json | 4 +- schema/LevelBoundaryStatic.schema.json | 4 +- schema/LinearResistanceStatic.schema.json | 4 +- schema/ManningResistanceStatic.schema.json | 4 +- schema/OutletStatic.schema.json | 16 +- schema/PIDControlStatic.schema.json | 4 +- schema/PumpStatic.schema.json | 12 +- schema/TabulatedRatingCurveStatic.schema.json | 4 +- schema/UserStatic.schema.json | 4 +- search.json | 27 +-- 16 files changed, 167 insertions(+), 192 deletions(-) diff --git a/.nojekyll b/.nojekyll index b867834c0..7c45aa96f 100644 --- a/.nojekyll +++ b/.nojekyll @@ -1 +1 @@ -1563874e \ No newline at end of file +3d3a2e08 \ No newline at end of file diff --git a/build/index.html b/build/index.html index 48c8228ef..21bd4b512 100644 --- a/build/index.html +++ b/build/index.html @@ -236,54 +236,54 @@

1 API Reference1.1 Functions

# Ribasim.basin_bottomMethod.

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

-

source

+

source

# Ribasim.basin_bottomsMethod.

Get the bottom on both ends of a node. If only one has a bottom, use that for both.

-

source

+

source

# Ribasim.create_callbacksMethod.

Create the different callbacks that are used to store output and feed the simulation with new data. The different callbacks are combined to a CallbackSet that goes to the integrator. Returns the CallbackSet and the SavedValues for flow.

-

source

+

source

# Ribasim.create_graphMethod.

Return a directed graph, and a mapping from source and target nodes to edge fid.

-

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.discrete_control_affect!Method.

Change parameters based on the control logic.

-

source

+

source

# Ribasim.discrete_control_affect_downcrossing!Method.

An downcrossing means that a condition (always greater than) becomes false.

-

source

+

source

# Ribasim.discrete_control_affect_upcrossing!Method.

An upcrossing means that a condition (always greater than) becomes true.

-

source

+

source

# Ribasim.discrete_control_conditionMethod.

Listens for changes in condition truths.

-

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

#                         1 2 3 4 5 6 7 8 9
 Ribasim.findlastgroup(2, [5,4,2,2,5,2,2,2,1])
 # output
 6:8
-

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.formulate!Method.

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

-

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)
@@ -311,121 +311,121 @@

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

Get the compressor based on the Output

-

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

-

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

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

-

source

+

source

# Ribasim.input_pathMethod.

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

-

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 GeoPackage. 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 GeoPackage. Always returns a StructVector of the given struct type T, which is empty if the table is not found. This function validates the schema, and enforces the required sort order.

-

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

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

-

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

Calculate a profile storage by integrating the areas over the levels

-

source

+

source

# Ribasim.qh_interpolationMethod.

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

-

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

Copy the current flow to the SavedValues

-

source

+

source

# Ribasim.scalar_interpolation_derivativeMethod.

Derivative of scalar interpolation.

-

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_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_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 NaN, 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 GeoPackage into memory can be sorted. Tables loaded from Arrow files are memory mapped and can therefore not be sorted.

-

source

+

source

# Ribasim.update_basinMethod.

Load updates from ‘Basin / time’ into the parameters

-

source

+

source

# Ribasim.update_jac_prototype!Method.

Method for nodes that do not contribute to the Jacobian

-

source

+

source

# Ribasim.update_jac_prototype!Method.

The controlled basin affects itself and the basins upstream and downstream of the controlled pump affect eachother if there is a basin upstream of the pump. The state for the integral term and the controlled basin affect eachother, and the same for the integral state and the basin upstream of the pump if it is indeed a basin.

-

source

+

source

# Ribasim.update_jac_prototype!Method.

If both the unique node upstream and the unique node downstream of these nodes are basins, then these directly depend on eachother and affect the Jacobian 2x Basins always depend on themselves.

-

source

+

source

# Ribasim.update_jac_prototype!Method.

If both the unique node upstream and the nodes down stream (or one node further if a fractional flow is in between) are basins, then the downstream basin depends on the upstream basin(s) and affect the Jacobian as many times as there are downstream basins Upstream basins always depend on themselves.

-

source

+

source

# Ribasim.update_tabulated_rating_curve!Method.

Load updates from ‘TabulatedRatingCurve / time’ into the parameters

-

source

+

source

# Ribasim.valid_discrete_controlMethod.

Check:

    @@ -433,35 +433,35 @@

    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.water_balance!Method.

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

    -

    source

    +

    source

    # Ribasim.config.algorithmMethod.

    Create an OrdinaryDiffEqAlgorithm from solver config

    -

    source

    +

    source

    # Ribasim.config.snake_caseMethod.

    Convert a string from CamelCase to snake_case.

    -

    source

    +

    source

    @@ -476,24 +476,24 @@

    source

    +

    source

    # Ribasim.ConnectivityType.

    Store the connectivity information

    graphflow, graphcontrol: directed graph with vertices equal to ids flow: store the flow on every flow edge edgeidsflow, edgeidscontrol: get the external edge id from (src, dst) edgeconnectiontypeflow, edgeconnectiontypescontrol: get (srcnodetype, dstnodetype) from edge id

    if autodiff T = DiffCache{SparseArrays.SparseMatrixCSC{Float64, Int64}, Vector{Float64}} else T = SparseMatrixCSC{Float64, Int} end

    -

    source

    +

    source

    # Ribasim.DiscreteControlType.

    nodeid: node ID of the DiscreteControl node; these are not unique but repeated by the amount of conditions of this DiscreteControl node listenfeatureid: the ID of the node/edge being condition on variable: the name of the variable in the condition greaterthan: The threshold value in the condition conditionvalue: The current value of each condition controlstate: Dictionary: node ID => (control state, control state start) logic_mapping: Dictionary: (control node ID, truth state) => control state record: Namedtuple with discrete control information for output

    -

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

      @@ -502,10 +502,10 @@

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

      Requirements:

        @@ -513,7 +513,7 @@

        source

        +

        source

        # Ribasim.ManningResistanceType.

        This is a simple Manning-Gauckler reach connection.

          @@ -543,7 +543,7 @@

          source

          +

          source

          # Ribasim.ModelType.

          Model(
               sys::MTK.AbstractODESystem,
          @@ -552,33 +552,33 @@ 

          integrator::SciMLBase.AbstractODEIntegrator )

          Struct that combines data from the System and Integrator that we will need during and after model construction.

          -

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

          struct TabulatedRatingCurve{C}

          Rating curve from level to discharge. The rating curve is a lookup table with linear interpolation in between. Relation can be updated in time, which is done by moving data from the time field into the tables, which is done in the update_tabulated_rating_curve callback.

          Type parameter C indicates the content backing the StructVector, which can be a NamedTuple of Vectors or Arrow Primitives, and is added to avoid type instabilities.

          nodeid: node ID of the TabulatedRatingCurve node active: whether this node is active and thus contributes flows tables: The current Q(h) relationships time: The time table used for updating the tables controlmapping: dictionary from (nodeid, controlstate) to Q(h) and/or active state

          -

          source

          +

          source

          # Ribasim.TerminalType.

          node_id: node ID of the Terminal node

          -

          source

          +

          source

          # Ribasim.UserType.

          demand: water flux demand of user over time active: whether this node is active and thus demands water allocated: water flux currently allocated to user returnfactor: the factor in [0,1] of how much of the abstracted water is given back to the system minlevel: The level of the source basin below which the user does not abstract priority: integer > 0, the lower the number the higher the priority of the users demand

          -

          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

          @@ -618,8 +618,8 @@

          Ribasim.findlastgroup
        • Ribasim.findsorted
        • Ribasim.formulate!
        • -
        • Ribasim.formulate_flow!
        • Ribasim.formulate_flow!
        • +
        • Ribasim.formulate_flow!
        • Ribasim.formulate_flow!
        • Ribasim.get_area_and_level
        • Ribasim.get_compressor
        • @@ -651,10 +651,10 @@

          Ribasim.set_table_row!
        • Ribasim.sorted_table!
        • Ribasim.update_basin
        • -
        • Ribasim.update_jac_prototype!
        • -
        • Ribasim.update_jac_prototype!
        • -
        • Ribasim.update_jac_prototype!
        • Ribasim.update_jac_prototype!
        • +
        • Ribasim.update_jac_prototype!
        • +
        • Ribasim.update_jac_prototype!
        • +
        • Ribasim.update_jac_prototype!
        • Ribasim.update_tabulated_rating_curve!
        • Ribasim.valid_discrete_control
        • Ribasim.valid_edge_types
        • diff --git a/contribute/addnode.html b/contribute/addnode.html index 9e7941a30..7b2e55ed0 100644 --- a/contribute/addnode.html +++ b/contribute/addnode.html @@ -423,7 +423,7 @@

          4 Validation

          5 Tests

          -

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

          +

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

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

          See here for monitoring of Python test coverage.

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

          @@ -456,8 +456,7 @@

          7 Finishing up

          To generate the Python module models.py and config.py from the JSON Schemas, run:

          -
          datamodel-codegen --use-title-as-name --use-double-quotes --disable-timestamp --use-default --strict-nullable --input docs/schema/root.schema.json --output python/ribasim/ribasim/models.py
          -datamodel-codegen --use-title-as-name --use-double-quotes --disable-timestamp --use-default --strict-nullable --input docs/schema/Config.schema.json --output python/ribasim/ribasim/config.py
          +
          pixi run codegen

          Since adding a node type touches both the Python and Julia code, it is a good idea to run both the Python test suite and Julia test suite locally before creating a pull request.

          diff --git a/contribute/python.html b/contribute/python.html index 3d01bf994..967d7b337 100644 --- a/contribute/python.html +++ b/contribute/python.html @@ -210,11 +210,7 @@

          On this page

        • 1.2 Running the tests
        • 1.3 Updating example notebooks
        • 1.4 Prepare model input
        • -
        • 1.5 Setup Visual Studio Code (optional)
        • -
        • 1.6 How to publish to PyPI
        • -
        • 1.7 Linting
        -
      • 2 Code maintenance
      @@ -258,55 +254,56 @@

      1.4 Prepare model input

      -

      Before running the Julia tests or building binaries, example model input needs to created. This is done by running the following Python script:

      -
      python python/ribasim/tests/conftest.py
      -

      This places example model input files under ./data/. If the example models change, re-run this script.

      - -
      -

      1.5 Setup Visual Studio Code (optional)

      -
        -
      1. Install the Python, ruff and autoDocstring extensions.

      2. -
      3. Copy .vscode/settings_template.json into .vscode/settings.json

      4. -
      -
      -
      -

      1.6 How to publish to PyPI

      -
        -
      1. Update __version__ in ribasim/__init__.py

      2. -
      3. Open a terminal and run cd python/ribasim

      4. -
      5. Activate the ribasim environment with conda activate ribasim

      6. -
      7. If present remove dist folder

      8. -
      9. Re-create the wheels:

      10. -
      -
      python -m build
      -
        -
      1. Check the package files:
      2. -
      -
      twine check dist/*
      -
        -
      1. Make a new commit with the updated version number, and push to remote

      2. -
      3. Re-upload the new files:

      4. -
      -
      twine upload dist/*
      -
      -
      -

      1.7 Linting

      -

      To run our linting suite locally, execute:

      -
      pixi run lint
      -
      - -
      -

      2 Code maintenance

      -

      For new features new tests have to be added. To monitor how much of the code is covered by the tests we use Codecov. For a simple overview of the local code coverage run

      -
      pixi shell
      -pytest --cov=ribasim tests/
      -

      from python/ribasim. For an extensive overview in html format use

      -
      pixi shell
      -pytest --cov=ribasim --cov-report=html tests/
      -

      which creates a folder htmlcov in the working directory. To see te contents open htmlcov/index.html in a browser.

      +

      Before running the Julia tests or building binaries, example model input needs to created. This is done by running the following:

      +
      pixi run generate-testmodels```
      +
      +This places example model input files under `./generated_testmodels/`.
      +If the example models change, re-run this script.
      +
      +## Setup Visual Studio Code (optional) {#sec-vscode}
      +
      +1. Install the [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python), [ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) and [autoDocstring](https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring) extensions.
      +
      +2. Copy `.vscode/settings_template.json` into `.vscode/settings.json`
      +
      +## How to publish to PyPI
      +
      +1) Update `__version__` in `ribasim/__init__.py`
      +
      +2) Open a terminal and run `cd python/ribasim`
      +
      +3) Activate the ribasim environment with `conda activate ribasim`
      +
      +4) If present remove dist folder
      +
      +5) Re-create the wheels:
      +

      python -m build

      +
      
      +6) Check the package files:
      +

      twine check dist/*

      +
      
      +7) Make a new commit with the updated version number, and push to remote
      +
      +8) Re-upload the new files:
      +

      twine upload dist/*

      +
      
      +## Linting
      +
      +To run our linting suite locally, execute:
      +
      +

      pixi run lint

      +
      
      +# Code maintenance {#sec-codecov}
      +
      +For new features new tests have to be added. To monitor how much of the code is covered by the tests we use [Codecov](https://about.codecov.io/).
      +For a simple overview of the local code coverage run
      +

      pixi shell pytest –cov=ribasim tests/

      +
      from  `python/ribasim`. For an extensive overview in `html` format use
      +

      pixi shell pytest –cov=ribasim –cov-report=html tests/ ``which creates a folderhtmlcovin the working directory. To see te contents openhtmlcov/index.html` in a browser.

      The code coverage of pushed branches can be seen here.

      +
      diff --git a/python/examples.html b/python/examples.html index 2f322889d..ae7aa44de 100644 --- a/python/examples.html +++ b/python/examples.html @@ -548,7 +548,7 @@

      2 Update the basi ax = df_flow.pivot_table(index="time", columns="edge", values="flow_m3d").plot() ax.legend(bbox_to_anchor=(1.3, 1), title="Edge")
      -
      <matplotlib.legend.Legend at 0x7f75574d6350>
      +
      <matplotlib.legend.Legend at 0x7ff8040b4f50>

      diff --git a/schema/DiscreteControlCondition.schema.json b/schema/DiscreteControlCondition.schema.json index 00cd84ec3..3ce188fe2 100644 --- a/schema/DiscreteControlCondition.schema.json +++ b/schema/DiscreteControlCondition.schema.json @@ -27,10 +27,10 @@ "format": "default", "anyOf": [ { - "type": "number" + "type": "null" }, { - "type": "null" + "type": "number" } ] } diff --git a/schema/FlowBoundaryStatic.schema.json b/schema/FlowBoundaryStatic.schema.json index 429fe8f20..8300ea9fd 100644 --- a/schema/FlowBoundaryStatic.schema.json +++ b/schema/FlowBoundaryStatic.schema.json @@ -11,10 +11,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, diff --git a/schema/LevelBoundaryStatic.schema.json b/schema/LevelBoundaryStatic.schema.json index 85ff75b67..f635afce0 100644 --- a/schema/LevelBoundaryStatic.schema.json +++ b/schema/LevelBoundaryStatic.schema.json @@ -11,10 +11,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, diff --git a/schema/LinearResistanceStatic.schema.json b/schema/LinearResistanceStatic.schema.json index c0261ad6f..5a5146ea9 100644 --- a/schema/LinearResistanceStatic.schema.json +++ b/schema/LinearResistanceStatic.schema.json @@ -11,10 +11,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, diff --git a/schema/ManningResistanceStatic.schema.json b/schema/ManningResistanceStatic.schema.json index f8ec17058..22bad56df 100644 --- a/schema/ManningResistanceStatic.schema.json +++ b/schema/ManningResistanceStatic.schema.json @@ -19,10 +19,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, diff --git a/schema/OutletStatic.schema.json b/schema/OutletStatic.schema.json index 8c24fac8c..d8feae988 100644 --- a/schema/OutletStatic.schema.json +++ b/schema/OutletStatic.schema.json @@ -5,10 +5,10 @@ "format": "default", "anyOf": [ { - "type": "number" + "type": "null" }, { - "type": "null" + "type": "number" } ] }, @@ -22,10 +22,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, @@ -33,10 +33,10 @@ "format": "default", "anyOf": [ { - "type": "number" + "type": "null" }, { - "type": "null" + "type": "number" } ] }, @@ -63,10 +63,10 @@ "format": "default", "anyOf": [ { - "type": "number" + "type": "null" }, { - "type": "null" + "type": "number" } ] } diff --git a/schema/PIDControlStatic.schema.json b/schema/PIDControlStatic.schema.json index c3226017b..df64576ee 100644 --- a/schema/PIDControlStatic.schema.json +++ b/schema/PIDControlStatic.schema.json @@ -19,10 +19,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, diff --git a/schema/PumpStatic.schema.json b/schema/PumpStatic.schema.json index fa15283fa..afe80e42c 100644 --- a/schema/PumpStatic.schema.json +++ b/schema/PumpStatic.schema.json @@ -5,10 +5,10 @@ "format": "default", "anyOf": [ { - "type": "number" + "type": "null" }, { - "type": "null" + "type": "number" } ] }, @@ -22,10 +22,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, @@ -52,10 +52,10 @@ "format": "default", "anyOf": [ { - "type": "number" + "type": "null" }, { - "type": "null" + "type": "number" } ] } diff --git a/schema/TabulatedRatingCurveStatic.schema.json b/schema/TabulatedRatingCurveStatic.schema.json index c073e11c1..cb244575b 100644 --- a/schema/TabulatedRatingCurveStatic.schema.json +++ b/schema/TabulatedRatingCurveStatic.schema.json @@ -11,10 +11,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, diff --git a/schema/UserStatic.schema.json b/schema/UserStatic.schema.json index d0c806834..c6a6ed0e4 100644 --- a/schema/UserStatic.schema.json +++ b/schema/UserStatic.schema.json @@ -15,10 +15,10 @@ "format": "default", "anyOf": [ { - "type": "boolean" + "type": "null" }, { - "type": "null" + "type": "boolean" } ] }, diff --git a/search.json b/search.json index 57a58f054..f0653c334 100644 --- a/search.json +++ b/search.json @@ -340,7 +340,7 @@ "href": "contribute/python.html", "title": "Python tooling development", "section": "", - "text": "First, set up pixi as described on their getting started page.\nThen set up the environment by running the following commands:\npixi install\npixi run post-install\n\n\n\nIn order to run tests on Ribasim Python execute\npixi run test-ribasim-python\n\n\n\nMake sure to run Clear All Outputs on the notebook before committing.\n\n\n\nBefore running the Julia tests or building binaries, example model input needs to created. This is done by running the following Python script:\npython python/ribasim/tests/conftest.py\nThis places example model input files under ./data/. If the example models change, re-run this script.\n\n\n\n\nInstall the Python, ruff and autoDocstring extensions.\nCopy .vscode/settings_template.json into .vscode/settings.json\n\n\n\n\n\nUpdate __version__ in ribasim/__init__.py\nOpen a terminal and run cd python/ribasim\nActivate the ribasim environment with conda activate ribasim\nIf present remove dist folder\nRe-create the wheels:\n\npython -m build\n\nCheck the package files:\n\ntwine check dist/*\n\nMake a new commit with the updated version number, and push to remote\nRe-upload the new files:\n\ntwine upload dist/*\n\n\n\nTo run our linting suite locally, execute:\npixi run lint" + "text": "First, set up pixi as described on their getting started page.\nThen set up the environment by running the following commands:\npixi install\npixi run post-install\n\n\n\nIn order to run tests on Ribasim Python execute\npixi run test-ribasim-python\n\n\n\nMake sure to run Clear All Outputs on the notebook before committing.\n\n\n\nBefore running the Julia tests or building binaries, example model input needs to created. This is done by running the following:\npixi run generate-testmodels```\n\nThis places example model input files under `./generated_testmodels/`.\nIf the example models change, re-run this script.\n\n## Setup Visual Studio Code (optional) {#sec-vscode}\n\n1. Install the [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python), [ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) and [autoDocstring](https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring) extensions.\n\n2. Copy `.vscode/settings_template.json` into `.vscode/settings.json`\n\n## How to publish to PyPI\n\n1) Update `__version__` in `ribasim/__init__.py`\n\n2) Open a terminal and run `cd python/ribasim`\n\n3) Activate the ribasim environment with `conda activate ribasim`\n\n4) If present remove dist folder\n\n5) Re-create the wheels:\npython -m build\n\n6) Check the package files:\ntwine check dist/*\n\n7) Make a new commit with the updated version number, and push to remote\n\n8) Re-upload the new files:\ntwine upload dist/*\n\n## Linting\n\nTo run our linting suite locally, execute:\n\npixi run lint\n\n# Code maintenance {#sec-codecov}\n\nFor new features new tests have to be added. To monitor how much of the code is covered by the tests we use [Codecov](https://about.codecov.io/).\nFor a simple overview of the local code coverage run\npixi shell pytest –cov=ribasim tests/\nfrom `python/ribasim`. For an extensive overview in `html` format use\npixi shell pytest –cov=ribasim –cov-report=html tests/ ``which creates a folderhtmlcovin the working directory. To see te contents openhtmlcov/index.html` in a browser.\nThe code coverage of pushed branches can be seen here." }, { "objectID": "contribute/python.html#setting-up-pixi", @@ -368,28 +368,7 @@ "href": "contribute/python.html#prepare-model-input", "title": "Python tooling development", "section": "", - "text": "Before running the Julia tests or building binaries, example model input needs to created. This is done by running the following Python script:\npython python/ribasim/tests/conftest.py\nThis places example model input files under ./data/. If the example models change, re-run this script." - }, - { - "objectID": "contribute/python.html#sec-vscode", - "href": "contribute/python.html#sec-vscode", - "title": "Python tooling development", - "section": "", - "text": "Install the Python, ruff and autoDocstring extensions.\nCopy .vscode/settings_template.json into .vscode/settings.json" - }, - { - "objectID": "contribute/python.html#how-to-publish-to-pypi", - "href": "contribute/python.html#how-to-publish-to-pypi", - "title": "Python tooling development", - "section": "", - "text": "Update __version__ in ribasim/__init__.py\nOpen a terminal and run cd python/ribasim\nActivate the ribasim environment with conda activate ribasim\nIf present remove dist folder\nRe-create the wheels:\n\npython -m build\n\nCheck the package files:\n\ntwine check dist/*\n\nMake a new commit with the updated version number, and push to remote\nRe-upload the new files:\n\ntwine upload dist/*" - }, - { - "objectID": "contribute/python.html#linting", - "href": "contribute/python.html#linting", - "title": "Python tooling development", - "section": "", - "text": "To run our linting suite locally, execute:\npixi run lint" + "text": "Before running the Julia tests or building binaries, example model input needs to created. This is done by running the following:\npixi run generate-testmodels```\n\nThis places example model input files under `./generated_testmodels/`.\nIf the example models change, re-run this script.\n\n## Setup Visual Studio Code (optional) {#sec-vscode}\n\n1. Install the [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python), [ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) and [autoDocstring](https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring) extensions.\n\n2. Copy `.vscode/settings_template.json` into `.vscode/settings.json`\n\n## How to publish to PyPI\n\n1) Update `__version__` in `ribasim/__init__.py`\n\n2) Open a terminal and run `cd python/ribasim`\n\n3) Activate the ribasim environment with `conda activate ribasim`\n\n4) If present remove dist folder\n\n5) Re-create the wheels:\npython -m build\n\n6) Check the package files:\ntwine check dist/*\n\n7) Make a new commit with the updated version number, and push to remote\n\n8) Re-upload the new files:\ntwine upload dist/*\n\n## Linting\n\nTo run our linting suite locally, execute:\n\npixi run lint\n\n# Code maintenance {#sec-codecov}\n\nFor new features new tests have to be added. To monitor how much of the code is covered by the tests we use [Codecov](https://about.codecov.io/).\nFor a simple overview of the local code coverage run\npixi shell pytest –cov=ribasim tests/\nfrom `python/ribasim`. For an extensive overview in `html` format use\npixi shell pytest –cov=ribasim –cov-report=html tests/ ``which creates a folderhtmlcovin the working directory. To see te contents openhtmlcov/index.html` in a browser.\nThe code coverage of pushed branches can be seen here." }, { "objectID": "contribute/core.html", @@ -683,7 +662,7 @@ "href": "python/examples.html", "title": "Examples", "section": "", - "text": "1 Basic model with static forcing\n\nimport geopandas as gpd\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nimport ribasim\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1, 3, 3, 6, 6, 9, 9],\n \"area\": [0.01, 1000.0] * 4,\n \"level\": [0.0, 1.0] * 4,\n }\n)\n\n# Convert steady forcing to m/s\n# 2 mm/d precipitation, 1 mm/d evaporation\nseconds_in_day = 24 * 3600\nprecipitation = 0.002 / seconds_in_day\nevaporation = 0.001 / seconds_in_day\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [0],\n \"drainage\": [0.0],\n \"potential_evaporation\": [evaporation],\n \"infiltration\": [0.0],\n \"precipitation\": [precipitation],\n \"urban_runoff\": [0.0],\n }\n)\nstatic = static.iloc[[0, 0, 0, 0]]\nstatic[\"node_id\"] = [1, 3, 6, 9]\n\nbasin = ribasim.Basin(profile=profile, static=static)\n\nSetup linear resistance:\n\nlinear_resistance = ribasim.LinearResistance(\n static=pd.DataFrame(\n data={\"node_id\": [10, 12], \"resistance\": [5e3, (3600.0 * 24) / 100.0]}\n )\n)\n\nSetup Manning resistance:\n\nmanning_resistance = ribasim.ManningResistance(\n static=pd.DataFrame(\n data={\n \"node_id\": [2],\n \"length\": [900.0],\n \"manning_n\": [0.04],\n \"profile_width\": [6.0],\n \"profile_slope\": [3.0],\n }\n )\n)\n\nSet up a rating curve node:\n\n# Discharge: lose 1% of storage volume per day at storage = 1000.0.\nq1000 = 1000.0 * 0.01 / seconds_in_day\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\n \"node_id\": [4, 4],\n \"level\": [0.0, 1.0],\n \"discharge\": [0.0, q1000],\n }\n )\n)\n\nSetup fractional flows:\n\nfractional_flow = ribasim.FractionalFlow(\n static=pd.DataFrame(\n data={\n \"node_id\": [5, 8, 13],\n \"fraction\": [0.3, 0.6, 0.1],\n }\n )\n)\n\nSetup pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [7],\n \"flow_rate\": [0.5 / 3600],\n }\n )\n)\n\nSetup level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [11, 17],\n \"level\": [0.5, 1.5],\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [15, 16],\n \"flow_rate\": [1e-4, 1e-4],\n }\n )\n)\n\nSetup terminal:\n\nterminal = ribasim.Terminal(\n static=pd.DataFrame(\n data={\n \"node_id\": [14],\n }\n )\n)\n\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin,\n (1.0, 0.0), # 2: ManningResistance\n (2.0, 0.0), # 3: Basin\n (3.0, 0.0), # 4: TabulatedRatingCurve\n (3.0, 1.0), # 5: FractionalFlow\n (3.0, 2.0), # 6: Basin\n (4.0, 1.0), # 7: Pump\n (4.0, 0.0), # 8: FractionalFlow\n (5.0, 0.0), # 9: Basin\n (6.0, 0.0), # 10: LinearResistance\n (2.0, 2.0), # 11: LevelBoundary\n (2.0, 1.0), # 12: LinearResistance\n (3.0, -1.0), # 13: FractionalFlow\n (3.0, -2.0), # 14: Terminal\n (3.0, 3.0), # 15: FlowBoundary\n (0.0, 1.0), # 16: FlowBoundary\n (6.0, 1.0), # 17: LevelBoundary\n ]\n)\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_id, node_type = ribasim.Node.get_node_ids_and_types(\n basin,\n manning_resistance,\n rating_curve,\n pump,\n fractional_flow,\n linear_resistance,\n level_boundary,\n flow_boundary,\n terminal,\n)\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(node_id, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array(\n [1, 2, 3, 4, 4, 5, 6, 8, 7, 9, 11, 12, 4, 13, 15, 16, 10], dtype=np.int64\n)\nto_id = np.array(\n [2, 3, 4, 5, 8, 6, 7, 9, 9, 10, 12, 3, 13, 14, 6, 1, 17], dtype=np.int64\n)\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": len(from_id) * [\"flow\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"basic\",\n node=node,\n edge=edge,\n basin=basin,\n level_boundary=level_boundary,\n flow_boundary=flow_boundary,\n pump=pump,\n linear_resistance=linear_resistance,\n manning_resistance=manning_resistance,\n tabulated_rating_curve=rating_curve,\n fractional_flow=fractional_flow,\n terminal=terminal,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"basic\")\n\n\n\n2 Update the basic model with transient forcing\nThis assumes you have already created the basic model with static forcing.\n\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport ribasim\n\n\nmodel = ribasim.Model.from_toml(datadir / \"basic/basic.toml\")\n\n\ntime = pd.date_range(model.starttime, model.endtime)\nday_of_year = time.day_of_year.to_numpy()\nseconds_per_day = 24 * 60 * 60\nevaporation = (\n (-1.0 * np.cos(day_of_year / 365.0 * 2 * np.pi) + 1.0) * 0.0025 / seconds_per_day\n)\nrng = np.random.default_rng(seed=0)\nprecipitation = (\n rng.lognormal(mean=-1.0, sigma=1.7, size=time.size) * 0.001 / seconds_per_day\n)\n\nWe’ll use xarray to easily broadcast the values.\n\ntimeseries = (\n pd.DataFrame(\n data={\n \"node_id\": 1,\n \"time\": time,\n \"drainage\": 0.0,\n \"potential_evaporation\": evaporation,\n \"infiltration\": 0.0,\n \"precipitation\": precipitation,\n \"urban_runoff\": 0.0,\n }\n )\n .set_index(\"time\")\n .to_xarray()\n)\n\nbasin_ids = model.basin.static[\"node_id\"].to_numpy()\nbasin_nodes = xr.DataArray(\n np.ones(len(basin_ids)), coords={\"node_id\": basin_ids}, dims=[\"node_id\"]\n)\nforcing = (timeseries * basin_nodes).to_dataframe().reset_index()\n\n\nstate = pd.DataFrame(\n data={\n \"node_id\": basin_ids,\n \"level\": 1.4,\n \"concentration\": 0.0,\n }\n)\n\n\nmodel.basin.forcing = forcing\nmodel.basin.state = state\n\n\nmodel.modelname = \"basic_transient\"\nmodel.write(datadir / \"basic_transient\")\n\nNow run the model with ribasim basic-transient/basic.toml. After running the model, read back the output:\n\ndf_basin = pd.read_feather(datadir / \"basic_transient/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\ndf_basin_wide[\"level\"].plot()\n\n<Axes: xlabel='time'>\n\n\n\n\n\n\ndf_flow = pd.read_feather(datadir / \"basic_transient/output/flow.arrow\")\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow * 86400\nax = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\").plot()\nax.legend(bbox_to_anchor=(1.3, 1), title=\"Edge\")\n\n<matplotlib.legend.Legend at 0x7f75574d6350>\n\n\n\n\n\n\ntype(df_flow)\n\npandas.core.frame.DataFrame\n\n\n\n\n3 Model with discrete control\nThe model constructed below consists of a single basin which slowly drains trough a TabulatedRatingCurve, but is held within a range around a target level (setpoint) by two connected pumps. These two pumps behave like a reversible pump. When pumping can be done in only one direction, and the other direction is only possible under gravity, use an Outlet for that direction.\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin\n (1.0, 0.5), # 2: Pump\n (1.0, -0.5), # 3: Pump\n (2.0, 0.0), # 4: LevelBoundary\n (-1.0, 0.0), # 5: TabulatedRatingCurve\n (-2.0, 0.0), # 6: Terminal\n (1.0, 0.0), # 7: DiscreteControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"Basin\",\n \"Pump\",\n \"Pump\",\n \"LevelBoundary\",\n \"TabulatedRatingCurve\",\n \"Terminal\",\n \"DiscreteControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 3, 4, 2, 1, 5, 7, 7], dtype=np.int64)\nto_id = np.array([3, 4, 2, 1, 5, 6, 2, 3], dtype=np.int64)\n\nedge_type = 6 * [\"flow\"] + 2 * [\"control\"]\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\"from_node_id\": from_id, \"to_node_id\": to_id, \"edge_type\": edge_type},\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1],\n \"area\": [1000.0, 1000.0],\n \"level\": [0.0, 1.0],\n }\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [1],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(data={\"node_id\": [1], \"level\": [20.0]})\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the discrete control:\n\ncondition = pd.DataFrame(\n data={\n \"node_id\": 3 * [7],\n \"listen_feature_id\": 3 * [1],\n \"variable\": 3 * [\"level\"],\n \"greater_than\": [5.0, 10.0, 15.0], # min, setpoint, max\n }\n)\n\nlogic = pd.DataFrame(\n data={\n \"node_id\": 5 * [7],\n \"truth_state\": [\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n \"control_state\": [\"in\", \"in\", \"none\", \"out\", \"out\"],\n }\n)\n\ndiscrete_control = ribasim.DiscreteControl(condition=condition, logic=logic)\n\nThe above control logic can be summarized as follows: - If the level gets above the maximum, activate the control state “out” until the setpoint is reached; - If the level gets below the minimum, active the control state “in” until the setpoint is reached; - Otherwise activate the control state “none”.\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": 3 * [2] + 3 * [3],\n \"control_state\": 2 * [\"none\", \"in\", \"out\"],\n \"flow_rate\": [0.0, 2e-3, 0.0, 0.0, 0.0, 2e-3],\n }\n )\n)\n\nThe pump data defines the following:\n\n\n\nControl state\nPump #2 flow rate (m/s)\nPump #3 flow rate (m/s)\n\n\n\n\n“none”\n0.0\n0.0\n\n\n“in”\n2e-3\n0.0\n\n\n“out”\n0.0\n2e-3\n\n\n\nSetup the level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(data={\"node_id\": [4], \"level\": [10.0]})\n)\n\nSetup the rating curve:\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\"node_id\": 2 * [5], \"level\": [2.0, 15.0], \"discharge\": [0.0, 1e-3]}\n )\n)\n\nSetup the terminal:\n\nterminal = ribasim.Terminal(static=pd.DataFrame(data={\"node_id\": [6]}))\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"level_setpoint_with_minmax\",\n node=node,\n edge=edge,\n basin=basin,\n pump=pump,\n level_boundary=level_boundary,\n tabulated_rating_curve=rating_curve,\n terminal=terminal,\n discrete_control=discrete_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nListen edges are plotted with a dashed line since they are not present in the “Edge / static” schema but only in the “Control / condition” schema.\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"level_setpoint_with_minmax\")\n\nNow run the model with level_setpoint_with_minmax/level_setpoint_with_minmax.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"level_setpoint_with_minmax/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\n\nax = df_basin_wide[\"level\"].plot()\n\ngreater_than = model.discrete_control.condition.greater_than\n\nax.hlines(\n greater_than,\n df_basin.time[0],\n df_basin.time.max(),\n lw=1,\n ls=\"--\",\n color=\"k\",\n)\n\ndf_control = pd.read_feather(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\ny_min, y_max = ax.get_ybound()\nax.fill_between(df_control.time[:2], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\nax.fill_between(df_control.time[2:4], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\n\nax.set_xticks(\n date2num(df_control.time).tolist(),\n df_control.control_state.tolist(),\n rotation=50,\n)\n\nax.set_yticks(greater_than, [\"min\", \"setpoint\", \"max\"])\nax.set_ylabel(\"level\")\nplt.show()\n\n\n\n\nThe highlighted regions show where a pump is active.\nLet’s print an overview of what happened with control:\n\nmodel.print_discrete_control_record(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\n0. At 2020-01-01 00:00:00 the control node with ID 7 reached truth state TTT:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level > 15.0\n\n This yielded control state \"out\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.002\n\n1. At 2020-02-09 01:17:29.324000 the control node with ID 7 reached truth state TFF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n2. At 2020-07-05 13:24:51.165000 the control node with ID 7 reached truth state FFF:\n For node ID 1 (Basin): level < 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"in\":\n For node ID 2 (Pump): flow_rate = 0.002\n For node ID 3 (Pump): flow_rate = 0.0\n\n3. At 2020-08-11 11:49:59.015000 the control node with ID 7 reached truth state TTF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n\n\nNote that crossing direction specific truth states (containing “U”, “D”) are not present in this overview even though they are part of the control logic. This is because in the control logic for this model these truth states are only used to sustain control states, while the overview only shows changes in control states.\n\n\n4 Model with PID control\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: FlowBoundary\n (1.0, 0.0), # 2: Basin\n (2.0, 0.5), # 3: Pump\n (3.0, 0.0), # 4: LevelBoundary\n (1.5, 1.0), # 5: PidControl\n (2.0, -0.5), # 6: outlet\n (1.5, -1.0), # 7: PidControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"FlowBoundary\",\n \"Basin\",\n \"Pump\",\n \"LevelBoundary\",\n \"PidControl\",\n \"Outlet\",\n \"PidControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 2, 3, 4, 6, 5, 7], dtype=np.int64)\nto_id = np.array([2, 3, 4, 6, 2, 3, 6], dtype=np.int64)\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": 5 * [\"flow\"] + 2 * [\"control\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\"node_id\": [2, 2], \"level\": [0.0, 1.0], \"area\": [1000.0, 1000.0]}\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"level\": [6.0],\n }\n)\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [3],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup the outlet:\n\noutlet = ribasim.Outlet(\n static=pd.DataFrame(\n data={\n \"node_id\": [6],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(data={\"node_id\": [1], \"flow_rate\": [1e-3]})\n)\n\nSetup flow boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [4],\n \"level\": [1.0], # Not relevant\n }\n )\n)\n\nSetup PID control:\n\npid_control = ribasim.PidControl(\n time=pd.DataFrame(\n data={\n \"node_id\": 4 * [5, 7],\n \"time\": [\n \"2020-01-01 00:00:00\",\n \"2020-01-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n ],\n \"listen_node_id\": 4 * [2, 2],\n \"target\": [5.0, 5.0, 5.0, 5.0, 7.5, 7.5, 7.5, 7.5],\n \"proportional\": 4 * [-1e-3, 1e-3],\n \"integral\": 4 * [-1e-7, 1e-7],\n \"derivative\": 4 * [0.0, 0.0],\n }\n )\n)\n\nNote that the coefficients for the pump and the outlet are equal in magnitude but opposite in sign. This way the pump and the outlet equally work towards the same goal, while having opposite effects on the controlled basin due to their connectivity to this basin.\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"pid_control\",\n node=node,\n edge=edge,\n basin=basin,\n flow_boundary=flow_boundary,\n level_boundary=level_boundary,\n pump=pump,\n outlet=outlet,\n pid_control=pid_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2020-12-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"pid_control\")\n\nNow run the model with ribasim pid_control/pid_control.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"pid_control/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\nax = df_basin_wide[\"level\"].plot()\nax.set_ylabel(\"level [m]\")\n\n# Plot target level\ntarget_levels = model.pid_control.time.target.to_numpy()[::2]\ntimes = date2num(model.pid_control.time.time)[::2]\nax.plot(times, target_levels, color=\"k\", ls=\":\", label=\"target level\");" + "text": "1 Basic model with static forcing\n\nimport geopandas as gpd\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nimport ribasim\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1, 3, 3, 6, 6, 9, 9],\n \"area\": [0.01, 1000.0] * 4,\n \"level\": [0.0, 1.0] * 4,\n }\n)\n\n# Convert steady forcing to m/s\n# 2 mm/d precipitation, 1 mm/d evaporation\nseconds_in_day = 24 * 3600\nprecipitation = 0.002 / seconds_in_day\nevaporation = 0.001 / seconds_in_day\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [0],\n \"drainage\": [0.0],\n \"potential_evaporation\": [evaporation],\n \"infiltration\": [0.0],\n \"precipitation\": [precipitation],\n \"urban_runoff\": [0.0],\n }\n)\nstatic = static.iloc[[0, 0, 0, 0]]\nstatic[\"node_id\"] = [1, 3, 6, 9]\n\nbasin = ribasim.Basin(profile=profile, static=static)\n\nSetup linear resistance:\n\nlinear_resistance = ribasim.LinearResistance(\n static=pd.DataFrame(\n data={\"node_id\": [10, 12], \"resistance\": [5e3, (3600.0 * 24) / 100.0]}\n )\n)\n\nSetup Manning resistance:\n\nmanning_resistance = ribasim.ManningResistance(\n static=pd.DataFrame(\n data={\n \"node_id\": [2],\n \"length\": [900.0],\n \"manning_n\": [0.04],\n \"profile_width\": [6.0],\n \"profile_slope\": [3.0],\n }\n )\n)\n\nSet up a rating curve node:\n\n# Discharge: lose 1% of storage volume per day at storage = 1000.0.\nq1000 = 1000.0 * 0.01 / seconds_in_day\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\n \"node_id\": [4, 4],\n \"level\": [0.0, 1.0],\n \"discharge\": [0.0, q1000],\n }\n )\n)\n\nSetup fractional flows:\n\nfractional_flow = ribasim.FractionalFlow(\n static=pd.DataFrame(\n data={\n \"node_id\": [5, 8, 13],\n \"fraction\": [0.3, 0.6, 0.1],\n }\n )\n)\n\nSetup pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [7],\n \"flow_rate\": [0.5 / 3600],\n }\n )\n)\n\nSetup level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [11, 17],\n \"level\": [0.5, 1.5],\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [15, 16],\n \"flow_rate\": [1e-4, 1e-4],\n }\n )\n)\n\nSetup terminal:\n\nterminal = ribasim.Terminal(\n static=pd.DataFrame(\n data={\n \"node_id\": [14],\n }\n )\n)\n\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin,\n (1.0, 0.0), # 2: ManningResistance\n (2.0, 0.0), # 3: Basin\n (3.0, 0.0), # 4: TabulatedRatingCurve\n (3.0, 1.0), # 5: FractionalFlow\n (3.0, 2.0), # 6: Basin\n (4.0, 1.0), # 7: Pump\n (4.0, 0.0), # 8: FractionalFlow\n (5.0, 0.0), # 9: Basin\n (6.0, 0.0), # 10: LinearResistance\n (2.0, 2.0), # 11: LevelBoundary\n (2.0, 1.0), # 12: LinearResistance\n (3.0, -1.0), # 13: FractionalFlow\n (3.0, -2.0), # 14: Terminal\n (3.0, 3.0), # 15: FlowBoundary\n (0.0, 1.0), # 16: FlowBoundary\n (6.0, 1.0), # 17: LevelBoundary\n ]\n)\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_id, node_type = ribasim.Node.get_node_ids_and_types(\n basin,\n manning_resistance,\n rating_curve,\n pump,\n fractional_flow,\n linear_resistance,\n level_boundary,\n flow_boundary,\n terminal,\n)\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(node_id, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array(\n [1, 2, 3, 4, 4, 5, 6, 8, 7, 9, 11, 12, 4, 13, 15, 16, 10], dtype=np.int64\n)\nto_id = np.array(\n [2, 3, 4, 5, 8, 6, 7, 9, 9, 10, 12, 3, 13, 14, 6, 1, 17], dtype=np.int64\n)\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": len(from_id) * [\"flow\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"basic\",\n node=node,\n edge=edge,\n basin=basin,\n level_boundary=level_boundary,\n flow_boundary=flow_boundary,\n pump=pump,\n linear_resistance=linear_resistance,\n manning_resistance=manning_resistance,\n tabulated_rating_curve=rating_curve,\n fractional_flow=fractional_flow,\n terminal=terminal,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"basic\")\n\n\n\n2 Update the basic model with transient forcing\nThis assumes you have already created the basic model with static forcing.\n\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport ribasim\n\n\nmodel = ribasim.Model.from_toml(datadir / \"basic/basic.toml\")\n\n\ntime = pd.date_range(model.starttime, model.endtime)\nday_of_year = time.day_of_year.to_numpy()\nseconds_per_day = 24 * 60 * 60\nevaporation = (\n (-1.0 * np.cos(day_of_year / 365.0 * 2 * np.pi) + 1.0) * 0.0025 / seconds_per_day\n)\nrng = np.random.default_rng(seed=0)\nprecipitation = (\n rng.lognormal(mean=-1.0, sigma=1.7, size=time.size) * 0.001 / seconds_per_day\n)\n\nWe’ll use xarray to easily broadcast the values.\n\ntimeseries = (\n pd.DataFrame(\n data={\n \"node_id\": 1,\n \"time\": time,\n \"drainage\": 0.0,\n \"potential_evaporation\": evaporation,\n \"infiltration\": 0.0,\n \"precipitation\": precipitation,\n \"urban_runoff\": 0.0,\n }\n )\n .set_index(\"time\")\n .to_xarray()\n)\n\nbasin_ids = model.basin.static[\"node_id\"].to_numpy()\nbasin_nodes = xr.DataArray(\n np.ones(len(basin_ids)), coords={\"node_id\": basin_ids}, dims=[\"node_id\"]\n)\nforcing = (timeseries * basin_nodes).to_dataframe().reset_index()\n\n\nstate = pd.DataFrame(\n data={\n \"node_id\": basin_ids,\n \"level\": 1.4,\n \"concentration\": 0.0,\n }\n)\n\n\nmodel.basin.forcing = forcing\nmodel.basin.state = state\n\n\nmodel.modelname = \"basic_transient\"\nmodel.write(datadir / \"basic_transient\")\n\nNow run the model with ribasim basic-transient/basic.toml. After running the model, read back the output:\n\ndf_basin = pd.read_feather(datadir / \"basic_transient/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\ndf_basin_wide[\"level\"].plot()\n\n<Axes: xlabel='time'>\n\n\n\n\n\n\ndf_flow = pd.read_feather(datadir / \"basic_transient/output/flow.arrow\")\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow * 86400\nax = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\").plot()\nax.legend(bbox_to_anchor=(1.3, 1), title=\"Edge\")\n\n<matplotlib.legend.Legend at 0x7ff8040b4f50>\n\n\n\n\n\n\ntype(df_flow)\n\npandas.core.frame.DataFrame\n\n\n\n\n3 Model with discrete control\nThe model constructed below consists of a single basin which slowly drains trough a TabulatedRatingCurve, but is held within a range around a target level (setpoint) by two connected pumps. These two pumps behave like a reversible pump. When pumping can be done in only one direction, and the other direction is only possible under gravity, use an Outlet for that direction.\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin\n (1.0, 0.5), # 2: Pump\n (1.0, -0.5), # 3: Pump\n (2.0, 0.0), # 4: LevelBoundary\n (-1.0, 0.0), # 5: TabulatedRatingCurve\n (-2.0, 0.0), # 6: Terminal\n (1.0, 0.0), # 7: DiscreteControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"Basin\",\n \"Pump\",\n \"Pump\",\n \"LevelBoundary\",\n \"TabulatedRatingCurve\",\n \"Terminal\",\n \"DiscreteControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 3, 4, 2, 1, 5, 7, 7], dtype=np.int64)\nto_id = np.array([3, 4, 2, 1, 5, 6, 2, 3], dtype=np.int64)\n\nedge_type = 6 * [\"flow\"] + 2 * [\"control\"]\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\"from_node_id\": from_id, \"to_node_id\": to_id, \"edge_type\": edge_type},\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1],\n \"area\": [1000.0, 1000.0],\n \"level\": [0.0, 1.0],\n }\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [1],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(data={\"node_id\": [1], \"level\": [20.0]})\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the discrete control:\n\ncondition = pd.DataFrame(\n data={\n \"node_id\": 3 * [7],\n \"listen_feature_id\": 3 * [1],\n \"variable\": 3 * [\"level\"],\n \"greater_than\": [5.0, 10.0, 15.0], # min, setpoint, max\n }\n)\n\nlogic = pd.DataFrame(\n data={\n \"node_id\": 5 * [7],\n \"truth_state\": [\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n \"control_state\": [\"in\", \"in\", \"none\", \"out\", \"out\"],\n }\n)\n\ndiscrete_control = ribasim.DiscreteControl(condition=condition, logic=logic)\n\nThe above control logic can be summarized as follows: - If the level gets above the maximum, activate the control state “out” until the setpoint is reached; - If the level gets below the minimum, active the control state “in” until the setpoint is reached; - Otherwise activate the control state “none”.\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": 3 * [2] + 3 * [3],\n \"control_state\": 2 * [\"none\", \"in\", \"out\"],\n \"flow_rate\": [0.0, 2e-3, 0.0, 0.0, 0.0, 2e-3],\n }\n )\n)\n\nThe pump data defines the following:\n\n\n\nControl state\nPump #2 flow rate (m/s)\nPump #3 flow rate (m/s)\n\n\n\n\n“none”\n0.0\n0.0\n\n\n“in”\n2e-3\n0.0\n\n\n“out”\n0.0\n2e-3\n\n\n\nSetup the level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(data={\"node_id\": [4], \"level\": [10.0]})\n)\n\nSetup the rating curve:\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\"node_id\": 2 * [5], \"level\": [2.0, 15.0], \"discharge\": [0.0, 1e-3]}\n )\n)\n\nSetup the terminal:\n\nterminal = ribasim.Terminal(static=pd.DataFrame(data={\"node_id\": [6]}))\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"level_setpoint_with_minmax\",\n node=node,\n edge=edge,\n basin=basin,\n pump=pump,\n level_boundary=level_boundary,\n tabulated_rating_curve=rating_curve,\n terminal=terminal,\n discrete_control=discrete_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nListen edges are plotted with a dashed line since they are not present in the “Edge / static” schema but only in the “Control / condition” schema.\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"level_setpoint_with_minmax\")\n\nNow run the model with level_setpoint_with_minmax/level_setpoint_with_minmax.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"level_setpoint_with_minmax/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\n\nax = df_basin_wide[\"level\"].plot()\n\ngreater_than = model.discrete_control.condition.greater_than\n\nax.hlines(\n greater_than,\n df_basin.time[0],\n df_basin.time.max(),\n lw=1,\n ls=\"--\",\n color=\"k\",\n)\n\ndf_control = pd.read_feather(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\ny_min, y_max = ax.get_ybound()\nax.fill_between(df_control.time[:2], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\nax.fill_between(df_control.time[2:4], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\n\nax.set_xticks(\n date2num(df_control.time).tolist(),\n df_control.control_state.tolist(),\n rotation=50,\n)\n\nax.set_yticks(greater_than, [\"min\", \"setpoint\", \"max\"])\nax.set_ylabel(\"level\")\nplt.show()\n\n\n\n\nThe highlighted regions show where a pump is active.\nLet’s print an overview of what happened with control:\n\nmodel.print_discrete_control_record(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\n0. At 2020-01-01 00:00:00 the control node with ID 7 reached truth state TTT:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level > 15.0\n\n This yielded control state \"out\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.002\n\n1. At 2020-02-09 01:17:29.324000 the control node with ID 7 reached truth state TFF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n2. At 2020-07-05 13:24:51.165000 the control node with ID 7 reached truth state FFF:\n For node ID 1 (Basin): level < 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"in\":\n For node ID 2 (Pump): flow_rate = 0.002\n For node ID 3 (Pump): flow_rate = 0.0\n\n3. At 2020-08-11 11:49:59.015000 the control node with ID 7 reached truth state TTF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n\n\nNote that crossing direction specific truth states (containing “U”, “D”) are not present in this overview even though they are part of the control logic. This is because in the control logic for this model these truth states are only used to sustain control states, while the overview only shows changes in control states.\n\n\n4 Model with PID control\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: FlowBoundary\n (1.0, 0.0), # 2: Basin\n (2.0, 0.5), # 3: Pump\n (3.0, 0.0), # 4: LevelBoundary\n (1.5, 1.0), # 5: PidControl\n (2.0, -0.5), # 6: outlet\n (1.5, -1.0), # 7: PidControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"FlowBoundary\",\n \"Basin\",\n \"Pump\",\n \"LevelBoundary\",\n \"PidControl\",\n \"Outlet\",\n \"PidControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 2, 3, 4, 6, 5, 7], dtype=np.int64)\nto_id = np.array([2, 3, 4, 6, 2, 3, 6], dtype=np.int64)\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": 5 * [\"flow\"] + 2 * [\"control\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\"node_id\": [2, 2], \"level\": [0.0, 1.0], \"area\": [1000.0, 1000.0]}\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"level\": [6.0],\n }\n)\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [3],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup the outlet:\n\noutlet = ribasim.Outlet(\n static=pd.DataFrame(\n data={\n \"node_id\": [6],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(data={\"node_id\": [1], \"flow_rate\": [1e-3]})\n)\n\nSetup flow boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [4],\n \"level\": [1.0], # Not relevant\n }\n )\n)\n\nSetup PID control:\n\npid_control = ribasim.PidControl(\n time=pd.DataFrame(\n data={\n \"node_id\": 4 * [5, 7],\n \"time\": [\n \"2020-01-01 00:00:00\",\n \"2020-01-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n ],\n \"listen_node_id\": 4 * [2, 2],\n \"target\": [5.0, 5.0, 5.0, 5.0, 7.5, 7.5, 7.5, 7.5],\n \"proportional\": 4 * [-1e-3, 1e-3],\n \"integral\": 4 * [-1e-7, 1e-7],\n \"derivative\": 4 * [0.0, 0.0],\n }\n )\n)\n\nNote that the coefficients for the pump and the outlet are equal in magnitude but opposite in sign. This way the pump and the outlet equally work towards the same goal, while having opposite effects on the controlled basin due to their connectivity to this basin.\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"pid_control\",\n node=node,\n edge=edge,\n basin=basin,\n flow_boundary=flow_boundary,\n level_boundary=level_boundary,\n pump=pump,\n outlet=outlet,\n pid_control=pid_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2020-12-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"pid_control\")\n\nNow run the model with ribasim pid_control/pid_control.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"pid_control/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\nax = df_basin_wide[\"level\"].plot()\nax.set_ylabel(\"level [m]\")\n\n# Plot target level\ntarget_levels = model.pid_control.time.target.to_numpy()[::2]\ntimes = date2num(model.pid_control.time.time)[::2]\nax.plot(times, target_levels, color=\"k\", ls=\":\", label=\"target level\");" }, { "objectID": "python/reference/Pump.html",