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
= 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 @@ flow_in_min 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"