diff --git a/.gitignore b/.gitignore index 7b5586e8c..8ee64eb35 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,4 @@ report.xml # Designated working dir for working on Ribasim models models/ +playground/output diff --git a/.vscode/launch.json b/.vscode/launch.json index ba6cd8152..512027265 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,7 @@ { "version": "0.2.0", "configurations": [ - { + { "type": "julia", "request": "launch", "name": "Julia: current file", diff --git a/.vscode/settings.json b/.vscode/settings.json index 25b45cff7..d9180dfd7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "[julia]": { "editor.formatOnSave": true }, - "notebook.formatOnSave.enabled": true, + "notebook.formatOnSave.enabled": false, "notebook.codeActionsOnSave": { "source.fixAll.ruff": true, "source.organizeImports.ruff": true diff --git a/core/src/graph.jl b/core/src/graph.jl index 6ebf2a8a4..3725db2fc 100644 --- a/core/src/graph.jl +++ b/core/src/graph.jl @@ -6,8 +6,10 @@ and data of edges (EdgeMetadata): [`EdgeMetadata`](@ref) """ function create_graph(db::DB, config::Config, chunk_sizes::Vector{Int})::MetaGraph - node_rows = - execute(db, "SELECT node_id, node_type, subnetwork_id FROM Node ORDER BY fid") + node_rows = execute( + db, + "SELECT node_id, node_type, subnetwork_id FROM Node ORDER BY node_type, node_id", + ) edge_rows = execute( db, "SELECT fid, from_node_type, from_node_id, to_node_type, to_node_id, edge_type, subnetwork_id FROM Edge ORDER BY fid", diff --git a/core/src/read.jl b/core/src/read.jl index dec5df4b8..9a0177a9f 100644 --- a/core/src/read.jl +++ b/core/src/read.jl @@ -567,7 +567,7 @@ function DiscreteControl(db::DB, config::Config)::DiscreteControl return DiscreteControl( NodeID.(NodeType.DiscreteControl, condition.node_id), # Not unique - NodeID.(condition.listen_feature_type, condition.listen_feature_id), + NodeID.(condition.listen_node_type, condition.listen_node_id), condition.variable, look_ahead, condition.greater_than, @@ -930,19 +930,11 @@ function Parameters(db::DB, config::Config)::Parameters return p end -function get_nodetypes(db::DB)::Vector{String} - return only(execute(columntable, db, "SELECT node_type FROM Node ORDER BY fid")) -end - function get_ids(db::DB, nodetype)::Vector{Int} - sql = "SELECT node_id FROM Node WHERE node_type = $(esc_id(nodetype)) ORDER BY fid" + sql = "SELECT node_id FROM Node WHERE node_type = $(esc_id(nodetype)) ORDER BY node_id" return only(execute(columntable, db, sql)) end -function get_names(db::DB)::Vector{String} - return only(execute(columntable, db, "SELECT name FROM Node ORDER BY fid")) -end - function get_names(db::DB, nodetype)::Vector{String} sql = "SELECT name FROM Node where node_type = $(esc_id(nodetype)) ORDER BY fid" return only(execute(columntable, db, sql)) diff --git a/core/src/schema.jl b/core/src/schema.jl index c6a6a8e6f..27b6b8c72 100644 --- a/core/src/schema.jl +++ b/core/src/schema.jl @@ -183,8 +183,8 @@ end @version DiscreteControlConditionV1 begin node_id::Int - listen_feature_type::Union{Missing, String} - listen_feature_id::Int + listen_node_type::String + listen_node_id::Int variable::String greater_than::Float64 look_ahead::Union{Missing, Float64} @@ -199,7 +199,7 @@ end @version PidControlStaticV1 begin node_id::Int active::Union{Missing, Bool} - listen_node_type::Union{Missing, String} + listen_node_type::String listen_node_id::Int target::Float64 proportional::Float64 @@ -210,7 +210,7 @@ end @version PidControlTimeV1 begin node_id::Int - listen_node_type::Union{Missing, String} + listen_node_type::String listen_node_id::Int time::DateTime target::Float64 diff --git a/core/src/util.jl b/core/src/util.jl index fd0a93f0e..aae6abd98 100644 --- a/core/src/util.jl +++ b/core/src/util.jl @@ -613,7 +613,11 @@ function allocation_path_exists_in_graph( end function has_main_network(allocation::Allocation)::Bool - return first(allocation.allocation_network_ids) == 1 + if !is_active(allocation) + false + else + first(allocation.allocation_network_ids) == 1 + end end function is_main_network(allocation_network_id::Int)::Bool diff --git a/core/src/validation.jl b/core/src/validation.jl index d8372e668..82b03dbf8 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -91,7 +91,6 @@ function variable_nt(s::Any) end # functions used by sort(x; by) -sort_by_fid(row) = row.fid sort_by_id(row) = row.node_id sort_by_time_id(row) = (row.time, row.node_id) sort_by_id_level(row) = (row.node_id, row.level) diff --git a/core/test/run_models_test.jl b/core/test/run_models_test.jl index 8a1414275..dc293a3f2 100644 --- a/core/test/run_models_test.jl +++ b/core/test/run_models_test.jl @@ -96,9 +96,9 @@ @testset "Results values" begin @test flow.time[1] == DateTime(2020) - @test coalesce.(flow.edge_id[1:4], -1) == [-1, -1, 9, 11] - @test flow.from_node_id[1:4] == [6, typemax(Int), 0, 6] - @test flow.to_node_id[1:4] == [6, typemax(Int), typemax(Int), 0] + @test coalesce.(flow.edge_id[1:4], -1) == [-1, -1, 1, 2] + @test flow.from_node_id[1:4] == [6, 922, 6, 0] + @test flow.to_node_id[1:4] == [6, 922, 0, 922] @test basin.storage[1] ≈ 1.0 @test basin.level[1] ≈ 0.044711584 diff --git a/core/test/validation_test.jl b/core/test/validation_test.jl index 2020c27e9..d652b23c6 100644 --- a/core/test/validation_test.jl +++ b/core/test/validation_test.jl @@ -254,7 +254,7 @@ end @test logger.logs[3].kwargs[:control_state] == "" @test logger.logs[4].level == Error @test logger.logs[4].message == "Cannot connect a basin to a fractional_flow." - @test logger.logs[4].kwargs[:edge_id] == 6 + @test logger.logs[4].kwargs[:edge_id] == 7 @test logger.logs[4].kwargs[:id_src] == NodeID(:Basin, 2) @test logger.logs[4].kwargs[:id_dst] == NodeID(:FractionalFlow, 8) end @@ -362,10 +362,10 @@ end @test length(logger.logs) == 2 @test logger.logs[1].level == Error @test logger.logs[1].message == - "Invalid edge type 'foo' for edge #0 from node #1 to node #2." + "Invalid edge type 'foo' for edge #1 from node #1 to node #2." @test logger.logs[2].level == Error @test logger.logs[2].message == - "Invalid edge type 'bar' for edge #1 from node #2 to node #3." + "Invalid edge type 'bar' for edge #2 from node #2 to node #3." end @testitem "Subgrid validation" begin diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 01f4a9ea1..d698bbaf1 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -67,24 +67,23 @@ quartodoc: desc: The Model class represents an entire Ribasim model. contents: - Model - - title: Network - desc: The Node and Edge database layers define the network layout. + - title: Edge + desc: The Edge database layer. contents: - - Node - - Edge + - EdgeTable - title: Node types desc: Available node types to model different situations. contents: - - Basin - - FractionalFlow - - TabulatedRatingCurve - - Pump - - Outlet - - UserDemand - - LevelBoundary - - FlowBoundary - - LinearResistance - - ManningResistance - - Terminal - - DiscreteControl - - PidControl + - nodes.basin + - nodes.fractional_flow + - nodes.tabulated_rating_curve + - nodes.pump + - nodes.outlet + - nodes.user_demand + - nodes.level_boundary + - nodes.flow_boundary + - nodes.linear_resistance + - nodes.manning_resistance + - nodes.terminal + - nodes.discrete_control + - nodes.pid_control diff --git a/docs/core/usage.qmd b/docs/core/usage.qmd index 38423ad9b..bdf9e7f4d 100644 --- a/docs/core/usage.qmd +++ b/docs/core/usage.qmd @@ -603,14 +603,14 @@ DiscreteControl is implemented based on [VectorContinuousCallback](https://docs. The condition schema defines conditions of the form 'the discrete_control node with this node id listens to whether the given variable of the node with the given listen feature id is grater than the given value'. If the condition variable comes from a time-series, a look ahead $\Delta t$ can be supplied. -column | type | unit | restriction -------------------- | -------- | ------- | ----------- -node_id | Int | - | sorted -listen_feature_id | Int | - | sorted per node_id -listen_feature_type | String | - | known node type -variable | String | - | must be "level" or "flow_rate", sorted per listen_feature_id -greater_than | Float64 | various | sorted per variable -look_ahead | Float64 | $s$ | Only on transient boundary conditions, non-negative (optional, default 0) +column | type | unit | restriction +----------------- | -------- | ------- | ----------- +node_id | Int | - | sorted +listen_node_type | String | - | known node type +listen_node_id | Int | - | sorted per node_id +variable | String | - | must be "level" or "flow_rate", sorted per listen_node_id +greater_than | Float64 | various | sorted per variable +look_ahead | Float64 | $s$ | Only on transient boundary conditions, non-negative (optional, default 0) ## DiscreteControl / logic @@ -642,16 +642,17 @@ The PidControl node controls the level in a basin by continuously controlling th In the future controlling the flow on a particular edge could be supported. -column | type | unit | restriction --------------- | -------- | -------- | ----------- -node_id | Int | - | sorted -control_state | String | - | (optional) sorted per node_id -active | Bool | - | (optional, default true) -listen_node_id | Int | - | - -target | Float64 | $m$ | - -proportional | Float64 | $s^{-1}$ | - -integral | Float64 | $s^{-2}$ | - -derivative | Float64 | - | - +column | type | unit | restriction +---------------- | -------- | -------- | ----------- +node_id | Int | - | sorted +control_state | String | - | (optional) sorted per node_id +active | Bool | - | (optional, default true) +listen_node_type | Int | - | known node type +listen_node_id | Int | - | - +target | Float64 | $m$ | - +proportional | Float64 | $s^{-1}$ | - +integral | Float64 | $s^{-2}$ | - +derivative | Float64 | - | - ## PidControl / time @@ -663,15 +664,16 @@ these values interpolated linearly, and outside these values area constant given nearest time value. Note that a `node_id` can be either in this table or in the static one, but not both. -column | type | unit | restriction --------------- | -------- | -------- | ----------- -node_id | Int | - | sorted -time | DateTime | - | sorted per node_id -listen_node_id | Int | - | - -target | Float64 | $m$ | - -proportional | Float64 | $s^{-1}$ | - -integral | Float64 | $s^{-2}$ | - -derivative | Float64 | - | - +column | type | unit | restriction +---------------- | -------- | -------- | ----------- +node_id | Int | - | sorted +time | DateTime | - | sorted per node_id +listen_node_type | Int | - | known node type +listen_node_id | Int | - | - +target | Float64 | $m$ | - +proportional | Float64 | $s^{-1}$ | - +integral | Float64 | $s^{-2}$ | - +derivative | Float64 | - | - # Results diff --git a/docs/python/examples.ipynb b/docs/python/examples.ipynb index f2ab8ce82..13c1c7422 100644 --- a/docs/python/examples.ipynb +++ b/docs/python/examples.ipynb @@ -14,7 +14,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Basic model with static forcing" + "# Basic model with static forcing\n" ] }, { @@ -35,62 +35,31 @@ "metadata": {}, "outputs": [], "source": [ + "import shutil\n", "from pathlib import Path\n", "\n", - "import geopandas as gpd\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", - "import ribasim" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the basins:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "profile = 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\n", - "seconds_in_day = 24 * 3600\n", - "precipitation = 0.002 / seconds_in_day\n", - "evaporation = 0.001 / seconds_in_day\n", - "\n", - "static = pd.DataFrame(\n", - " data={\n", - " \"node_id\": [0],\n", - " \"potential_evaporation\": [evaporation],\n", - " \"precipitation\": [precipitation],\n", - " }\n", + "import ribasim\n", + "from ribasim.config import Node\n", + "from ribasim.model import Model\n", + "from 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", ")\n", - "static = static.iloc[[0, 0, 0, 0]]\n", - "static[\"node_id\"] = [1, 3, 6, 9]\n", - "\n", - "basin = ribasim.Basin(profile=profile, static=static)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup linear resistance:" + "from shapely.geometry import Point" ] }, { @@ -99,19 +68,8 @@ "metadata": {}, "outputs": [], "source": [ - "linear_resistance = ribasim.LinearResistance(\n", - " static=pd.DataFrame(\n", - " data={\"node_id\": [10, 12], \"resistance\": [5e3, (3600.0 * 24) / 100.0]}\n", - " )\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup Manning resistance:" + "datadir = Path(\"data\")\n", + "shutil.rmtree(datadir, ignore_errors=True)" ] }, { @@ -120,17 +78,7 @@ "metadata": {}, "outputs": [], "source": [ - "manning_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", - ")" + "model = Model(starttime=\"2020-01-01 00:00:00\", endtime=\"2021-01-01 00:00:00\")" ] }, { @@ -138,7 +86,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Set up a rating curve node:" + "Setup the basins:\n" ] }, { @@ -147,18 +95,37 @@ "metadata": {}, "outputs": [], "source": [ - "# Discharge: lose 1% of storage volume per day at storage = 1000.0.\n", - "q1000 = 1000.0 * 0.01 / seconds_in_day\n", + "time = pd.date_range(model.starttime, model.endtime)\n", + "day_of_year = time.day_of_year.to_numpy()\n", + "seconds_per_day = 24 * 60 * 60\n", + "evaporation = (\n", + " (-1.0 * np.cos(day_of_year / 365.0 * 2 * np.pi) + 1.0) * 0.0025 / seconds_per_day\n", + ")\n", + "rng = np.random.default_rng(seed=0)\n", + "precipitation = (\n", + " rng.lognormal(mean=-1.0, sigma=1.7, size=time.size) * 0.001 / seconds_per_day\n", + ")\n", "\n", - "rating_curve = ribasim.TabulatedRatingCurve(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [4, 4],\n", - " \"level\": [0.0, 1.0],\n", - " \"flow_rate\": [0.0, q1000],\n", - " }\n", - " )\n", - ")" + "# Convert steady forcing to m/s\n", + "# 2 mm/d precipitation, 1 mm/d evaporation\n", + "\n", + "basin_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", + "\n", + "model.basin.add(Node(1, Point(0.0, 0.0)), basin_data)\n", + "model.basin.add(Node(3, Point(2.0, 0.0)), basin_data)\n", + "model.basin.add(Node(6, Point(3.0, 2.0)), basin_data)\n", + "model.basin.add(Node(9, Point(5.0, 0.0)), basin_data)" ] }, { @@ -166,7 +133,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup fractional flows:" + "Setup linear resistance:\n" ] }, { @@ -175,13 +142,13 @@ "metadata": {}, "outputs": [], "source": [ - "fractional_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", + "model.linear_resistance.add(\n", + " Node(10, Point(6.0, 0.0)),\n", + " [linear_resistance.Static(resistance=[5e3])],\n", + ")\n", + "model.linear_resistance.add(\n", + " Node(12, Point(2.0, 1.0)),\n", + " [linear_resistance.Static(resistance=[3600.0 * 24.0 / 100.0])],\n", ")" ] }, @@ -190,7 +157,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup pump:" + "Setup Manning resistance:\n" ] }, { @@ -199,13 +166,13 @@ "metadata": {}, "outputs": [], "source": [ - "pump = ribasim.Pump(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [7],\n", - " \"flow_rate\": [0.5 / 3600],\n", - " }\n", - " )\n", + "model.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", ")" ] }, @@ -214,7 +181,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup level boundary:" + "Set up a rating curve node:\n" ] }, { @@ -223,13 +190,9 @@ "metadata": {}, "outputs": [], "source": [ - "level_boundary = ribasim.LevelBoundary(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [11, 17],\n", - " \"level\": [0.5, 1.5],\n", - " }\n", - " )\n", + "model.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", ")" ] }, @@ -238,7 +201,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup flow boundary:" + "Setup fractional flows:\n" ] }, { @@ -247,13 +210,15 @@ "metadata": {}, "outputs": [], "source": [ - "flow_boundary = ribasim.FlowBoundary(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [15, 16],\n", - " \"flow_rate\": [1e-4, 1e-4],\n", - " }\n", - " )\n", + "model.fractional_flow.add(\n", + " Node(5, Point(3.0, 1.0)), [fractional_flow.Static(fraction=[0.3])]\n", + ")\n", + "model.fractional_flow.add(\n", + " Node(8, Point(4.0, 0.0)), [fractional_flow.Static(fraction=[0.6])]\n", + ")\n", + "model.fractional_flow.add(\n", + " Node(13, Point(3.0, -1.0)),\n", + " [fractional_flow.Static(fraction=[0.1])],\n", ")" ] }, @@ -262,29 +227,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup terminal:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "terminal = ribasim.Terminal(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [14],\n", - " }\n", - " )\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Set up the nodes:" + "Setup pump:\n" ] }, { @@ -293,57 +236,15 @@ "metadata": {}, "outputs": [], "source": [ - "xy = 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", - ")\n", - "node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n", - "\n", - "node_id, node_type = ribasim.Node.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.\n", - "node = ribasim.Node(\n", - " df=gpd.GeoDataFrame(\n", - " data={\"node_type\": node_type},\n", - " index=pd.Index(node_id, name=\"fid\"),\n", - " geometry=node_xy,\n", - " crs=\"EPSG:28992\",\n", - " )\n", - ")" + "model.pump.add(Node(7, Point(4.0, 1.0)), [pump.Static(flow_rate=[0.5 / 3600])])" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the edges:" + "Setup level boundary:\n" ] }, { @@ -352,23 +253,11 @@ "metadata": {}, "outputs": [], "source": [ - "from_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", - ")\n", - "to_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", + "model.level_boundary.add(\n", + " Node(11, Point(2.0, 2.0)), [level_boundary.Static(level=[0.5])]\n", ")\n", - "lines = node.geometry_from_connectivity(from_id, to_id)\n", - "edge = ribasim.Edge(\n", - " df=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", + "model.level_boundary.add(\n", + " Node(17, Point(6.0, 1.0)), [level_boundary.Static(level=[1.5])]\n", ")" ] }, @@ -377,7 +266,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup a model:" + "Setup flow boundary:\n" ] }, { @@ -386,22 +275,11 @@ "metadata": {}, "outputs": [], "source": [ - "model = ribasim.Model(\n", - " network=ribasim.Network(\n", - " node=node,\n", - " edge=edge,\n", - " ),\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", + "model.flow_boundary.add(\n", + " Node(15, Point(3.0, 3.0)), [flow_boundary.Static(flow_rate=[1e-4])]\n", + ")\n", + "model.flow_boundary.add(\n", + " Node(16, Point(0.0, 1.0)), [flow_boundary.Static(flow_rate=[1e-4])]\n", ")" ] }, @@ -410,7 +288,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's take a look at the model:" + "Setup terminal:\n" ] }, { @@ -419,15 +297,14 @@ "metadata": {}, "outputs": [], "source": [ - "model.plot()" + "model.terminal.add(Node(14, Point(3.0, -2.0)))" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Write the model to a TOML and GeoPackage:" + "Setup the edges:\n" ] }, { @@ -436,8 +313,23 @@ "metadata": {}, "outputs": [], "source": [ - "datadir = Path(\"data\")\n", - "model.write(datadir / \"basic/ribasim.toml\")" + "model.edge.add(model.basin[1], model.manning_resistance[2], \"flow\")\n", + "model.edge.add(model.manning_resistance[2], model.basin[3], \"flow\")\n", + "model.edge.add(model.basin[3], model.tabulated_rating_curve[4], \"flow\")\n", + "model.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[5], \"flow\")\n", + "model.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[8], \"flow\")\n", + "model.edge.add(model.fractional_flow[5], model.basin[6], \"flow\")\n", + "model.edge.add(model.basin[6], model.pump[7], \"flow\")\n", + "model.edge.add(model.fractional_flow[8], model.basin[9], \"flow\")\n", + "model.edge.add(model.pump[7], model.basin[9], \"flow\")\n", + "model.edge.add(model.basin[9], model.linear_resistance[10], \"flow\")\n", + "model.edge.add(model.level_boundary[11], model.linear_resistance[12], \"flow\")\n", + "model.edge.add(model.linear_resistance[12], model.basin[3], \"flow\")\n", + "model.edge.add(model.tabulated_rating_curve[4], model.fractional_flow[13], \"flow\")\n", + "model.edge.add(model.fractional_flow[13], model.terminal[14], \"flow\")\n", + "model.edge.add(model.flow_boundary[15], model.basin[6], \"flow\")\n", + "model.edge.add(model.flow_boundary[16], model.basin[1], \"flow\")\n", + "model.edge.add(model.linear_resistance[10], model.level_boundary[17], \"flow\")" ] }, { @@ -445,30 +337,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Update the basic model with transient forcing\n", - "\n", - "This assumes you have already created the basic model with static forcing." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "import ribasim\n", - "import xarray as xr" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model = ribasim.Model(filepath=datadir / \"basic/ribasim.toml\")" + "Let's take a look at the model:\n" ] }, { @@ -477,16 +346,7 @@ "metadata": {}, "outputs": [], "source": [ - "time = pd.date_range(model.starttime, model.endtime)\n", - "day_of_year = time.day_of_year.to_numpy()\n", - "seconds_per_day = 24 * 60 * 60\n", - "evaporation = (\n", - " (-1.0 * np.cos(day_of_year / 365.0 * 2 * np.pi) + 1.0) * 0.0025 / seconds_per_day\n", - ")\n", - "rng = np.random.default_rng(seed=0)\n", - "precipitation = (\n", - " rng.lognormal(mean=-1.0, sigma=1.7, size=time.size) * 0.001 / seconds_per_day\n", - ")" + "model.plot()" ] }, { @@ -494,60 +354,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We'll use xarray to easily broadcast the values." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "timeseries = (\n", - " pd.DataFrame(\n", - " data={\n", - " \"node_id\": 1,\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", - " )\n", - " .set_index(\"time\")\n", - " .to_xarray()\n", - ")\n", - "\n", - "basin_ids = model.basin.static.df[\"node_id\"].to_numpy()\n", - "basin_nodes = xr.DataArray(\n", - " np.ones(len(basin_ids)), coords={\"node_id\": basin_ids}, dims=[\"node_id\"]\n", - ")\n", - "forcing = (timeseries * basin_nodes).to_dataframe().reset_index()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "state = pd.DataFrame(\n", - " data={\n", - " \"node_id\": basin_ids,\n", - " \"level\": 1.4,\n", - " }\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.basin.time.df = forcing\n", - "model.basin.state.df = state" + "Write the model to a TOML and GeoPackage:\n" ] }, { @@ -556,7 +363,7 @@ "metadata": {}, "outputs": [], "source": [ - "model.write(datadir / \"basic_transient/ribasim.toml\")" + "model.write(datadir / \"basic/ribasim.toml\")" ] }, { @@ -573,7 +380,7 @@ " \"julia\",\n", " \"--project=../../core\",\n", " \"--eval\",\n", - " f'using Ribasim; Ribasim.main(\"{datadir.as_posix()}/basic_transient/ribasim.toml\")',\n", + " f'using Ribasim; Ribasim.main(\"{datadir.as_posix()}/basic/ribasim.toml\")',\n", " ],\n", " check=True,\n", ")" @@ -584,8 +391,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now run the model with `ribasim basic_transient/ribasim.toml`.\n", - "After running the model, read back the results:" + "Now run the model with `ribasim basic/ribasim.toml`.\n", + "After running the model, read back the results:\n" ] }, { @@ -594,7 +401,7 @@ "metadata": {}, "outputs": [], "source": [ - "df_basin = pd.read_feather(datadir / \"basic_transient/results/basin.arrow\")\n", + "df_basin = pd.read_feather(datadir / \"basic/results/basin.arrow\")\n", "df_basin_wide = df_basin.pivot_table(\n", " index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n", ")\n", @@ -607,22 +414,13 @@ "metadata": {}, "outputs": [], "source": [ - "df_flow = pd.read_feather(datadir / \"basic_transient/results/flow.arrow\")\n", + "df_flow = pd.read_feather(datadir / \"basic/results/flow.arrow\")\n", "df_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\n", "df_flow[\"flow_m3d\"] = df_flow.flow_rate * 86400\n", "ax = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\").plot()\n", "ax.legend(bbox_to_anchor=(1.3, 1), title=\"Edge\")" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "type(df_flow)" - ] - }, { "attachments": {}, "cell_type": "markdown", @@ -630,7 +428,7 @@ "source": [ "# Model with discrete control\n", "\n", - "The 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." + "The 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.\n" ] }, { @@ -638,7 +436,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Set up the nodes:" + "Setup the basins:\n" ] }, { @@ -647,47 +445,7 @@ "metadata": {}, "outputs": [], "source": [ - "xy = np.array(\n", - " [\n", - " (0.0, 0.0), # 1: Basin\n", - " (1.0, 1.0), # 2: Pump\n", - " (1.0, -1.0), # 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", - "\n", - "node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n", - "\n", - "node_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.\n", - "node = ribasim.Node(\n", - " df=gpd.GeoDataFrame(\n", - " data={\"node_type\": node_type},\n", - " index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n", - " geometry=node_xy,\n", - " crs=\"EPSG:28992\",\n", - " )\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the edges:" + "model = Model(starttime=\"2020-01-01 00:00:00\", endtime=\"2021-01-01 00:00:00\")" ] }, { @@ -696,18 +454,12 @@ "metadata": {}, "outputs": [], "source": [ - "from_id = np.array([1, 3, 4, 2, 1, 5, 7, 7], dtype=np.int64)\n", - "to_id = np.array([3, 4, 2, 1, 5, 6, 2, 3], dtype=np.int64)\n", - "\n", - "edge_type = 6 * [\"flow\"] + 2 * [\"control\"]\n", - "\n", - "lines = node.geometry_from_connectivity(from_id, to_id)\n", - "edge = ribasim.Edge(\n", - " df=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", + "model.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", + " ],\n", ")" ] }, @@ -716,34 +468,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the basins:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "profile = pd.DataFrame(\n", - " data={\n", - " \"node_id\": [1, 1],\n", - " \"area\": [1000.0, 1000.0],\n", - " \"level\": [0.0, 1.0],\n", - " }\n", - ")\n", - "\n", - "state = pd.DataFrame(data={\"node_id\": [1], \"level\": [20.0]})\n", - "\n", - "basin = ribasim.Basin(profile=profile, state=state)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the discrete control:" + "Setup the discrete control:\n" ] }, { @@ -752,24 +477,21 @@ "metadata": {}, "outputs": [], "source": [ - "condition = 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", - "\n", - "logic = 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", - "\n", - "discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic)" + "model.discrete_control.add(\n", + " Node(7, Point(1.0, 0.0)),\n", + " [\n", + " discrete_control.Condition(\n", + " listen_node_id=[1, 1, 1],\n", + " listen_node_type=[\"Basin\", \"Basin\", \"Basin\"],\n", + " variable=[\"level\", \"level\", \"level\"],\n", + " greater_than=[5.0, 10.0, 15.0],\n", + " ),\n", + " discrete_control.Logic(\n", + " truth_state=[\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n", + " control_state=[\"in\", \"in\", \"none\", \"out\", \"out\"],\n", + " ),\n", + " ],\n", + ")" ] }, { @@ -777,9 +499,10 @@ "metadata": {}, "source": [ "The above control logic can be summarized as follows:\n", + "\n", "- If the level gets above the maximum, activate the control state \"out\" until the setpoint is reached;\n", "- If the level gets below the minimum, active the control state \"in\" until the setpoint is reached;\n", - "- Otherwise activate the control state \"none\"." + "- Otherwise activate the control state \"none\".\n" ] }, { @@ -787,7 +510,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the pump:" + "Setup the pump:\n" ] }, { @@ -796,14 +519,13 @@ "metadata": {}, "outputs": [], "source": [ - "pump = 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", + "model.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", + ")\n", + "model.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", ")" ] }, @@ -813,18 +535,18 @@ "source": [ "The pump data defines the following:\n", "\n", - "Control state | Pump #2 flow rate (m/s)| Pump #3 flow rate (m/s)\n", - "--------------|------------------------|------------------\n", - "\"none\" | 0.0 | 0.0\n", - "\"in\" | 2e-3 | 0.0\n", - "\"out\" | 0.0 | 2e-3" + "| Control state | Pump #2 flow rate (m/s) | Pump #3 flow rate (m/s) |\n", + "| ------------- | ----------------------- | ----------------------- |\n", + "| \"none\" | 0.0 | 0.0 |\n", + "| \"in\" | 2e-3 | 0.0 |\n", + "| \"out\" | 0.0 | 2e-3 |\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the level boundary:" + "Setup the level boundary:\n" ] }, { @@ -833,8 +555,8 @@ "metadata": {}, "outputs": [], "source": [ - "level_boundary = ribasim.LevelBoundary(\n", - " static=pd.DataFrame(data={\"node_id\": [4], \"level\": [10.0]})\n", + "model.level_boundary.add(\n", + " Node(4, Point(2.0, 0.0)), [level_boundary.Static(level=[10.0])]\n", ")" ] }, @@ -842,7 +564,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the rating curve:" + "Setup the rating curve:\n" ] }, { @@ -851,10 +573,9 @@ "metadata": {}, "outputs": [], "source": [ - "rating_curve = ribasim.TabulatedRatingCurve(\n", - " static=pd.DataFrame(\n", - " data={\"node_id\": 2 * [5], \"level\": [2.0, 15.0], \"flow_rate\": [0.0, 1e-3]}\n", - " )\n", + "model.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, 1e-3])],\n", ")" ] }, @@ -863,7 +584,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the terminal:" + "Setup the terminal:\n" ] }, { @@ -872,7 +593,7 @@ "metadata": {}, "outputs": [], "source": [ - "terminal = ribasim.Terminal(static=pd.DataFrame(data={\"node_id\": [6]}))" + "model.terminal.add(Node(6, Point(-2.0, 0.0)))" ] }, { @@ -880,7 +601,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup a model:" + "Setup edges:\n" ] }, { @@ -889,20 +610,14 @@ "metadata": {}, "outputs": [], "source": [ - "model = ribasim.Model(\n", - " network=ribasim.Network(\n", - " node=node,\n", - " edge=edge,\n", - " ),\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", - ")" + "model.edge.add(model.basin[1], model.pump[3], \"flow\")\n", + "model.edge.add(model.pump[3], model.level_boundary[4], \"flow\")\n", + "model.edge.add(model.level_boundary[4], model.pump[2], \"flow\")\n", + "model.edge.add(model.pump[2], model.basin[1], \"flow\")\n", + "model.edge.add(model.basin[1], model.tabulated_rating_curve[5], \"flow\")\n", + "model.edge.add(model.tabulated_rating_curve[5], model.terminal[6], \"flow\")\n", + "model.edge.add(model.discrete_control[7], model.pump[2], \"control\")\n", + "model.edge.add(model.discrete_control[7], model.pump[3], \"control\")" ] }, { @@ -910,7 +625,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let’s take a look at the model:" + "Let’s take a look at the model:\n" ] }, { @@ -927,7 +642,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Listen edges are plotted with a dashed line since they are not present in the \"Edge / static\" schema but only in the \"Control / condition\" schema." + "Listen 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" ] }, { @@ -965,7 +680,7 @@ "metadata": {}, "source": [ "Now run the model with `level_setpoint_with_minmax/ribasim.toml`.\n", - "After running the model, read back the results:" + "After running the model, read back the results:\n" ] }, { @@ -1017,47 +732,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The highlighted regions show where a pump is active." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's print an overview of what happened with control:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model.print_discrete_control_record(\n", - " datadir / \"level_setpoint_with_minmax/results/control.arrow\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note 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." + "The highlighted regions show where a pump is active.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Model with PID control" + "# Model with PID control\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Set up the nodes:" + "Set up the model:\n" ] }, { @@ -1066,38 +755,9 @@ "metadata": {}, "outputs": [], "source": [ - "xy = 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", - "\n", - "node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n", - "\n", - "node_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.\n", - "node = ribasim.Node(\n", - " df=gpd.GeoDataFrame(\n", - " data={\"node_type\": node_type},\n", - " index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n", - " geometry=node_xy,\n", - " crs=\"EPSG:28992\",\n", - " )\n", + "model = Model(\n", + " starttime=\"2020-01-01 00:00:00\",\n", + " endtime=\"2020-12-01 00:00:00\",\n", ")" ] }, @@ -1105,7 +765,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the edges:" + "Setup the basins:\n" ] }, { @@ -1114,20 +774,9 @@ "metadata": {}, "outputs": [], "source": [ - "from_id = np.array([1, 2, 3, 4, 6, 5, 7], dtype=np.int64)\n", - "to_id = np.array([2, 3, 4, 6, 2, 3, 6], dtype=np.int64)\n", - "\n", - "lines = node.geometry_from_connectivity(from_id, to_id)\n", - "edge = ribasim.Edge(\n", - " df=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", + "model.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", ")" ] }, @@ -1135,34 +784,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the basins:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "profile = pd.DataFrame(\n", - " data={\"node_id\": [2, 2], \"level\": [0.0, 1.0], \"area\": [1000.0, 1000.0]}\n", - ")\n", - "\n", - "state = pd.DataFrame(\n", - " data={\n", - " \"node_id\": [2],\n", - " \"level\": [6.0],\n", - " }\n", - ")\n", - "\n", - "basin = ribasim.Basin(profile=profile, state=state)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the pump:" + "Setup the pump:\n" ] }, { @@ -1171,13 +793,9 @@ "metadata": {}, "outputs": [], "source": [ - "pump = 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", + "model.pump.add(\n", + " Node(3, Point(2.0, 0.5)),\n", + " [pump.Static(flow_rate=[0.0])], # Will be overwritten by PID controller\n", ")" ] }, @@ -1185,7 +803,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the outlet:" + "Setup the outlet:\n" ] }, { @@ -1194,13 +812,9 @@ "metadata": {}, "outputs": [], "source": [ - "outlet = 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", + "model.outlet.add(\n", + " Node(6, Point(2.0, -0.5)),\n", + " [outlet.Static(flow_rate=[0.0])], # Will be overwritten by PID controller\n", ")" ] }, @@ -1208,7 +822,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup flow boundary:" + "Setup flow boundary:\n" ] }, { @@ -1217,8 +831,9 @@ "metadata": {}, "outputs": [], "source": [ - "flow_boundary = ribasim.FlowBoundary(\n", - " static=pd.DataFrame(data={\"node_id\": [1], \"flow_rate\": [1e-3]})\n", + "model.flow_boundary.add(\n", + " Node(1, Point(0.0, 0.0)),\n", + " [flow_boundary.Static(flow_rate=[1e-3])],\n", ")" ] }, @@ -1226,7 +841,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup flow boundary:" + "Setup flow boundary:\n" ] }, { @@ -1235,13 +850,9 @@ "metadata": {}, "outputs": [], "source": [ - "level_boundary = ribasim.LevelBoundary(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [4],\n", - " \"level\": [1.0], # Not relevant\n", - " }\n", - " )\n", + "model.level_boundary.add(\n", + " Node(4, Point(3.0, 0.0)),\n", + " [level_boundary.Static(level=[1])],\n", ")" ] }, @@ -1249,7 +860,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup PID control:" + "Setup PID control:\n" ] }, { @@ -1258,27 +869,43 @@ "metadata": {}, "outputs": [], "source": [ - "pid_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", + "model.pid_control.add(\n", + " Node(5, Point(1.5, 1.0)),\n", + " [\n", + " pid_control.Time(\n", + " time=[\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", + " ],\n", + " listen_node_id=[2, 2, 2, 2],\n", + " listen_node_type=[\"Basin\", \"Basin\", \"Basin\", \"Basin\"],\n", + " target=[5.0, 5.0, 7.5, 7.5],\n", + " proportional=[-1e-3, 1e-3, 1e-3, 1e-3],\n", + " integral=[-1e-7, 1e-7, -1e-7, 1e-7],\n", + " derivative=[0.0, 0.0, 0.0, 0.0],\n", + " )\n", + " ],\n", + ")\n", + "model.pid_control.add(\n", + " Node(7, Point(1.5, -1.0)),\n", + " [\n", + " pid_control.Time(\n", + " time=[\n", + " \"2020-01-01 00:00:00\",\n", + " \"2020-05-01 00:00:00\",\n", + " \"2020-07-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", + " listen_node_id=[2, 2, 2, 2],\n", + " listen_node_type=[\"Basin\", \"Basin\", \"Basin\", \"Basin\"],\n", + " target=[5.0, 5.0, 7.5, 7.5],\n", + " proportional=[-1e-3, 1e-3, 1e-3, 1e-3],\n", + " integral=[-1e-7, 1e-7, -1e-7, 1e-7],\n", + " derivative=[0.0, 0.0, 0.0, 0.0],\n", + " )\n", + " ],\n", ")" ] }, @@ -1286,14 +913,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note 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." + "Note 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.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup a model:" + "Setup the edges:\n" ] }, { @@ -1302,27 +929,20 @@ "metadata": {}, "outputs": [], "source": [ - "model = ribasim.Model(\n", - " network=ribasim.Network(\n", - " node=node,\n", - " edge=edge,\n", - " ),\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", - ")" + "model.edge.add(model.flow_boundary[1], model.basin[2], \"flow\")\n", + "model.edge.add(model.basin[2], model.pump[3], \"flow\")\n", + "model.edge.add(model.pump[3], model.level_boundary[4], \"flow\")\n", + "model.edge.add(model.level_boundary[4], model.outlet[6], \"flow\")\n", + "model.edge.add(model.outlet[6], model.basin[2], \"flow\")\n", + "model.edge.add(model.pid_control[5], model.pump[3], \"control\")\n", + "model.edge.add(model.pid_control[7], model.outlet[6], \"control\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's take a look at the model:" + "Let's take a look at the model:\n" ] }, { @@ -1338,7 +958,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Write the model to a TOML and GeoPackage:" + "Write the model to a TOML and GeoPackage:\n" ] }, { @@ -1376,7 +996,7 @@ "metadata": {}, "source": [ "Now run the model with `ribasim pid_control/ribasim.toml`.\n", - "After running the model, read back the results:" + "After running the model, read back the results:\n" ] }, { @@ -1405,14 +1025,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Model with allocation (user demand)" + "# Model with allocation (user demand)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the nodes:" + "Setup a model:\n" ] }, { @@ -1421,49 +1041,9 @@ "metadata": {}, "outputs": [], "source": [ - "xy = np.array(\n", - " [\n", - " (0.0, 0.0), # 1: FlowBoundary\n", - " (1.0, 0.0), # 2: Basin\n", - " (1.0, 1.0), # 3: UserDemand\n", - " (2.0, 0.0), # 4: LinearResistance\n", - " (3.0, 0.0), # 5: Basin\n", - " (3.0, 1.0), # 6: UserDemand\n", - " (4.0, 0.0), # 7: TabulatedRatingCurve\n", - " (4.5, 0.0), # 8: FractionalFlow\n", - " (4.5, 0.5), # 9: FractionalFlow\n", - " (5.0, 0.0), # 10: Terminal\n", - " (4.5, 0.25), # 11: DiscreteControl\n", - " (4.5, 1.0), # 12: Basin\n", - " (5.0, 1.0), # 13: UserDemand\n", - " ]\n", - ")\n", - "node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n", - "\n", - "node_type = [\n", - " \"FlowBoundary\",\n", - " \"Basin\",\n", - " \"UserDemand\",\n", - " \"LinearResistance\",\n", - " \"Basin\",\n", - " \"UserDemand\",\n", - " \"TabulatedRatingCurve\",\n", - " \"FractionalFlow\",\n", - " \"FractionalFlow\",\n", - " \"Terminal\",\n", - " \"DiscreteControl\",\n", - " \"Basin\",\n", - " \"UserDemand\",\n", - "]\n", - "\n", - "# All nodes belong to allocation network id 1\n", - "node = ribasim.Node(\n", - " df=gpd.GeoDataFrame(\n", - " data={\"node_type\": node_type, \"subnetwork_id\": 1},\n", - " index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n", - " geometry=node_xy,\n", - " crs=\"EPSG:28992\",\n", - " )\n", + "model = Model(\n", + " starttime=\"2020-01-01 00:00:00\",\n", + " endtime=\"2020-01-20 00:00:00\",\n", ")" ] }, @@ -1471,7 +1051,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the edges:" + "Setup the basins:\n" ] }, { @@ -1480,30 +1060,22 @@ "metadata": {}, "outputs": [], "source": [ - "from_id = np.array(\n", - " [1, 2, 2, 4, 5, 5, 7, 3, 6, 7, 8, 9, 12, 13, 11, 11],\n", - " dtype=np.int64,\n", + "basin_data = [\n", + " basin.Profile(area=[300_000.0, 300_000.0], level=[0.0, 1.0]),\n", + " basin.State(level=[1.0]),\n", + "]\n", + "\n", + "model.basin.add(\n", + " Node(2, Point(1.0, 0.0), subnetwork_id=1),\n", + " basin_data,\n", ")\n", - "to_id = np.array(\n", - " [2, 3, 4, 5, 6, 7, 8, 2, 5, 9, 10, 12, 13, 10, 8, 9],\n", - " dtype=np.int64,\n", + "model.basin.add(\n", + " Node(5, Point(3.0, 0.0), subnetwork_id=1),\n", + " basin_data,\n", ")\n", - "# Denote the first edge, 1 => 2, as a source edge for\n", - "# allocation network 1\n", - "subnetwork_id = len(from_id) * [None]\n", - "subnetwork_id[0] = 1\n", - "lines = node.geometry_from_connectivity(from_id, to_id)\n", - "edge = ribasim.Edge(\n", - " df=gpd.GeoDataFrame(\n", - " data={\n", - " \"from_node_id\": from_id,\n", - " \"to_node_id\": to_id,\n", - " \"edge_type\": (len(from_id) - 2) * [\"flow\"] + 2 * [\"control\"],\n", - " \"subnetwork_id\": subnetwork_id,\n", - " },\n", - " geometry=lines,\n", - " crs=\"EPSG:28992\",\n", - " )\n", + "model.basin.add(\n", + " Node(12, Point(4.5, 1.0), subnetwork_id=1),\n", + " basin_data,\n", ")" ] }, @@ -1511,7 +1083,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the basins:" + "Setup the flow boundary:\n" ] }, { @@ -1520,24 +1092,16 @@ "metadata": {}, "outputs": [], "source": [ - "profile = pd.DataFrame(\n", - " data={\n", - " \"node_id\": [2, 2, 5, 5, 12, 12],\n", - " \"area\": 300_000.0,\n", - " \"level\": 3 * [0.0, 1.0],\n", - " }\n", - ")\n", - "\n", - "state = pd.DataFrame(data={\"node_id\": [2, 5, 12], \"level\": 1.0})\n", - "\n", - "basin = ribasim.Basin(profile=profile, state=state)" + "model.flow_boundary.add(\n", + " Node(1, Point(0.0, 0.0), subnetwork_id=1), [flow_boundary.Static(flow_rate=[2.0])]\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the flow boundary:" + "Setup the linear resistance:\n" ] }, { @@ -1546,13 +1110,9 @@ "metadata": {}, "outputs": [], "source": [ - "flow_boundary = ribasim.FlowBoundary(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [1],\n", - " \"flow_rate\": 2.0,\n", - " }\n", - " )\n", + "model.linear_resistance.add(\n", + " Node(4, Point(2.0, 0.0), subnetwork_id=1),\n", + " [linear_resistance.Static(resistance=[0.06])],\n", ")" ] }, @@ -1560,7 +1120,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the linear resistance:" + "Setup the tabulated rating curve:\n" ] }, { @@ -1569,13 +1129,9 @@ "metadata": {}, "outputs": [], "source": [ - "linear_resistance = ribasim.LinearResistance(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [4],\n", - " \"resistance\": 0.06,\n", - " }\n", - " )\n", + "model.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", ")" ] }, @@ -1583,7 +1139,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the tabulated rating curve:" + "Setup the fractional flow:\n" ] }, { @@ -1592,14 +1148,13 @@ "metadata": {}, "outputs": [], "source": [ - "tabulated_rating_curve = ribasim.TabulatedRatingCurve(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": 7,\n", - " \"level\": [0.0, 0.5, 1.0],\n", - " \"flow_rate\": [0.0, 0.0, 2.0],\n", - " }\n", - " )\n", + "model.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", + ")\n", + "model.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", ")" ] }, @@ -1607,7 +1162,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the fractional flow:" + "Setup the terminal:\n" ] }, { @@ -1616,22 +1171,14 @@ "metadata": {}, "outputs": [], "source": [ - "fractional_flow = ribasim.FractionalFlow(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [8, 8, 9, 9],\n", - " \"fraction\": [0.6, 0.9, 0.4, 0.1],\n", - " \"control_state\": [\"divert\", \"close\", \"divert\", \"close\"],\n", - " }\n", - " )\n", - ")" + "model.terminal.add(Node(10, Point(5.0, 0.0), subnetwork_id=1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the terminal:" + "Setup the discrete control:\n" ] }, { @@ -1640,12 +1187,19 @@ "metadata": {}, "outputs": [], "source": [ - "terminal = ribasim.Terminal(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [10],\n", - " }\n", - " )\n", + "model.discrete_control.add(\n", + " Node(11, Point(4.5, 0.25), subnetwork_id=1),\n", + " [\n", + " discrete_control.Condition(\n", + " listen_node_id=[5],\n", + " listen_node_type=[\"Basin\"],\n", + " variable=[\"level\"],\n", + " greater_than=[0.52],\n", + " ),\n", + " discrete_control.Logic(\n", + " truth_state=[\"T\", \"F\"], control_state=[\"divert\", \"close\"]\n", + " ),\n", + " ],\n", ")" ] }, @@ -1653,7 +1207,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the discrete control:" + "Setup the users:\n" ] }, { @@ -1662,59 +1216,33 @@ "metadata": {}, "outputs": [], "source": [ - "condition = pd.DataFrame(\n", - " data={\n", - " \"node_id\": [11],\n", - " \"listen_feature_id\": 5,\n", - " \"variable\": \"level\",\n", - " \"greater_than\": 0.52,\n", - " }\n", + "model.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", ")\n", - "\n", - "logic = pd.DataFrame(\n", - " data={\n", - " \"node_id\": 11,\n", - " \"truth_state\": [\"T\", \"F\"],\n", - " \"control_state\": [\"divert\", \"close\"],\n", - " }\n", + "model.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", ")\n", - "\n", - "discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the users:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "user_demand = ribasim.UserDemand(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [6, 13],\n", - " \"demand\": [1.5, 1.0],\n", - " \"return_factor\": 0.0,\n", - " \"min_level\": -1.0,\n", - " \"priority\": [1, 3],\n", - " }\n", - " ),\n", - " time=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [3, 3, 3, 3],\n", - " \"demand\": [0.0, 1.0, 1.2, 1.2],\n", - " \"priority\": [1, 1, 2, 2],\n", - " \"return_factor\": 0.0,\n", - " \"min_level\": -1.0,\n", - " \"time\": 2 * [\"2020-01-01 00:00:00\", \"2020-01-20 00:00:00\"],\n", - " }\n", - " ),\n", + "model.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 00:00:00\", \"2020-01-20 00:00:00\"],\n", + " )\n", + " ],\n", ")" ] }, @@ -1722,7 +1250,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the allocation:" + "Setup the allocation:\n" ] }, { @@ -1731,14 +1259,14 @@ "metadata": {}, "outputs": [], "source": [ - "allocation = ribasim.Allocation(use_allocation=True, timestep=86400)" + "model.allocation = ribasim.Allocation(use_allocation=True, timestep=86400)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup a model:" + "Setup the edges:\n" ] }, { @@ -1747,30 +1275,29 @@ "metadata": {}, "outputs": [], "source": [ - "model = ribasim.Model(\n", - " network=ribasim.Network(\n", - " node=node,\n", - " edge=edge,\n", - " ),\n", - " basin=basin,\n", - " flow_boundary=flow_boundary,\n", - " linear_resistance=linear_resistance,\n", - " tabulated_rating_curve=tabulated_rating_curve,\n", - " terminal=terminal,\n", - " user_demand=user_demand,\n", - " discrete_control=discrete_control,\n", - " fractional_flow=fractional_flow,\n", - " allocation=allocation,\n", - " starttime=\"2020-01-01 00:00:00\",\n", - " endtime=\"2020-01-20 00:00:00\",\n", - ")" + "model.edge.add(model.flow_boundary[1], model.basin[2], \"flow\", subnetwork_id=1)\n", + "model.edge.add(model.basin[2], model.user_demand[3], \"flow\")\n", + "model.edge.add(model.basin[2], model.linear_resistance[4], \"flow\")\n", + "model.edge.add(model.linear_resistance[4], model.basin[5], \"flow\")\n", + "model.edge.add(model.basin[5], model.user_demand[6], \"flow\")\n", + "model.edge.add(model.basin[5], model.tabulated_rating_curve[7], \"flow\")\n", + "model.edge.add(model.tabulated_rating_curve[7], model.fractional_flow[8], \"flow\")\n", + "model.edge.add(model.user_demand[3], model.basin[2], \"flow\")\n", + "model.edge.add(model.user_demand[6], model.basin[5], \"flow\")\n", + "model.edge.add(model.tabulated_rating_curve[7], model.fractional_flow[9], \"flow\")\n", + "model.edge.add(model.fractional_flow[8], model.terminal[10], \"flow\")\n", + "model.edge.add(model.fractional_flow[9], model.basin[12], \"flow\")\n", + "model.edge.add(model.basin[12], model.user_demand[13], \"flow\")\n", + "model.edge.add(model.user_demand[13], model.terminal[10], \"flow\")\n", + "model.edge.add(model.discrete_control[11], model.fractional_flow[8], \"control\")\n", + "model.edge.add(model.discrete_control[11], model.fractional_flow[9], \"control\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's take a look at the model:" + "Let's take a look at the model:\n" ] }, { @@ -1786,7 +1313,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Write the model to a TOML and GeoPackage:" + "Write the model to a TOML and GeoPackage:\n" ] }, { @@ -1824,7 +1351,7 @@ "metadata": {}, "source": [ "Now run the model with `ribasim allocation_example/ribasim.toml`.\n", - "After running the model, read back the results:" + "After running the model, read back the results:\n" ] }, { @@ -1868,7 +1395,7 @@ "\n", "- Abstraction 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.\n", "\n", - "- Although there is a plotted line for abstraction per priority, abstraction is actually accumulated over all priorities per user." + "- Although there is a plotted line for abstraction per priority, abstraction is actually accumulated over all priorities per user.\n" ] }, { @@ -1891,54 +1418,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Model with allocation (basin supply/demand)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setup the nodes:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "xy = np.array(\n", - " [\n", - " (0.0, 0.0), # 1: FlowBoundary\n", - " (1.0, 0.0), # 2: Basin\n", - " (2.0, 0.0), # 3: UserDemand\n", - " (1.0, -1.0), # 4: LevelDemand\n", - " (2.0, -1.0), # 5: Basin\n", - " ]\n", - ")\n", - "node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n", - "\n", - "node_type = [\"FlowBoundary\", \"Basin\", \"UserDemand\", \"LevelDemand\", \"Basin\"]\n", - "\n", - "# Make sure the feature id starts at 1: explicitly give an index.\n", - "node = ribasim.Node(\n", - " df=gpd.GeoDataFrame(\n", - " data={\n", - " \"node_type\": node_type,\n", - " \"subnetwork_id\": 5 * [2],\n", - " },\n", - " index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n", - " geometry=node_xy,\n", - " crs=\"EPSG:28992\",\n", - " )\n", - ")" + "# Model with allocation (basin supply/demand)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the edges:" + "Setup a model:\n" ] }, { @@ -1947,23 +1434,9 @@ "metadata": {}, "outputs": [], "source": [ - "from_id = np.array([1, 2, 4, 3, 4])\n", - "to_id = np.array([2, 3, 2, 5, 5])\n", - "edge_type = [\"flow\", \"flow\", \"control\", \"flow\", \"control\"]\n", - "subnetwork_id = [2, None, None, None, None]\n", - "\n", - "lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist())\n", - "edge = ribasim.Edge(\n", - " df=gpd.GeoDataFrame(\n", - " data={\n", - " \"from_node_id\": from_id,\n", - " \"to_node_id\": to_id,\n", - " \"edge_type\": edge_type,\n", - " \"subnetwork_id\": subnetwork_id,\n", - " },\n", - " geometry=lines,\n", - " crs=\"EPSG:28992\",\n", - " )\n", + "model = ribasim.Model(\n", + " starttime=\"2020-01-01 00:00:00\",\n", + " endtime=\"2020-02-01 00:00:00\",\n", ")" ] }, @@ -1971,7 +1444,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the basins:" + "Setup the basins:\n" ] }, { @@ -1980,40 +1453,47 @@ "metadata": {}, "outputs": [], "source": [ - "profile = pd.DataFrame(\n", - " data={\"node_id\": [2, 2, 5, 5], \"area\": 1e3, \"level\": [0.0, 1.0, 0.0, 1.0]}\n", - ")\n", - "static = pd.DataFrame(\n", - " data={\n", - " \"node_id\": [5],\n", - " \"drainage\": 0.0,\n", - " \"potential_evaporation\": 0.0,\n", - " \"infiltration\": 0.0,\n", - " \"precipitation\": 0.0,\n", - " \"urban_runoff\": 0.0,\n", - " }\n", + "basin_data = [\n", + " basin.Profile(area=[1e3, 1e3], level=[0.0, 1.0]),\n", + " basin.State(level=[0.5]),\n", + "]\n", + "model.basin.add(\n", + " Node(2, Point(1.0, 0.0)),\n", + " [\n", + " *basin_data,\n", + " basin.Time(\n", + " time=[\"2020-01-01 00:00:00\", \"2020-01-16 00:00:00\"],\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", ")\n", - "time = pd.DataFrame(\n", - " data={\n", - " \"node_id\": 2,\n", - " \"time\": [\"2020-01-01 00:00:00\", \"2020-01-16 00:00:00\"],\n", - " \"drainage\": 0.0,\n", - " \"potential_evaporation\": 0.0,\n", - " \"infiltration\": 0.0,\n", - " \"precipitation\": [1e-6, 0.0],\n", - " \"urban_runoff\": 0.0,\n", - " },\n", + "model.basin.add(\n", + " Node(5, Point(2.0, -1.0)),\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", - "\n", - "state = pd.DataFrame(data={\"node_id\": [2, 5], \"level\": 0.5})\n", - "basin = ribasim.Basin(profile=profile, static=static, time=time, state=state)" + "profile = pd.DataFrame(\n", + " data={\"node_id\": [2, 2, 5, 5], \"area\": 1e3, \"level\": [0.0, 1.0, 0.0, 1.0]}\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the flow boundary:" + "Setup the flow boundary:\n" ] }, { @@ -2022,8 +1502,8 @@ "metadata": {}, "outputs": [], "source": [ - "flow_boundary = ribasim.FlowBoundary(\n", - " static=pd.DataFrame(data={\"node_id\": [1], \"flow_rate\": 1e-3})\n", + "model.flow_boundary.add(\n", + " Node(1, Point(0.0, 0.0)), [flow_boundary.Static(flow_rate=[1e-3])]\n", ")" ] }, @@ -2031,7 +1511,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup allocation level control:" + "Setup allocation level control:\n" ] }, { @@ -2040,10 +1520,9 @@ "metadata": {}, "outputs": [], "source": [ - "level_demand = ribasim.LevelDemand(\n", - " static=pd.DataFrame(\n", - " data={\"node_id\": [4], \"priority\": 1, \"min_level\": 1.0, \"max_level\": 1.5}\n", - " )\n", + "model.level_demand.add(\n", + " Node(4, Point(1.0, -1.0)),\n", + " [level_demand.Static(priority=[1], min_level=[1.0], max_level=[1.5])],\n", ")" ] }, @@ -2051,7 +1530,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the users:" + "Setup the users:\n" ] }, { @@ -2060,16 +1539,13 @@ "metadata": {}, "outputs": [], "source": [ - "user_demand = ribasim.UserDemand(\n", - " static=pd.DataFrame(\n", - " data={\n", - " \"node_id\": [3],\n", - " \"priority\": [2],\n", - " \"demand\": [1.5e-3],\n", - " \"return_factor\": [0.2],\n", - " \"min_level\": [0.2],\n", - " }\n", - " )\n", + "model.user_demand.add(\n", + " Node(3, Point(2.0, 0.0)),\n", + " [\n", + " user_demand.Static(\n", + " priority=[2], demand=[1.5e-3], return_factor=[0.2], min_level=[0.2]\n", + " )\n", + " ],\n", ")" ] }, @@ -2077,7 +1553,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Setup the allocation:" + "Setup the allocation:\n" ] }, { @@ -2086,14 +1562,14 @@ "metadata": {}, "outputs": [], "source": [ - "allocation = ribasim.Allocation(use_allocation=True, timestep=1e5)" + "model.allocation = ribasim.Allocation(use_allocation=True, timestep=1e5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Setup a model:" + "Setup the edges:\n" ] }, { @@ -2102,23 +1578,18 @@ "metadata": {}, "outputs": [], "source": [ - "model = ribasim.Model(\n", - " network=ribasim.Network(node=node, edge=edge),\n", - " basin=basin,\n", - " flow_boundary=flow_boundary,\n", - " level_demand=level_demand,\n", - " user_demand=user_demand,\n", - " allocation=allocation,\n", - " starttime=\"2020-01-01 00:00:00\",\n", - " endtime=\"2020-02-01 00:00:00\",\n", - ")" + "model.edge.add(model.flow_boundary[1], model.basin[2], \"flow\", subnetwork_id=2)\n", + "model.edge.add(model.basin[2], model.user_demand[3], \"flow\")\n", + "model.edge.add(model.level_demand[4], model.basin[2], \"control\")\n", + "model.edge.add(model.user_demand[3], model.basin[5], \"flow\")\n", + "model.edge.add(model.level_demand[4], model.basin[5], \"control\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's take a look at the model:" + "Let's take a look at the model:\n" ] }, { @@ -2134,7 +1605,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Write the model to a TOML and GeoPackage:" + "Write the model to a TOML and GeoPackage:\n" ] }, { @@ -2171,7 +1642,7 @@ "metadata": {}, "source": [ "Now run the model with `ribasim level_demand/ribasim.toml`.\n", - "After running the model, read back the results:" + "After running the model, read back the results:\n" ] }, { @@ -2205,9 +1676,10 @@ "The Basin level is a piecewise linear function of time, with several stages explained below.\n", "\n", "Constants:\n", + "\n", "- $d$: UserDemand #3 demand,\n", "- $\\phi$: Basin #2 precipitation rate,\n", - "- $q$: LevelBoundary flow." + "- $q$: LevelBoundary flow.\n" ] }, { @@ -2215,12 +1687,13 @@ "metadata": {}, "source": [ "Stages:\n", + "\n", "- In the first stage the UserDemand abstracts fully, so the net change of Basin #2 is $q + \\phi - d$;\n", "- In the second stage the Basin takes precedence so the UserDemand doesn't abstract, hence the net change of Basin #2 is $q + \\phi$;\n", "- In 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$;\n", "- In 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$;\n", "- At 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$.\n", - "- In 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." + "- In 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" ] }, { @@ -2245,7 +1718,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.7" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/python/ribasim/ribasim/__init__.py b/python/ribasim/ribasim/__init__.py index a392b1b9d..56fd9d577 100644 --- a/python/ribasim/ribasim/__init__.py +++ b/python/ribasim/ribasim/__init__.py @@ -1,55 +1,8 @@ __version__ = "2024.4.0" -from ribasim.config import ( - Allocation, - Basin, - DiscreteControl, - FlowBoundary, - FractionalFlow, - LevelBoundary, - LevelDemand, - LinearResistance, - Logging, - ManningResistance, - Outlet, - PidControl, - Pump, - Results, - Solver, - TabulatedRatingCurve, - Terminal, - UserDemand, - Verbosity, -) -from ribasim.geometry.edge import Edge, EdgeSchema -from ribasim.geometry.node import Node, NodeSchema -from ribasim.model import Model, Network +from ribasim.config import Allocation, Logging, Solver +from ribasim.geometry.edge import EdgeTable +from ribasim.model import Model -__all__ = [ - "Allocation", - "Basin", - "DiscreteControl", - "Edge", - "EdgeSchema", - "FlowBoundary", - "FractionalFlow", - "LevelBoundary", - "LevelDemand", - "LinearResistance", - "Logging", - "ManningResistance", - "Model", - "Network", - "Node", - "NodeSchema", - "Outlet", - "PidControl", - "Pump", - "Results", - "Solver", - "TabulatedRatingCurve", - "Terminal", - "UserDemand", - "Verbosity", -] +__all__ = ["EdgeTable", "Allocation", "Logging", "Model", "Solver"] diff --git a/python/ribasim/ribasim/config.py b/python/ribasim/ribasim/config.py index e4a40134e..e28c73ed2 100644 --- a/python/ribasim/ribasim/config.py +++ b/python/ribasim/ribasim/config.py @@ -1,8 +1,15 @@ +from collections.abc import Sequence from enum import Enum +from typing import Any -from pydantic import Field +import pandas as pd +import pydantic +from geopandas import GeoDataFrame +from pydantic import ConfigDict, Field +from shapely.geometry import Point -from ribasim.geometry import BasinAreaSchema +from ribasim.geometry import BasinAreaSchema, NodeTable +from ribasim.geometry.edge import NodeData from ribasim.input_base import ChildModel, NodeModel, SpatialTableModel, TableModel # These schemas are autogenerated @@ -33,6 +40,7 @@ UserDemandStaticSchema, UserDemandTimeSchema, ) +from ribasim.utils import _pascal_to_snake class Allocation(ChildModel): @@ -73,14 +81,77 @@ class Logging(ChildModel): timing: bool = False -class Terminal(NodeModel): +class Node(pydantic.BaseModel): + node_id: int + geometry: Point + name: str = "" + subnetwork_id: int | None = None + + model_config = ConfigDict(arbitrary_types_allowed=True) + + def __init__(self, node_id: int, geometry: Point, **kwargs) -> None: + super().__init__(node_id=node_id, geometry=geometry, **kwargs) + + def into_geodataframe(self, node_type: str) -> GeoDataFrame: + return GeoDataFrame( + data={ + "node_id": pd.Series([self.node_id], dtype=int), + "node_type": pd.Series([node_type], dtype=str), + "name": pd.Series([self.name], dtype=str), + "subnetwork_id": pd.Series([self.subnetwork_id], dtype=pd.Int64Dtype()), + }, + geometry=[self.geometry], + ) + + +class MultiNodeModel(NodeModel): + node: NodeTable = Field(default_factory=NodeTable) + _node_type: str + + def add(self, node: Node, tables: Sequence[TableModel[Any]] | None = None) -> None: + if tables is None: + tables = [] + + node_id = node.node_id + if self.node.df is not None and node_id in self.node.df["node_id"].to_numpy(): + raise ValueError( + f"Node IDs have to be unique, but {node_id=} already exists." + ) + + for table in tables: + member_name = _pascal_to_snake(table.__class__.__name__) + existing_member = getattr(self, member_name) + existing_table = ( + existing_member.df if existing_member.df is not None else pd.DataFrame() + ) + assert table.df is not None + table_to_append = table.df.assign(node_id=node_id) + setattr(self, member_name, pd.concat([existing_table, table_to_append])) + + node_table = node.into_geodataframe( + node_type=self.__class__.__name__, + ) + self.node.df = ( + node_table # type: ignore + if self.node.df is None + else pd.concat([self.node.df, node_table]) + ) + + def __getitem__(self, index): + row = self.node.df[self.node.df["node_id"] == index].iloc[0] + return NodeData( + node_id=index, node_type=row["node_type"], geometry=row["geometry"] + ) + + +class Terminal(MultiNodeModel): static: TableModel[TerminalStaticSchema] = Field( default_factory=TableModel[TerminalStaticSchema], json_schema_extra={"sort_keys": ["node_id"]}, ) -class PidControl(NodeModel): +class PidControl(MultiNodeModel): static: TableModel[PidControlStaticSchema] = Field( default_factory=TableModel[PidControlStaticSchema], json_schema_extra={"sort_keys": ["node_id", "control_state"]}, @@ -91,7 +162,7 @@ class PidControl(NodeModel): ) -class LevelBoundary(NodeModel): +class LevelBoundary(MultiNodeModel): static: TableModel[LevelBoundaryStaticSchema] = Field( default_factory=TableModel[LevelBoundaryStaticSchema], json_schema_extra={"sort_keys": ["node_id"]}, @@ -102,14 +173,14 @@ class LevelBoundary(NodeModel): ) -class Pump(NodeModel): +class Pump(MultiNodeModel): static: TableModel[PumpStaticSchema] = Field( default_factory=TableModel[PumpStaticSchema], json_schema_extra={"sort_keys": ["node_id", "control_state"]}, ) -class TabulatedRatingCurve(NodeModel): +class TabulatedRatingCurve(MultiNodeModel): static: TableModel[TabulatedRatingCurveStaticSchema] = Field( default_factory=TableModel[TabulatedRatingCurveStaticSchema], json_schema_extra={"sort_keys": ["node_id", "control_state", "level"]}, @@ -120,7 +191,7 @@ class TabulatedRatingCurve(NodeModel): ) -class UserDemand(NodeModel): +class UserDemand(MultiNodeModel): static: TableModel[UserDemandStaticSchema] = Field( default_factory=TableModel[UserDemandStaticSchema], json_schema_extra={"sort_keys": ["node_id", "priority"]}, @@ -131,7 +202,7 @@ class UserDemand(NodeModel): ) -class LevelDemand(NodeModel): +class LevelDemand(MultiNodeModel): static: TableModel[LevelDemandStaticSchema] = Field( default_factory=TableModel[LevelDemandStaticSchema], json_schema_extra={"sort_keys": ["node_id", "priority"]}, @@ -142,7 +213,7 @@ class LevelDemand(NodeModel): ) -class FlowBoundary(NodeModel): +class FlowBoundary(MultiNodeModel): static: TableModel[FlowBoundaryStaticSchema] = Field( default_factory=TableModel[FlowBoundaryStaticSchema], json_schema_extra={"sort_keys": ["node_id"]}, @@ -153,7 +224,7 @@ class FlowBoundary(NodeModel): ) -class Basin(NodeModel): +class Basin(MultiNodeModel): profile: TableModel[BasinProfileSchema] = Field( default_factory=TableModel[BasinProfileSchema], json_schema_extra={"sort_keys": ["node_id", "level"]}, @@ -180,18 +251,18 @@ class Basin(NodeModel): ) -class ManningResistance(NodeModel): +class ManningResistance(MultiNodeModel): static: TableModel[ManningResistanceStaticSchema] = Field( default_factory=TableModel[ManningResistanceStaticSchema], json_schema_extra={"sort_keys": ["node_id", "control_state"]}, ) -class DiscreteControl(NodeModel): +class DiscreteControl(MultiNodeModel): condition: TableModel[DiscreteControlConditionSchema] = Field( default_factory=TableModel[DiscreteControlConditionSchema], json_schema_extra={ - "sort_keys": ["node_id", "listen_feature_id", "variable", "greater_than"] + "sort_keys": ["node_id", "listen_node_id", "variable", "greater_than"] }, ) logic: TableModel[DiscreteControlLogicSchema] = Field( @@ -200,21 +271,21 @@ class DiscreteControl(NodeModel): ) -class Outlet(NodeModel): +class Outlet(MultiNodeModel): static: TableModel[OutletStaticSchema] = Field( default_factory=TableModel[OutletStaticSchema], json_schema_extra={"sort_keys": ["node_id", "control_state"]}, ) -class LinearResistance(NodeModel): +class LinearResistance(MultiNodeModel): static: TableModel[LinearResistanceStaticSchema] = Field( default_factory=TableModel[LinearResistanceStaticSchema], json_schema_extra={"sort_keys": ["node_id", "control_state"]}, ) -class FractionalFlow(NodeModel): +class FractionalFlow(MultiNodeModel): static: TableModel[FractionalFlowStaticSchema] = Field( default_factory=TableModel[FractionalFlowStaticSchema], json_schema_extra={"sort_keys": ["node_id", "control_state"]}, diff --git a/python/ribasim/ribasim/geometry/__init__.py b/python/ribasim/ribasim/geometry/__init__.py index 55912eb5e..90889b3c4 100644 --- a/python/ribasim/ribasim/geometry/__init__.py +++ b/python/ribasim/ribasim/geometry/__init__.py @@ -1,5 +1,5 @@ from ribasim.geometry.area import BasinAreaSchema -from ribasim.geometry.edge import Edge -from ribasim.geometry.node import Node +from ribasim.geometry.edge import EdgeTable +from ribasim.geometry.node import NodeTable -__all__ = ["BasinAreaSchema", "Edge", "Node"] +__all__ = ["BasinAreaSchema", "EdgeTable", "NodeTable"] diff --git a/python/ribasim/ribasim/geometry/area.py b/python/ribasim/ribasim/geometry/area.py index 1f6b9e638..950165eed 100644 --- a/python/ribasim/ribasim/geometry/area.py +++ b/python/ribasim/ribasim/geometry/area.py @@ -8,5 +8,5 @@ class BasinAreaSchema(_BaseSchema): - node_id: Series[int] + node_id: Series[int] = pa.Field(nullable=False, default=0) geometry: GeoSeries[Any] = pa.Field(default=None, nullable=True) diff --git a/python/ribasim/ribasim/geometry/edge.py b/python/ribasim/ribasim/geometry/edge.py index d45cfb5e6..7bb69e944 100644 --- a/python/ribasim/ribasim/geometry/edge.py +++ b/python/ribasim/ribasim/geometry/edge.py @@ -1,18 +1,26 @@ -from typing import Any +from typing import Any, NamedTuple import matplotlib.pyplot as plt import numpy as np import pandas as pd import pandera as pa import shapely +from geopandas import GeoDataFrame from matplotlib.axes import Axes from numpy.typing import NDArray -from pandera.typing import Series +from pandera.typing import DataFrame, Series from pandera.typing.geopandas import GeoSeries +from shapely.geometry import LineString, MultiLineString, Point from ribasim.input_base import SpatialTableModel -__all__ = ("Edge",) +__all__ = ("EdgeTable",) + + +class NodeData(NamedTuple): + node_id: int + node_type: str + geometry: Point class EdgeSchema(pa.SchemaModel): @@ -31,15 +39,44 @@ class Config: add_missing_columns = True -class Edge(SpatialTableModel[EdgeSchema]): - """ - Defines the connections between nodes. - - Parameters - ---------- - static : pandas.DataFrame - Table describing the flow connections. - """ +class EdgeTable(SpatialTableModel[EdgeSchema]): + """Defines the connections between nodes.""" + + def __init__(self, **kwargs): + kwargs.setdefault("df", DataFrame[EdgeSchema]()) + super().__init__(**kwargs) + + def add( + self, + from_node: NodeData, + to_node: NodeData, + edge_type: str, + geometry: LineString | MultiLineString | None = None, + name: str = "", + subnetwork_id: int | None = None, + ): + geometry_to_append = ( + [LineString([from_node.geometry, to_node.geometry])] + if geometry is None + else [geometry] + ) + table_to_append = GeoDataFrame( + data={ + "from_node_type": pd.Series([from_node.node_type], dtype=str), + "from_node_id": pd.Series([from_node.node_id], dtype=int), + "to_node_type": pd.Series([to_node.node_type], dtype=str), + "to_node_id": pd.Series([to_node.node_id], dtype=int), + "edge_type": pd.Series([edge_type], dtype=str), + "name": pd.Series([name], dtype=str), + "subnetwork_id": pd.Series([subnetwork_id], dtype=pd.Int64Dtype()), + }, + geometry=geometry_to_append, + ) + + if self.df is None: + self.df = table_to_append + else: + self.df = pd.concat([self.df, table_to_append]) # type: ignore def get_where_edge_type(self, edge_type: str) -> NDArray[np.bool_]: assert self.df is not None diff --git a/python/ribasim/ribasim/geometry/node.py b/python/ribasim/ribasim/geometry/node.py index 33bb77b98..72f610745 100644 --- a/python/ribasim/ribasim/geometry/node.py +++ b/python/ribasim/ribasim/geometry/node.py @@ -1,4 +1,3 @@ -from collections.abc import Sequence from typing import Any import geopandas as gpd @@ -6,16 +5,13 @@ import numpy as np import pandas as pd import pandera as pa -import shapely from matplotlib.patches import Patch -from numpy.typing import NDArray from pandera.typing import Series from pandera.typing.geopandas import GeoSeries -from pydantic import field_validator from ribasim.input_base import SpatialTableModel -__all__ = ("Node",) +__all__ = ("NodeTable",) class NodeSchema(pa.SchemaModel): @@ -32,119 +28,9 @@ class Config: coerce = True -class Node(SpatialTableModel[NodeSchema]): +class NodeTable(SpatialTableModel[NodeSchema]): """The Ribasim nodes as Point geometries.""" - # TODO: Remove as soon as add api has been merged - @field_validator("df", mode="before") - @classmethod - def add_node_id_column(cls, df: gpd.GeoDataFrame) -> gpd.GeoDataFrame: - if "node_id" not in df.columns: - df.insert(0, "node_id", df.index) - return df - - @staticmethod - def node_ids_and_types(*nodes): - # TODO Not sure if this staticmethod belongs here - data_types = {"node_id": int, "node_type": str} - node_type = pd.DataFrame( - {col: pd.Series(dtype=dtype) for col, dtype in data_types.items()} - ) - - for node in nodes: - if not node: - continue - - ids, types = node.node_ids_and_types() - node_type_table = pd.DataFrame( - data={ - "node_id": ids, - "node_type": types, - } - ) - node_type = node_type._append(node_type_table) - - node_type = node_type.drop_duplicates(subset="node_id") - node_type = node_type.sort_values("node_id") - - node_id = node_type.node_id.tolist() - node_type = node_type.node_type.tolist() - - return node_id, node_type - - def geometry_from_connectivity( - self, from_id: Sequence[int], to_id: Sequence[int] - ) -> NDArray[Any]: - """ - Create edge shapely geometries from connectivities. - - Parameters - ---------- - node : Ribasim.Node - from_id : Sequence[int] - First node of every edge. - to_id : Sequence[int] - Second node of every edge. - - Returns - ------- - edge_geometry : np.ndarray - Array of shapely LineStrings. - """ - assert self.df is not None - geometry = self.df["geometry"] - from_points = shapely.get_coordinates(geometry.loc[from_id]) - to_points = shapely.get_coordinates(geometry.loc[to_id]) - n = len(from_points) - vertices = np.empty((n * 2, 2), dtype=from_points.dtype) - vertices[0::2, :] = from_points - vertices[1::2, :] = to_points - indices = np.repeat(np.arange(n), 2) - return shapely.linestrings(coords=vertices, indices=indices) - - def connectivity_from_geometry( - self, lines: NDArray[Any] - ) -> tuple[NDArray[Any], NDArray[Any]]: - """ - Derive from_node_id and to_node_id for every edge in lines. LineStrings - may be used to connect multiple nodes in a sequence, but every linestring - vertex must also a node. - - Parameters - ---------- - node : Node - lines : np.ndarray - Array of shapely linestrings. - - Returns - ------- - from_node_id : np.ndarray of int - to_node_id : np.ndarray of int - """ - assert self.df is not None - node_index = self.df.index - node_xy = shapely.get_coordinates(self.df.geometry.values) - edge_xy = shapely.get_coordinates(lines) - - xy = np.vstack([node_xy, edge_xy]) - _, inverse = np.unique(xy, return_inverse=True, axis=0) - _, index, inverse = np.unique( - xy, return_index=True, return_inverse=True, axis=0 - ) - uniques_index = index[inverse] - - node_node_id, edge_node_id = np.split(uniques_index, [len(node_xy)]) - if not np.isin(edge_node_id, node_node_id).all(): - raise ValueError( - "Edge lines contain coordinates that are not in the node layer. " - "Please ensure all edges are snapped to nodes exactly." - ) - - edge_node_id = edge_node_id.reshape((-1, 2)) - from_id = node_index[edge_node_id[:, 0]].to_numpy() - to_id = node_index[edge_node_id[:, 1]].to_numpy() - return from_id, to_id - def plot_allocation_networks(self, ax=None, zorder=None) -> Any: if ax is None: _, ax = plt.subplots() @@ -237,7 +123,8 @@ def plot(self, ax=None, zorder=None) -> Any: "LevelDemand": "k", "": "k", } - assert self.df is not None + if self.df is None: + return for nodetype, df in self.df.groupby("node_type"): assert isinstance(nodetype, str) @@ -254,7 +141,9 @@ def plot(self, ax=None, zorder=None) -> Any: assert self.df is not None geometry = self.df["geometry"] - for text, xy in zip(self.df.index, np.column_stack((geometry.x, geometry.y))): + for text, xy in zip( + self.df["node_id"], np.column_stack((geometry.x, geometry.y)) + ): ax.annotate(text=text, xy=xy, xytext=(2.0, 2.0), textcoords="offset points") return ax diff --git a/python/ribasim/ribasim/input_base.py b/python/ribasim/ribasim/input_base.py index 321b0b144..c76ca6cbd 100644 --- a/python/ribasim/ribasim/input_base.py +++ b/python/ribasim/ribasim/input_base.py @@ -1,4 +1,5 @@ import re +import warnings from abc import ABC, abstractmethod from collections.abc import Callable, Generator from contextlib import closing @@ -36,6 +37,7 @@ delimiter = " / " gpd.options.io_engine = "pyogrio" +warnings.filterwarnings("ignore", category=UserWarning, module="pyogrio") context_file_loading: ContextVar[dict[str, Any]] = ContextVar( "file_loading", default={} @@ -247,7 +249,8 @@ def _write_table(self, temp_path: Path) -> None: SQLite connection to the database. """ table = self.tablename() - assert self.df is not None + if self.df is None: + return # Add `fid` to all tables as primary key # Enables editing values manually in QGIS @@ -358,12 +361,11 @@ def _write_table(self, path: FilePath) -> None: gdf = gpd.GeoDataFrame(data=self.df) gdf = gdf.set_geometry("geometry") - gdf.index.name = "fid" - - gdf.to_file(path, layer=self.tablename(), driver="GPKG", index=True) + gdf.to_file(path, layer=self.tablename(), driver="GPKG", mode="a") def sort(self): - self.df.sort_index(inplace=True) + if self.df is not None: + self.df.sort_index(inplace=True) class ChildModel(BaseModel): @@ -408,10 +410,6 @@ def get_input_type(cls): def _layername(cls, field: str) -> str: return f"{cls.get_input_type()}{delimiter}{field}" - def add(*args, **kwargs): - # TODO This is the new API - pass - def tables(self) -> Generator[TableModel[Any], Any, None]: for key in self.fields(): attr = getattr(self, key) @@ -424,16 +422,16 @@ def node_ids(self) -> set[int]: node_ids.update(table.node_ids()) return node_ids - def node_ids_and_types(self) -> tuple[list[int], list[str]]: - ids = self.node_ids() - return list(ids), len(ids) * [self.get_input_type()] - def _save(self, directory: DirectoryPath, input_dir: DirectoryPath, **kwargs): - for field in self.fields(): - getattr(self, field)._save( - directory, - input_dir, - ) + # TODO: stop sorting loop so that "node" comes first + for field in sorted(self.fields(), key=lambda x: x != "node"): + attr = getattr(self, field) + # TODO + if hasattr(attr, "_save"): + attr._save( + directory, + input_dir, + ) def _repr_content(self) -> str: """Generate a succinct overview of the content. diff --git a/python/ribasim/ribasim/model.py b/python/ribasim/ribasim/model.py index 0a5e457a8..bcc7fb43e 100644 --- a/python/ribasim/ribasim/model.py +++ b/python/ribasim/ribasim/model.py @@ -1,18 +1,15 @@ import datetime -import shutil from pathlib import Path from typing import Any -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd import tomli import tomli_w +from matplotlib import pyplot as plt from pydantic import ( DirectoryPath, Field, + FilePath, field_serializer, - model_serializer, model_validator, ) @@ -37,154 +34,45 @@ Terminal, UserDemand, ) -from ribasim.geometry.edge import Edge -from ribasim.geometry.node import Node -from ribasim.input_base import ChildModel, FileModel, NodeModel, context_file_loading -from ribasim.types import FilePath - - -class Network(FileModel, NodeModel): - filepath: Path | None = Field( - default=Path("database.gpkg"), exclude=True, repr=False - ) - - node: Node = Field(default_factory=Node) - edge: Edge = Field(default_factory=Edge) - - def n_nodes(self): - if self.node.df is not None: - n = len(self.node.df) - else: - n = 0 - - return n - - @classmethod - def _load(cls, filepath: Path | None) -> dict[str, Any]: - directory = context_file_loading.get().get("directory", None) - if directory is not None: - context_file_loading.get()["database"] = directory / "database.gpkg" - return {} - - @classmethod - def _layername(cls, field: str) -> str: - return field.capitalize() - - def _save(self, directory, input_dir=Path(".")): - # We write all tables to a temporary database with a dot prefix, - # and at the end move this over the target file. - # This does not throw a PermissionError if the file is open in QGIS. - directory = Path(directory) - db_path = directory / input_dir / "database.gpkg" - db_path = db_path.resolve() - db_path.parent.mkdir(parents=True, exist_ok=True) - temp_path = db_path.with_stem(".database") - - # avoid adding tables to existing model - temp_path.unlink(missing_ok=True) - context_file_loading.get()["database"] = temp_path - - self.node._save(directory, input_dir) - self.edge._save(directory, input_dir) - - shutil.move(temp_path, db_path) - context_file_loading.get()["database"] = db_path - - @model_serializer - def set_modelname(self) -> str: - if self.filepath is not None: - return str(self.filepath.name) - else: - return str(self.model_fields["filepath"].default) +from ribasim.geometry.edge import EdgeTable +from ribasim.input_base import ( + ChildModel, + FileModel, + NodeModel, + context_file_loading, +) class Model(FileModel): - """ - A full Ribasim model schematisation with all input. - - Ribasim model containing the location of the nodes, the edges between the - nodes, and the node parametrization. - - Parameters - ---------- - starttime : datetime.datetime - Starting time of the simulation. - endtime : datetime.datetime - End time of the simulation. - - input_dir: Path = Path(".") - The directory of the input files. - results_dir: Path = Path("results") - The directory of the results files. - - network: Network - Class containing the topology (nodes and edges) of the model. - - results: Results - Results configuration options. - solver: Solver - Solver configuration options. - logging: Logging - Logging configuration options. - - allocation: Allocation - The allocation configuration. - basin : Basin - The waterbodies. - fractional_flow : FractionalFlow - Split flows into fractions. - level_boundary : LevelBoundary - Boundary condition specifying the water level. - flow_boundary : FlowBoundary - Boundary conditions specifying the flow. - linear_resistance: LinearResistance - Linear flow resistance. - manning_resistance : ManningResistance - Flow resistance based on the Manning formula. - tabulated_rating_curve : TabulatedRatingCurve - Tabulated rating curve describing flow based on the upstream water level. - pump : Pump - Prescribed flow rate from one basin to the other. - outlet : Outlet - Prescribed flow rate from one basin to the other. - terminal : Terminal - Water sink without state or properties. - discrete_control : DiscreteControl - Discrete control logic. - pid_control : PidControl - PID controller attempting to set the level of a basin to a desired value using a pump/outlet. - user_demand : UserDemand - UserDemand node type with demand and priority. - """ - starttime: datetime.datetime endtime: datetime.datetime input_dir: Path = Field(default_factory=lambda: Path(".")) results_dir: Path = Field(default_factory=lambda: Path("results")) - network: Network = Field(default_factory=Network, alias="database", exclude=True) - results: Results = Results() - solver: Solver = Solver() - logging: Logging = Logging() - allocation: Allocation = Field(default_factory=Allocation) - level_demand: LevelDemand = Field(default_factory=LevelDemand) + logging: Logging = Field(default_factory=Logging) + solver: Solver = Field(default_factory=Solver) + results: Results = Field(default_factory=Results) + basin: Basin = Field(default_factory=Basin) - fractional_flow: FractionalFlow = Field(default_factory=FractionalFlow) - level_boundary: LevelBoundary = Field(default_factory=LevelBoundary) - flow_boundary: FlowBoundary = Field(default_factory=FlowBoundary) linear_resistance: LinearResistance = Field(default_factory=LinearResistance) manning_resistance: ManningResistance = Field(default_factory=ManningResistance) tabulated_rating_curve: TabulatedRatingCurve = Field( default_factory=TabulatedRatingCurve ) + fractional_flow: FractionalFlow = Field(default_factory=FractionalFlow) pump: Pump = Field(default_factory=Pump) + level_boundary: LevelBoundary = Field(default_factory=LevelBoundary) + flow_boundary: FlowBoundary = Field(default_factory=FlowBoundary) outlet: Outlet = Field(default_factory=Outlet) terminal: Terminal = Field(default_factory=Terminal) discrete_control: DiscreteControl = Field(default_factory=DiscreteControl) pid_control: PidControl = Field(default_factory=PidControl) user_demand: UserDemand = Field(default_factory=UserDemand) + level_demand: LevelDemand = Field(default_factory=LevelDemand) + + edge: EdgeTable = Field(default_factory=EdgeTable) @model_validator(mode="after") def set_node_parent(self) -> "Model": @@ -213,13 +101,7 @@ def __repr__(self) -> str: INDENT = " " for field in self.fields(): attr = getattr(self, field) - if isinstance(attr, NodeModel): - attr_content = attr._repr_content() - typename = type(attr).__name__ - if attr_content: - content.append(f"{INDENT}{field}={typename}({attr_content}),") - else: - content.append(f"{INDENT}{field}={repr(attr)},") + content.append(f"{INDENT}{field}={repr(attr)},") content.append(")") return "\n".join(content) @@ -236,7 +118,11 @@ def _write_toml(self, fn: FilePath): return fn def _save(self, directory: DirectoryPath, input_dir: DirectoryPath): - self.network._save(directory, input_dir) + db_path = directory / input_dir / "database.gpkg" + db_path.parent.mkdir(parents=True, exist_ok=True) + db_path.unlink(missing_ok=True) + context_file_loading.get()["database"] = db_path + self.edge._save(directory, input_dir) for sub in self.nodes().values(): sub._save(directory, input_dir) @@ -244,7 +130,7 @@ def nodes(self): return { k: getattr(self, k) for k in self.model_fields.keys() - if isinstance(getattr(self, k), NodeModel) and k != "network" + if isinstance(getattr(self, k), NodeModel) } def children(self): @@ -255,54 +141,10 @@ def children(self): } def validate_model_node_field_ids(self): - """Check whether the node IDs of the node_type fields are valid.""" - - # Check node IDs of node fields - all_node_ids = set[int]() - for node in self.nodes().values(): - all_node_ids.update(node.node_ids()) - - unique, counts = np.unique(list(all_node_ids), return_counts=True) - node_ids_negative_integers = np.less(unique, 0) | np.not_equal( - unique.astype(np.int64), unique - ) - - if node_ids_negative_integers.any(): - raise ValueError( - f"Node IDs must be non-negative integers, got {unique[node_ids_negative_integers]}." - ) - - if (counts > 1).any(): - raise ValueError( - f"These node IDs were assigned to multiple node types: {unique[(counts > 1)]}." - ) + raise NotImplementedError() def validate_model_node_ids(self): - """Check whether the node IDs in the data tables correspond to the node IDs in the network.""" - - error_messages = [] - - for node in self.nodes().values(): - nodetype = node.get_input_type() - node_ids_data = set(node.node_ids()) - node_ids_network = set( - self.network.node.df.loc[ - self.network.node.df["node_type"] == nodetype - ].index - ) - - if not node_ids_network == node_ids_data: - extra_in_network = node_ids_network.difference(node_ids_data) - extra_in_data = node_ids_data.difference(node_ids_network) - error_messages.append( - f"""For {nodetype}, the node IDs in the data tables don't match the node IDs in the network. - Node IDs only in the data tables: {extra_in_data}. - Node IDs only in the network: {extra_in_network}. - """ - ) - - if len(error_messages) > 0: - raise ValueError("\n".join(error_messages)) + raise NotImplementedError() def validate_model(self): """Validate the model. @@ -315,28 +157,6 @@ def validate_model(self): self.validate_model_node_field_ids() self.validate_model_node_ids() - def _add_node_type(self, df: pd.DataFrame | None, id_col: str, type_col: str): - node = self.network.node.df - assert node is not None - if df is not None: - df[type_col] = node.loc[df[id_col], "node_type"].to_numpy() - - def _add_node_types(self): - """Add the from/to node types to tables that reference external node IDs. - - Only valid with globally unique node IDs, which is assured by using the node index. - """ - self._add_node_type(self.network.edge.df, "from_node_id", "from_node_type") - self._add_node_type(self.network.edge.df, "to_node_id", "to_node_type") - id_col, type_col = "listen_node_id", "listen_node_type" - self._add_node_type(self.pid_control.static.df, id_col, type_col) - self._add_node_type(self.pid_control.time.df, id_col, type_col) - self._add_node_type( - self.discrete_control.condition.df, - "listen_feature_id", - "listen_feature_type", - ) - @classmethod def read(cls, filepath: FilePath) -> "Model": """Read model from TOML file.""" @@ -352,8 +172,8 @@ def write(self, filepath: Path | str) -> Path: ---------- filepath: FilePath ending in .toml """ - self.validate_model() - self._add_node_types() + # TODO + # self.validate_model() filepath = Path(filepath) if not filepath.suffix == ".toml": raise ValueError(f"Filepath '{filepath}' is not a .toml file.") @@ -389,55 +209,7 @@ def reset_contextvar(self) -> "Model": return self def plot_control_listen(self, ax): - x_start, x_end = [], [] - y_start, y_end = [], [] - - condition = self.discrete_control.condition.df - if condition is not None: - for node_id in condition.node_id.unique(): - data_node_id = condition[condition.node_id == node_id] - - for listen_feature_id in data_node_id.listen_feature_id: - point_start = self.network.node.df.iloc[node_id - 1].geometry - x_start.append(point_start.x) - y_start.append(point_start.y) - - point_end = self.network.node.df.iloc[ - listen_feature_id - 1 - ].geometry - x_end.append(point_end.x) - y_end.append(point_end.y) - - for table in [self.pid_control.static.df, self.pid_control.time.df]: - if table is None: - continue - - node = self.network.node.df - - for node_id in table.node_id.unique(): - for listen_node_id in table.loc[ - table.node_id == node_id, "listen_node_id" - ].unique(): - point_start = node.iloc[listen_node_id - 1].geometry - x_start.append(point_start.x) - y_start.append(point_start.y) - - point_end = node.iloc[node_id - 1].geometry - x_end.append(point_end.x) - y_end.append(point_end.y) - - if len(x_start) == 0: - return - - # This part can probably be done more efficiently - for i, (x, y, x_, y_) in enumerate(zip(x_start, y_start, x_end, y_end)): - ax.plot( - [x, x_], - [y, y_], - c="gray", - ls="--", - label="Listen edge" if i == 0 else None, - ) + raise NotImplementedError() def plot(self, ax=None, indicate_subnetworks: bool = True) -> Any: """ @@ -456,92 +228,24 @@ def plot(self, ax=None, indicate_subnetworks: bool = True) -> Any: _, ax = plt.subplots() ax.axis("off") - self.network.edge.plot(ax=ax, zorder=2) - self.plot_control_listen(ax) - self.network.node.plot(ax=ax, zorder=3) + self.edge.plot(ax=ax, zorder=2) + for node in self.nodes().values(): + node.node.plot(ax=ax, zorder=3) + # TODO + # self.plot_control_listen(ax) + # self.node.plot(ax=ax, zorder=3) handles, labels = ax.get_legend_handles_labels() - if indicate_subnetworks: - ( - handles_subnetworks, - labels_subnetworks, - ) = self.network.node.plot_allocation_networks(ax=ax, zorder=1) - handles += handles_subnetworks - labels += labels_subnetworks + # TODO + # if indicate_subnetworks: + # ( + # handles_subnetworks, + # labels_subnetworks, + # ) = self.network.node.plot_allocation_networks(ax=ax, zorder=1) + # handles += handles_subnetworks + # labels += labels_subnetworks ax.legend(handles, labels, loc="lower left", bbox_to_anchor=(1, 0.5)) return ax - - def print_discrete_control_record(self, path: FilePath) -> None: - path = Path(path) - df_control = pd.read_feather(path) - node_attrs, node_instances = zip(*self.nodes().items()) - node_clss = [node_cls.get_input_type() for node_cls in node_instances] - truth_dict = {"T": ">", "F": "<"} - assert self.network.node.df is not None - assert self.network.edge.df is not None - - if self.discrete_control.condition.df is None: - raise ValueError("This model has no control input.") - - for index, row in df_control.iterrows(): - datetime = row["time"] - control_node_id = row["control_node_id"] - truth_state = row["truth_state"] - control_state = row["control_state"] - enumeration = f"{index}. " - - out = f"{enumeration}At {datetime} the control node with ID {control_node_id} reached truth state {truth_state}:\n" - - if self.discrete_control.condition.df is None: - return - - conditions = self.discrete_control.condition.df[ - self.discrete_control.condition.df.node_id == control_node_id - ] - - for truth_value, (index, condition) in zip( - truth_state, conditions.iterrows() - ): - var = condition["variable"] - listen_feature_id = condition["listen_feature_id"] - listen_node_type = self.network.node.df.loc[ - listen_feature_id, "node_type" - ] - symbol = truth_dict[truth_value] - greater_than = condition["greater_than"] - feature_type = "edge" if var == "flow" else "node" - - out += f"\tFor {feature_type} ID {listen_feature_id} ({listen_node_type}): {var} {symbol} {greater_than}\n" - - padding = len(enumeration) * " " - out += f'\n{padding}This yielded control state "{control_state}":\n' - - affect_node_ids = self.network.edge.df[ - self.network.edge.df.from_node_id == control_node_id - ].to_node_id - - for affect_node_id in affect_node_ids: - affect_node_type = self.network.node.df.loc[affect_node_id, "node_type"] - nodeattr = node_attrs[node_clss.index(affect_node_type)] - - out += f"\tFor node ID {affect_node_id} ({affect_node_type}): " - - static = getattr(self, nodeattr).static.df - row = static[ - (static.node_id == affect_node_id) - & (static.control_state == control_state) - ].iloc[0] - - names_and_values = [] - for var in static.columns: - if var not in ["node_id", "control_state"]: - value = row[var] - if value is not None: - names_and_values.append(f"{var} = {value}") - - out += ", ".join(names_and_values) + "\n" - - print(out) diff --git a/python/ribasim/ribasim/nodes/__init__.py b/python/ribasim/ribasim/nodes/__init__.py new file mode 100644 index 000000000..4901ad770 --- /dev/null +++ b/python/ribasim/ribasim/nodes/__init__.py @@ -0,0 +1,33 @@ +from ribasim.nodes import ( + basin, + discrete_control, + flow_boundary, + fractional_flow, + level_boundary, + level_demand, + linear_resistance, + manning_resistance, + outlet, + pid_control, + pump, + tabulated_rating_curve, + terminal, + user_demand, +) + +__all__ = [ + "basin", + "discrete_control", + "flow_boundary", + "fractional_flow", + "level_boundary", + "level_demand", + "linear_resistance", + "manning_resistance", + "outlet", + "pid_control", + "pump", + "tabulated_rating_curve", + "terminal", + "user_demand", +] diff --git a/python/ribasim/ribasim/nodes/basin.py b/python/ribasim/ribasim/nodes/basin.py new file mode 100644 index 000000000..0e529cdc1 --- /dev/null +++ b/python/ribasim/ribasim/nodes/basin.py @@ -0,0 +1,44 @@ +from geopandas import GeoDataFrame +from pandas import DataFrame + +from ribasim.geometry.area import BasinAreaSchema +from ribasim.input_base import TableModel +from ribasim.schemas import ( + BasinProfileSchema, + BasinStateSchema, + BasinStaticSchema, + BasinSubgridSchema, + BasinTimeSchema, +) + +__all__ = ["Static", "Time", "State", "Profile", "Subgrid", "Area"] + + +class Static(TableModel[BasinStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Time(TableModel[BasinTimeSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class State(TableModel[BasinStateSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Profile(TableModel[BasinProfileSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Subgrid(TableModel[BasinSubgridSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Area(TableModel[BasinAreaSchema]): + def __init__(self, **kwargs): + super().__init__(df=GeoDataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/discrete_control.py b/python/ribasim/ribasim/nodes/discrete_control.py new file mode 100644 index 000000000..eeb0b28ca --- /dev/null +++ b/python/ribasim/ribasim/nodes/discrete_control.py @@ -0,0 +1,16 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import DiscreteControlConditionSchema, DiscreteControlLogicSchema + +__all__ = ["Condition", "Logic"] + + +class Condition(TableModel[DiscreteControlConditionSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Logic(TableModel[DiscreteControlLogicSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/flow_boundary.py b/python/ribasim/ribasim/nodes/flow_boundary.py new file mode 100644 index 000000000..12ec8c028 --- /dev/null +++ b/python/ribasim/ribasim/nodes/flow_boundary.py @@ -0,0 +1,19 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + FlowBoundaryStaticSchema, + FlowBoundaryTimeSchema, +) + +__all__ = ["Static", "Time"] + + +class Static(TableModel[FlowBoundaryStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Time(TableModel[FlowBoundaryTimeSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/fractional_flow.py b/python/ribasim/ribasim/nodes/fractional_flow.py new file mode 100644 index 000000000..996ad6fed --- /dev/null +++ b/python/ribasim/ribasim/nodes/fractional_flow.py @@ -0,0 +1,11 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import FractionalFlowStaticSchema + +__all__ = ["Static"] + + +class Static(TableModel[FractionalFlowStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/level_boundary.py b/python/ribasim/ribasim/nodes/level_boundary.py new file mode 100644 index 000000000..da23c3c61 --- /dev/null +++ b/python/ribasim/ribasim/nodes/level_boundary.py @@ -0,0 +1,19 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + LevelBoundaryStaticSchema, + LevelBoundaryTimeSchema, +) + +__all__ = ["Static", "Time"] + + +class Static(TableModel[LevelBoundaryStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Time(TableModel[LevelBoundaryTimeSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/level_demand.py b/python/ribasim/ribasim/nodes/level_demand.py new file mode 100644 index 000000000..e65bd96d0 --- /dev/null +++ b/python/ribasim/ribasim/nodes/level_demand.py @@ -0,0 +1,19 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + LevelDemandStaticSchema, + LevelDemandTimeSchema, +) + +__all__ = ["Static", "Time"] + + +class Static(TableModel[LevelDemandStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Time(TableModel[LevelDemandTimeSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/linear_resistance.py b/python/ribasim/ribasim/nodes/linear_resistance.py new file mode 100644 index 000000000..5adb72af6 --- /dev/null +++ b/python/ribasim/ribasim/nodes/linear_resistance.py @@ -0,0 +1,13 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + LinearResistanceStaticSchema, +) + +__all__ = ["Static"] + + +class Static(TableModel[LinearResistanceStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/manning_resistance.py b/python/ribasim/ribasim/nodes/manning_resistance.py new file mode 100644 index 000000000..46feea8a0 --- /dev/null +++ b/python/ribasim/ribasim/nodes/manning_resistance.py @@ -0,0 +1,13 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + ManningResistanceStaticSchema, +) + +__all__ = ["Static"] + + +class Static(TableModel[ManningResistanceStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/outlet.py b/python/ribasim/ribasim/nodes/outlet.py new file mode 100644 index 000000000..8c6623cd9 --- /dev/null +++ b/python/ribasim/ribasim/nodes/outlet.py @@ -0,0 +1,13 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + OutletStaticSchema, +) + +__all__ = ["Static"] + + +class Static(TableModel[OutletStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/pid_control.py b/python/ribasim/ribasim/nodes/pid_control.py new file mode 100644 index 000000000..e359e849e --- /dev/null +++ b/python/ribasim/ribasim/nodes/pid_control.py @@ -0,0 +1,16 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import PidControlStaticSchema, PidControlTimeSchema + +__all__ = ["Static", "Time"] + + +class Static(TableModel[PidControlStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Time(TableModel[PidControlTimeSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/pump.py b/python/ribasim/ribasim/nodes/pump.py new file mode 100644 index 000000000..c2a9a2250 --- /dev/null +++ b/python/ribasim/ribasim/nodes/pump.py @@ -0,0 +1,13 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + PumpStaticSchema, +) + +__all__ = ["Static"] + + +class Static(TableModel[PumpStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/tabulated_rating_curve.py b/python/ribasim/ribasim/nodes/tabulated_rating_curve.py new file mode 100644 index 000000000..6afb46c5f --- /dev/null +++ b/python/ribasim/ribasim/nodes/tabulated_rating_curve.py @@ -0,0 +1,19 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + TabulatedRatingCurveStaticSchema, + TabulatedRatingCurveTimeSchema, +) + +__all__ = ["Static", "Time"] + + +class Static(TableModel[TabulatedRatingCurveStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Time(TableModel[TabulatedRatingCurveTimeSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/terminal.py b/python/ribasim/ribasim/nodes/terminal.py new file mode 100644 index 000000000..8982ad28a --- /dev/null +++ b/python/ribasim/ribasim/nodes/terminal.py @@ -0,0 +1,13 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + TerminalStaticSchema, +) + +__all__ = ["Static"] + + +class Static(TableModel[TerminalStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/nodes/user_demand.py b/python/ribasim/ribasim/nodes/user_demand.py new file mode 100644 index 000000000..d33192dc0 --- /dev/null +++ b/python/ribasim/ribasim/nodes/user_demand.py @@ -0,0 +1,19 @@ +from pandas import DataFrame + +from ribasim.input_base import TableModel +from ribasim.schemas import ( + UserDemandStaticSchema, + UserDemandTimeSchema, +) + +__all__ = ["Static", "Time"] + + +class Static(TableModel[UserDemandStaticSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) + + +class Time(TableModel[UserDemandTimeSchema]): + def __init__(self, **kwargs): + super().__init__(df=DataFrame(dict(**kwargs))) diff --git a/python/ribasim/ribasim/schemas.py b/python/ribasim/ribasim/schemas.py index 3a6c44e8a..8dd8c44be 100644 --- a/python/ribasim/ribasim/schemas.py +++ b/python/ribasim/ribasim/schemas.py @@ -12,18 +12,18 @@ class Config: class BasinProfileSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) area: Series[float] = pa.Field(nullable=False) level: Series[float] = pa.Field(nullable=False) class BasinStateSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) level: Series[float] = pa.Field(nullable=False) class BasinStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) drainage: Series[float] = pa.Field(nullable=True) potential_evaporation: Series[float] = pa.Field(nullable=True) infiltration: Series[float] = pa.Field(nullable=True) @@ -32,14 +32,14 @@ class BasinStaticSchema(_BaseSchema): class BasinSubgridSchema(_BaseSchema): - subgrid_id: Series[int] = pa.Field(nullable=False) - node_id: Series[int] = pa.Field(nullable=False) + subgrid_id: Series[int] = pa.Field(nullable=False, default=0) + node_id: Series[int] = pa.Field(nullable=False, default=0) basin_level: Series[float] = pa.Field(nullable=False) subgrid_level: Series[float] = pa.Field(nullable=False) class BasinTimeSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) time: Series[Timestamp] = pa.Field(nullable=False) drainage: Series[float] = pa.Field(nullable=True) potential_evaporation: Series[float] = pa.Field(nullable=True) @@ -49,67 +49,67 @@ class BasinTimeSchema(_BaseSchema): class DiscreteControlConditionSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) - listen_feature_type: Series[str] = pa.Field(nullable=True) - listen_feature_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) + listen_node_type: Series[str] = pa.Field(nullable=False) + listen_node_id: Series[int] = pa.Field(nullable=False, default=0) variable: Series[str] = pa.Field(nullable=False) greater_than: Series[float] = pa.Field(nullable=False) look_ahead: Series[float] = pa.Field(nullable=True) class DiscreteControlLogicSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) truth_state: Series[str] = pa.Field(nullable=False) control_state: Series[str] = pa.Field(nullable=False) class FlowBoundaryStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) flow_rate: Series[float] = pa.Field(nullable=False) class FlowBoundaryTimeSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) time: Series[Timestamp] = pa.Field(nullable=False) flow_rate: Series[float] = pa.Field(nullable=False) class FractionalFlowStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) fraction: Series[float] = pa.Field(nullable=False) control_state: Series[str] = pa.Field(nullable=True) class LevelBoundaryStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) level: Series[float] = pa.Field(nullable=False) class LevelBoundaryTimeSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) time: Series[Timestamp] = pa.Field(nullable=False) level: Series[float] = pa.Field(nullable=False) class LevelDemandStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) min_level: Series[float] = pa.Field(nullable=False) max_level: Series[float] = pa.Field(nullable=False) - priority: Series[int] = pa.Field(nullable=False) + priority: Series[int] = pa.Field(nullable=False, default=0) class LevelDemandTimeSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) time: Series[Timestamp] = pa.Field(nullable=False) min_level: Series[float] = pa.Field(nullable=False) max_level: Series[float] = pa.Field(nullable=False) - priority: Series[int] = pa.Field(nullable=False) + priority: Series[int] = pa.Field(nullable=False, default=0) class LinearResistanceStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) resistance: Series[float] = pa.Field(nullable=False) max_flow_rate: Series[float] = pa.Field(nullable=True) @@ -117,7 +117,7 @@ class LinearResistanceStaticSchema(_BaseSchema): class ManningResistanceStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) length: Series[float] = pa.Field(nullable=False) manning_n: Series[float] = pa.Field(nullable=False) @@ -127,7 +127,7 @@ class ManningResistanceStaticSchema(_BaseSchema): class OutletStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) flow_rate: Series[float] = pa.Field(nullable=False) min_flow_rate: Series[float] = pa.Field(nullable=True) @@ -137,10 +137,10 @@ class OutletStaticSchema(_BaseSchema): class PidControlStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) - listen_node_type: Series[str] = pa.Field(nullable=True) - listen_node_id: Series[int] = pa.Field(nullable=False) + listen_node_type: Series[str] = pa.Field(nullable=False) + listen_node_id: Series[int] = pa.Field(nullable=False, default=0) target: Series[float] = pa.Field(nullable=False) proportional: Series[float] = pa.Field(nullable=False) integral: Series[float] = pa.Field(nullable=False) @@ -149,9 +149,9 @@ class PidControlStaticSchema(_BaseSchema): class PidControlTimeSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) - listen_node_type: Series[str] = pa.Field(nullable=True) - listen_node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) + listen_node_type: Series[str] = pa.Field(nullable=False) + listen_node_id: Series[int] = pa.Field(nullable=False, default=0) time: Series[Timestamp] = pa.Field(nullable=False) target: Series[float] = pa.Field(nullable=False) proportional: Series[float] = pa.Field(nullable=False) @@ -161,7 +161,7 @@ class PidControlTimeSchema(_BaseSchema): class PumpStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) flow_rate: Series[float] = pa.Field(nullable=False) min_flow_rate: Series[float] = pa.Field(nullable=True) @@ -170,7 +170,7 @@ class PumpStaticSchema(_BaseSchema): class TabulatedRatingCurveStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) level: Series[float] = pa.Field(nullable=False) flow_rate: Series[float] = pa.Field(nullable=False) @@ -178,29 +178,29 @@ class TabulatedRatingCurveStaticSchema(_BaseSchema): class TabulatedRatingCurveTimeSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) time: Series[Timestamp] = pa.Field(nullable=False) level: Series[float] = pa.Field(nullable=False) flow_rate: Series[float] = pa.Field(nullable=False) class TerminalStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) class UserDemandStaticSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) active: Series[pa.BOOL] = pa.Field(nullable=True) demand: Series[float] = pa.Field(nullable=False) return_factor: Series[float] = pa.Field(nullable=False) min_level: Series[float] = pa.Field(nullable=False) - priority: Series[int] = pa.Field(nullable=False) + priority: Series[int] = pa.Field(nullable=False, default=0) class UserDemandTimeSchema(_BaseSchema): - node_id: Series[int] = pa.Field(nullable=False) + node_id: Series[int] = pa.Field(nullable=False, default=0) time: Series[Timestamp] = pa.Field(nullable=False) demand: Series[float] = pa.Field(nullable=False) return_factor: Series[float] = pa.Field(nullable=False) min_level: Series[float] = pa.Field(nullable=False) - priority: Series[int] = pa.Field(nullable=False) + priority: Series[int] = pa.Field(nullable=False, default=0) diff --git a/python/ribasim/ribasim/utils.py b/python/ribasim/ribasim/utils.py new file mode 100644 index 000000000..001b47862 --- /dev/null +++ b/python/ribasim/ribasim/utils.py @@ -0,0 +1,7 @@ +import re + + +def _pascal_to_snake(pascal_str): + # Insert a '_' before all uppercase letters that are not at the start of the string + # and convert the string to lowercase + return re.sub(r"(? Edge: +def edge() -> EdgeTable: a = (0.0, 0.0) b = (0.0, 1.0) c = (0.2, 0.5) @@ -15,19 +15,19 @@ def edge() -> Edge: df = gpd.GeoDataFrame( data={"from_node_id": [1, 1], "to_node_id": [2, 3]}, geometry=geometry ) - edge = Edge(df=df) + edge = EdgeTable(df=df) return edge def test_validation(edge): - assert isinstance(edge, Edge) + assert isinstance(edge, EdgeTable) with pytest.raises(ValidationError): df = gpd.GeoDataFrame( data={"from_node_id": [1, 1], "to_node_id": [None, 3]}, geometry=[None, None], ) - Edge(df=df) + EdgeTable(df=df) def test_edge_plot(edge): diff --git a/python/ribasim/tests/test_io.py b/python/ribasim/tests/test_io.py index a094ed6a5..7c6d34b51 100644 --- a/python/ribasim/tests/test_io.py +++ b/python/ribasim/tests/test_io.py @@ -1,4 +1,3 @@ -import pandas as pd import pytest import ribasim import tomli @@ -6,7 +5,7 @@ from pandas import DataFrame from pandas.testing import assert_frame_equal from pydantic import ValidationError -from ribasim import Pump, Terminal +from ribasim.nodes import pump, terminal def __assert_equal(a: DataFrame, b: DataFrame, is_network=False) -> None: @@ -35,6 +34,7 @@ def __assert_equal(a: DataFrame, b: DataFrame, is_network=False) -> None: return assert_frame_equal(a, b) +@pytest.mark.xfail(reason="Needs Model read implementation") def test_basic(basic, tmp_path): model_orig = basic toml_path = tmp_path / "basic/ribasim.toml" @@ -58,6 +58,7 @@ def test_basic(basic, tmp_path): assert model_loaded.basin.time.df is None +@pytest.mark.xfail(reason="Needs Model read implementation") def test_basic_arrow(basic_arrow, tmp_path): model_orig = basic_arrow model_orig.write(tmp_path / "basic_arrow/ribasim.toml") @@ -66,6 +67,7 @@ def test_basic_arrow(basic_arrow, tmp_path): __assert_equal(model_orig.basin.profile.df, model_loaded.basin.profile.df) +@pytest.mark.xfail(reason="Needs Model read implementation") def test_basic_transient(basic_transient, tmp_path): model_orig = basic_transient model_orig.write(tmp_path / "basic_transient/ribasim.toml") @@ -84,63 +86,32 @@ def test_basic_transient(basic_transient, tmp_path): assert time.df.shape == (1468, 7) +@pytest.mark.xfail(reason="Needs implementation") def test_pydantic(): - static_data_bad = pd.DataFrame(data={"node_id": [1, 2, 3]}) - - with pytest.raises(ValidationError): - Pump(static=static_data_bad) + pass + # static_data_bad = pd.DataFrame(data={"node_id": [1, 2, 3]}) + # test that it throws on missing flow_rate def test_repr(): - static_data = pd.DataFrame( - data={"node_id": [1, 2, 3], "flow_rate": [1.0, -1.0, 0.0]} - ) - - pump_1 = Pump(static=static_data) - pump_2 = Pump() + pump_static = pump.Static(flow_rate=[1.0, -1.0, 0.0]) - assert repr(pump_1) == "Pump(static)" - assert repr(pump_2) == "Pump()" + assert repr(pump_static).startswith("Pump / static") # Ensure _repr_html doesn't error - assert isinstance(pump_1.static._repr_html_(), str) - assert isinstance(pump_2.static._repr_html_(), str) + assert isinstance(pump_static._repr_html_(), str) def test_extra_columns(basic_transient): - terminal = Terminal( - static=pd.DataFrame( - data={ - "node_id": [1, 2, 3], - "meta_id": [-1, -2, -3], - } - ) - ) - assert "meta_id" in terminal.static.df.columns - assert (terminal.static.df.meta_id == [-1, -2, -3]).all() + terminal_static = terminal.Static(meta_id=[-1, -2, -3]) + assert "meta_id" in terminal_static.df.columns + assert (terminal_static.df.meta_id == [-1, -2, -3]).all() with pytest.raises(ValidationError): # Extra column "extra" needs "meta_" prefix - Terminal( - static=pd.DataFrame( - data={ - "node_id": [1, 2, 3], - "meta_id": [-1, -2, -3], - "extra": [-1, -2, -3], - } - ) - ) - - with pytest.raises(ValidationError): - # Missing required column "node_id" - Terminal( - static=pd.DataFrame( - data={ - "meta_id": [-1, -2, -3], - } - ) - ) + terminal.Static(meta_id=[-1, -2, -3], extra=[-1, -2, -3]) +@pytest.mark.xfail(reason="Needs Model read implementation") def test_sort(level_setpoint_with_minmax, tmp_path): model = level_setpoint_with_minmax table = model.discrete_control.condition @@ -150,7 +121,7 @@ def test_sort(level_setpoint_with_minmax, tmp_path): assert table.df.iloc[0]["greater_than"] == 15.0 assert table._sort_keys == [ "node_id", - "listen_feature_id", + "listen_node_id", "variable", "greater_than", ] @@ -168,6 +139,7 @@ def test_sort(level_setpoint_with_minmax, tmp_path): __assert_equal(table.df, table_loaded.df) +@pytest.mark.xfail(reason="Needs Model read implementation") def test_roundtrip(trivial, tmp_path): model1 = trivial model1dir = tmp_path / "model1" diff --git a/python/ribasim/tests/test_model.py b/python/ribasim/tests/test_model.py index 8af6be566..4b7d8023f 100644 --- a/python/ribasim/tests/test_model.py +++ b/python/ribasim/tests/test_model.py @@ -1,11 +1,13 @@ import re from sqlite3 import connect +import numpy as np import pandas as pd import pytest from pydantic import ValidationError -from ribasim import Model, Solver +from ribasim.config import Solver from ribasim.input_base import esc_id +from ribasim.model import Model from shapely import Point @@ -60,6 +62,7 @@ def test_exclude_unset(basic): assert d["solver"]["saveat"] == 86400.0 +@pytest.mark.xfail(reason="Needs implementation") def test_invalid_node_id(basic): model = basic @@ -97,6 +100,7 @@ def test_node_id_duplicate(basic): model.validate_model_node_field_ids() +@pytest.mark.xfail(reason="Needs implementation") def test_node_ids_misassigned(basic): model = basic @@ -111,6 +115,7 @@ def test_node_ids_misassigned(basic): model.validate_model_node_ids() +@pytest.mark.xfail(reason="Needs implementation") def test_node_ids_unsequential(basic): model = basic @@ -127,11 +132,12 @@ def test_node_ids_unsequential(basic): model.validate_model_node_field_ids() +@pytest.mark.xfail(reason="Needs Model read implementation") def test_tabulated_rating_curve_model(tabulated_rating_curve, tmp_path): model_orig = tabulated_rating_curve basin_area = tabulated_rating_curve.basin.area.df assert basin_area is not None - assert basin_area.geometry.geom_type[0] == "Polygon" + assert basin_area.geometry.geom_type.iloc[0] == "Polygon" model_orig.write(tmp_path / "tabulated_rating_curve/ribasim.toml") model_new = Model.read(tmp_path / "tabulated_rating_curve/ribasim.toml") pd.testing.assert_series_equal( @@ -147,32 +153,24 @@ def test_plot(discrete_control_of_pid_control): def test_write_adds_fid_in_tables(basic, tmp_path): model_orig = basic # for node an explicit index was provided - nrow = len(model_orig.network.node.df) - assert model_orig.network.node.df.index.name == "fid" - assert model_orig.network.node.df.index.equals( - pd.RangeIndex(start=1, stop=nrow + 1) - ) + nrow = len(model_orig.basin.node.df) + assert model_orig.basin.node.df.index.name is None + assert model_orig.basin.node.df.index.equals(pd.Index(np.full(nrow, 0))) # for edge no index was provided, but it still needs to write it to file - nrow = len(model_orig.network.edge.df) - assert model_orig.network.edge.df.index.name is None - assert model_orig.network.edge.df.index.equals(pd.RangeIndex(start=0, stop=nrow)) + nrow = len(model_orig.edge.df) + assert model_orig.edge.df.index.name is None + assert model_orig.edge.df.index.equals(pd.Index(np.full(nrow, 0))) model_orig.write(tmp_path / "basic/ribasim.toml") with connect(tmp_path / "basic/database.gpkg") as connection: query = f"select * from {esc_id('Basin / profile')}" df = pd.read_sql_query(query, connection) assert "fid" in df.columns - fids = df["fid"] - assert fids.equals(pd.Series(range(1, len(fids) + 1))) query = "select fid from Node" df = pd.read_sql_query(query, connection) assert "fid" in df.columns - fids = df["fid"] - assert fids.equals(pd.Series(range(1, len(fids) + 1))) query = "select fid from Edge" df = pd.read_sql_query(query, connection) assert "fid" in df.columns - fids = df["fid"] - assert fids.equals(pd.Series(range(0, len(fids)))) diff --git a/python/ribasim/tests/test_utils.py b/python/ribasim/tests/test_utils.py deleted file mode 100644 index ba04ce27c..000000000 --- a/python/ribasim/tests/test_utils.py +++ /dev/null @@ -1,35 +0,0 @@ -import geopandas as gpd -import numpy as np -import pandas as pd -from numpy.testing import assert_array_equal -from ribasim.geometry.node import Node -from shapely import LineString - - -def test_utils(): - node_type = ("Basin", "LinearResistance", "Basin") - - xy = np.array([(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)]) - - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node = Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - from_id = np.array([1, 2], dtype=np.int64) - to_id = np.array([2, 3], dtype=np.int64) - - lines = node.geometry_from_connectivity(from_id, to_id) - assert lines[0].equals(LineString([xy[from_id[0] - 1], xy[to_id[0] - 1]])) - assert lines[1].equals(LineString([xy[from_id[1] - 1], xy[to_id[1] - 1]])) - - from_id_, to_id_ = node.connectivity_from_geometry(lines) - - assert_array_equal(from_id, from_id_) - assert_array_equal(to_id, to_id_) diff --git a/python/ribasim_testmodels/ribasim_testmodels/__init__.py b/python/ribasim_testmodels/ribasim_testmodels/__init__.py index 7004e5aa1..43e3575be 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/__init__.py +++ b/python/ribasim_testmodels/ribasim_testmodels/__init__.py @@ -2,7 +2,7 @@ from collections.abc import Callable -import ribasim +from ribasim.model import Model import ribasim_testmodels from ribasim_testmodels.allocation import ( @@ -93,7 +93,7 @@ ] # provide a mapping from model name to its constructor, so we can iterate over all models -constructors: dict[str, Callable[[], ribasim.Model]] = {} +constructors: dict[str, Callable[[], Model]] = {} for model_name_model in __all__: model_name = model_name_model.removesuffix("_model") model_constructor = getattr(ribasim_testmodels, model_name_model) diff --git a/python/ribasim_testmodels/ribasim_testmodels/allocation.py b/python/ribasim_testmodels/ribasim_testmodels/allocation.py index 0c51d0f2b..8a1ef3e81 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/allocation.py +++ b/python/ribasim_testmodels/ribasim_testmodels/allocation.py @@ -1,1761 +1,887 @@ -import geopandas as gpd +from typing import Any + import numpy as np import pandas as pd -import ribasim - - -def user_demand_model(): +from ribasim.config import Allocation, Node, Solver +from ribasim.input_base import TableModel +from ribasim.model import Model +from ribasim.nodes import ( + basin, + discrete_control, + flow_boundary, + fractional_flow, + level_demand, + linear_resistance, + outlet, + pump, + tabulated_rating_curve, + user_demand, +) +from shapely.geometry import Point + + +def user_demand_model() -> Model: """Create a UserDemand test model with static and dynamic UserDemand on the same basin.""" - # Set up the nodes: - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, 0.5), # 2: UserDemand - (1.0, -0.5), # 3: UserDemand - (2.0, 0.0), # 4: Terminal - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["Basin", "UserDemand", "UserDemand", "Terminal"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", + solver=Solver(algorithm="Tsit5"), ) - # Setup the edges: - from_id = np.array([1, 1, 2, 3], dtype=np.int64) - to_id = np.array([2, 3, 4, 4], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) + model.basin.add( + Node(1, Point(0, 0)), + [basin.Profile(area=1000.0, level=[0.0, 1.0]), basin.State(level=[1.0])], ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": 1, - "area": 1000.0, - "level": [0.0, 1.0], - } + model.user_demand.add( + Node(2, Point(1, 0.5)), + [ + user_demand.Static( + demand=[1e-4], return_factor=0.9, min_level=0.9, priority=1 + ) + ], ) - - state = pd.DataFrame(data={"node_id": [1], "level": 1.0}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup the UserDemands: - user_demand = ribasim.UserDemand( - static=pd.DataFrame( - data={ - "node_id": [2], - "demand": 1e-4, - "return_factor": 0.9, - "min_level": 0.9, - "priority": 1, - } - ), - time=pd.DataFrame( - data={ - "node_id": 3, - "time": [ + model.user_demand.add( + Node(3, Point(1, -0.5)), + [ + user_demand.Time( + time=[ "2020-06-01 00:00:00", "2020-06-01 01:00:00", "2020-07-01 00:00:00", "2020-07-01 01:00:00", ], - "demand": [0.0, 3e-4, 3e-4, 0.0], - "return_factor": 0.4, - "min_level": 0.5, - "priority": 1, - } - ), - ) - - # Setup the terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [4], - } - ) + demand=[0.0, 3e-4, 3e-4, 0.0], + return_factor=0.4, + min_level=0.5, + priority=1, + ) + ], ) + model.terminal.add(Node(4, Point(2, 0))) - solver = ribasim.Solver(algorithm="Tsit5") - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - user_demand=user_demand, - terminal=terminal, - solver=solver, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", - ) + model.edge.add(model.basin[1], model.user_demand[2], "flow") + model.edge.add(model.basin[1], model.user_demand[3], "flow") + model.edge.add(model.user_demand[2], model.terminal[4], "flow") + model.edge.add(model.user_demand[3], model.terminal[4], "flow") return model -def subnetwork_model(): +def subnetwork_model() -> Model: """Create a UserDemand testmodel representing a subnetwork. This model is merged into main_network_with_subnetworks_model. """ - # Setup the nodes: - xy = np.array( - [ - (3.0, 1.0), # 1: FlowBoundary - (2.0, 1.0), # 2: Basin - (1.0, 1.0), # 3: Outlet - (0.0, 1.0), # 4: Terminal - (2.0, 2.0), # 5: Pump - (2.0, 3.0), # 6: Basin - (1.0, 3.0), # 7: Outlet - (0.0, 3.0), # 8: Basin - (2.0, 5.0), # 9: Terminal - (2.0, 0.0), # 10: UserDemand - (3.0, 3.0), # 11: UserDemand - (0.0, 4.0), # 12: UserDemand - (2.0, 4.0), # 13: Outlet - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "FlowBoundary", - "Basin", - "Outlet", - "Terminal", - "Pump", - "Basin", - "Outlet", - "Basin", - "Terminal", - "UserDemand", - "UserDemand", - "UserDemand", - "Outlet", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type, "subnetwork_id": 2}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array( - [1, 2, 3, 2, 2, 5, 6, 7, 6, 8, 6, 13, 10, 11, 12], dtype=np.int64 - ) - to_id = np.array([2, 3, 4, 10, 5, 6, 7, 8, 11, 12, 13, 9, 2, 6, 8], dtype=np.int64) - subnetwork_id = len(from_id) * [None] - subnetwork_id[0] = 2 - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - "subnetwork_id": subnetwork_id, - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={"node_id": [2, 2, 6, 6, 8, 8], "area": 100000.0, "level": 3 * [0.0, 1.0]} - ) - - state = pd.DataFrame(data={"node_id": [2, 6, 8], "level": 10.0}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup the flow boundary: - flow_boundary = ribasim.FlowBoundary( - time=pd.DataFrame( - data={ - "node_id": 1, - "flow_rate": np.arange(10, 0, -2), - "time": pd.to_datetime([f"2020-{i}-1 00:00:00" for i in range(1, 6)]), - }, - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-04-01 00:00:00", + allocation=Allocation(use_allocation=True, timestep=86400), ) - # Setup the UserDemand: - user_demand = ribasim.UserDemand( - static=pd.DataFrame( - data={ - "node_id": [10, 11, 12], - "demand": [4.0, 5.0, 3.0], - "return_factor": 0.9, - "min_level": 0.9, - "priority": [2, 1, 2], - } - ), - ) + basin_data: list[TableModel[Any]] = [ + basin.Profile(area=100000.0, level=[0.0, 1.0]), + basin.State(level=[10.0]), + ] + outlet_data: list[TableModel[Any]] = [ + outlet.Static(flow_rate=[3.0], max_flow_rate=3.0) + ] - # Setup the pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [5], - "flow_rate": [4.0], - "max_flow_rate": [4.0], - } - ) + model.flow_boundary.add( + Node(1, Point(3, 1), subnetwork_id=2), + [ + flow_boundary.Time( + time=pd.date_range(start="2020-01", end="2020-05", freq="MS"), + flow_rate=np.arange(10, 0, -2), + ) + ], ) - - # Setup the outlets: - outlet = ribasim.Outlet( - static=pd.DataFrame( - data={"node_id": [3, 7, 13], "flow_rate": 3.0, "max_flow_rate": 3.0} - ) + model.basin.add(Node(2, Point(2, 1), subnetwork_id=2), basin_data) + model.outlet.add(Node(3, Point(1, 1), subnetwork_id=2), outlet_data) + model.terminal.add(Node(4, Point(0, 1), subnetwork_id=2)) + model.pump.add( + Node(5, Point(2, 2), subnetwork_id=2), + [pump.Static(flow_rate=[4.0], max_flow_rate=4.0)], + ) + model.basin.add(Node(6, Point(2, 3), subnetwork_id=2), basin_data) + model.outlet.add(Node(7, Point(1, 3), subnetwork_id=2), outlet_data) + model.basin.add(Node(8, Point(0, 3), subnetwork_id=2), basin_data) + model.terminal.add(Node(9, Point(2, 5), subnetwork_id=2)) + model.user_demand.add( + Node(10, Point(2, 0), subnetwork_id=2), + [ + user_demand.Static( + demand=[4.0], return_factor=0.9, min_level=0.9, priority=2 + ) + ], ) - - # Setup the terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [4, 9], - } - ) + model.user_demand.add( + Node(11, Point(3, 3), subnetwork_id=2), + [ + user_demand.Static( + demand=[5.0], return_factor=0.9, min_level=0.9, priority=1 + ) + ], ) - - # Setup allocation: - allocation = ribasim.Allocation(use_allocation=True, timestep=86400) - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - user_demand=user_demand, - flow_boundary=flow_boundary, - pump=pump, - outlet=outlet, - terminal=terminal, - allocation=allocation, - starttime="2020-01-01 00:00:00", - endtime="2020-04-01 00:00:00", + model.user_demand.add( + Node(12, Point(0, 4), subnetwork_id=2), + [ + user_demand.Static( + demand=[3.0], return_factor=0.9, min_level=0.9, priority=2 + ) + ], ) + model.outlet.add(Node(13, Point(2, 4), subnetwork_id=2), outlet_data) + + model.edge.add(model.flow_boundary[1], model.basin[2], "flow", subnetwork_id=2) + model.edge.add(model.basin[2], model.outlet[3], "flow") + model.edge.add(model.outlet[3], model.terminal[4], "flow") + model.edge.add(model.basin[2], model.user_demand[10], "flow") + model.edge.add(model.basin[2], model.pump[5], "flow") + model.edge.add(model.pump[5], model.basin[6], "flow") + model.edge.add(model.basin[6], model.outlet[7], "flow") + model.edge.add(model.outlet[7], model.basin[8], "flow") + model.edge.add(model.basin[6], model.user_demand[11], "flow") + model.edge.add(model.basin[8], model.user_demand[12], "flow") + model.edge.add(model.basin[6], model.outlet[13], "flow") + model.edge.add(model.outlet[13], model.terminal[9], "flow") + model.edge.add(model.user_demand[10], model.basin[2], "flow") + model.edge.add(model.user_demand[11], model.basin[6], "flow") + model.edge.add(model.user_demand[12], model.basin[8], "flow") return model -def looped_subnetwork_model(): +def looped_subnetwork_model() -> Model: """Create a UserDemand testmodel representing a subnetwork containing a loop in the topology. This model is merged into main_network_with_subnetworks_model. """ - # Setup the nodes: - xy = np.array( - [ - (0.0, 0.0), # 1: UserDemand - (0.0, 1.0), # 2: Basin - (-1.0, 1.0), # 3: Outlet - (-2.0, 1.0), # 4: Terminal - (2.0, 1.0), # 5: FlowBoundary - (0.0, 2.0), # 6: Pump - (-2.0, 3.0), # 7: Basin - (-1.0, 3.0), # 8: Outlet - (0.0, 3.0), # 9: Basin - (1.0, 3.0), # 10: Outlet - (2.0, 3.0), # 11: Basin - (-2.0, 4.0), # 12: UserDemand - (0.0, 4.0), # 13: TabulatedRatingCurve - (2.0, 4.0), # 14: TabulatedRatingCurve - (0.0, 5.0), # 15: Basin - (1.0, 5.0), # 16: Pump - (2.0, 5.0), # 17: Basin - (-1.0, 6.0), # 18: UserDemand - (0.0, 6.0), # 19: TabulatedRatingCurve - (2.0, 6.0), # 20: UserDemand - (0.0, 7.0), # 21: Basin - (0.0, 8.0), # 22: Outlet - (0.0, 9.0), # 23: Terminal - (3.0, 3.0), # 24: UserDemand - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = [ - "UserDemand", - "Basin", - "Outlet", - "Terminal", - "FlowBoundary", - "Pump", - "Basin", - "Outlet", - "Basin", - "Outlet", - "Basin", - "UserDemand", - "TabulatedRatingCurve", - "TabulatedRatingCurve", - "Basin", - "Pump", - "Basin", - "UserDemand", - "TabulatedRatingCurve", - "UserDemand", - "Basin", - "Outlet", - "Terminal", - "UserDemand", - ] + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", + allocation=Allocation(use_allocation=True, timestep=86400), + ) - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type, "subnetwork_id": 2}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + basin_data: list[TableModel[Any]] = [ + basin.Profile(area=1000.0, level=[0.0, 1.0]), + basin.State(level=[1.0]), + ] + tabulated_rating_curve_data = tabulated_rating_curve.Static( + level=[0.0, 1.0], flow_rate=[0.0, 2.0] ) + outlet_data = outlet.Static(flow_rate=[3e-3], max_flow_rate=3.0) - # Setup the edges: - from_id = np.array( + model.user_demand.add( + Node(1, Point(0, 0), subnetwork_id=2), [ - 5, - 2, - 3, - 2, - 2, - 6, - 9, - 8, - 7, - 9, - 13, - 15, - 16, - 17, - 15, - 19, - 15, - 18, - 21, - 22, - 9, - 10, - 11, - 14, - 1, - 12, - 20, - 11, - 24, + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=2 + ) ], - dtype=np.int64, ) - to_id = np.array( + model.basin.add(Node(2, Point(0, 1), subnetwork_id=2), basin_data) + model.outlet.add(Node(3, Point(-1, 1), subnetwork_id=2), [outlet_data]) + model.terminal.add(Node(4, Point(-2, 1), subnetwork_id=2)) + model.flow_boundary.add( + Node(5, Point(2, 1), subnetwork_id=2), + [flow_boundary.Static(flow_rate=[4.5e-3])], + ) + model.pump.add( + Node(6, Point(0, 2), subnetwork_id=2), + [pump.Static(flow_rate=[4e-3], max_flow_rate=4e-3)], + ) + model.basin.add(Node(7, Point(-2, 3), subnetwork_id=2), basin_data) + model.outlet.add(Node(8, Point(-1, 3), subnetwork_id=2), [outlet_data]) + model.basin.add(Node(9, Point(0, 3), subnetwork_id=2), basin_data) + model.outlet.add(Node(10, Point(1, 3), subnetwork_id=2), [outlet_data]) + model.basin.add(Node(11, Point(2, 3), subnetwork_id=2), basin_data) + model.user_demand.add( + Node(12, Point(-2, 4), subnetwork_id=2), [ - 2, - 3, - 4, - 1, - 6, - 9, - 8, - 7, - 12, - 13, - 15, - 16, - 17, - 20, - 19, - 21, - 18, - 21, - 22, - 23, - 10, - 11, - 14, - 17, - 2, - 7, - 17, - 24, - 11, + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=1 + ) ], - dtype=np.int64, - ) - lines = node.geometry_from_connectivity(from_id, to_id) - subnetwork_id = len(from_id) * [None] - subnetwork_id[0] = 2 - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - "subnetwork_id": subnetwork_id, - }, - geometry=lines, - crs="EPSG:28992", - ) ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [2, 2, 7, 7, 9, 9, 11, 11, 15, 15, 17, 17, 21, 21], - "area": 1000.0, - "level": 7 * [0.0, 1.0], - } + model.tabulated_rating_curve.add( + Node(13, Point(0, 4), subnetwork_id=2), [tabulated_rating_curve_data] ) - - state = pd.DataFrame(data={"node_id": [2, 7, 9, 11, 15, 17, 21], "level": 1.0}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup the flow boundary: - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame(data={"node_id": [5], "flow_rate": [4.5e-3]}) + model.tabulated_rating_curve.add( + Node(14, Point(2, 4), subnetwork_id=2), [tabulated_rating_curve_data] ) - - # Setup the user_demands: - user_demand = ribasim.UserDemand( - static=pd.DataFrame( - data={ - "node_id": [1, 12, 18, 20, 24], - "demand": 1.0e-3, - "return_factor": 0.9, - "min_level": 0.9, - "priority": [2, 1, 3, 3, 2], - } - ) + model.basin.add(Node(15, Point(0, 5), subnetwork_id=2), basin_data) + model.pump.add( + Node(16, Point(1, 5), subnetwork_id=2), + [pump.Static(flow_rate=[4e-3], max_flow_rate=4e-3)], ) - - # Setup the pumps: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [6, 16], - "flow_rate": 4.0e-3, - "max_flow_rate": 4.0e-3, - } - ) + model.basin.add(Node(17, Point(2, 5), subnetwork_id=2), basin_data) + model.user_demand.add( + Node(18, Point(-1, 6), subnetwork_id=2), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=3 + ) + ], ) - - # Setup the outlets: - outlet = ribasim.Outlet( - static=pd.DataFrame( - data={"node_id": [3, 8, 10, 22], "flow_rate": 3.0e-3, "max_flow_rate": 3.0} - ) + model.tabulated_rating_curve.add( + Node(19, Point(0, 6), subnetwork_id=2), [tabulated_rating_curve_data] ) - - # Setup the tabulated rating curves - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [13, 13, 14, 14, 19, 19], - "level": 3 * [0.0, 1.0], - "flow_rate": 3 * [0.0, 2.0], - } - ) + model.user_demand.add( + Node(20, Point(2, 6), subnetwork_id=2), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=3 + ) + ], ) - - # Setup the terminals: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [4, 23], - } - ) + model.basin.add(Node(21, Point(0, 7), subnetwork_id=2), basin_data) + model.outlet.add(Node(22, Point(0, 8), subnetwork_id=2), [outlet_data]) + model.terminal.add(Node(23, Point(0, 9), subnetwork_id=2)) + model.user_demand.add( + Node(24, Point(3, 3), subnetwork_id=2), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=2 + ) + ], ) - # Setup allocation: - allocation = ribasim.Allocation(use_allocation=True, timestep=86400) - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - flow_boundary=flow_boundary, - user_demand=user_demand, - pump=pump, - outlet=outlet, - tabulated_rating_curve=rating_curve, - terminal=terminal, - allocation=allocation, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", - ) + model.edge.add(model.flow_boundary[5], model.basin[2], "flow", subnetwork_id=2) + model.edge.add(model.basin[2], model.outlet[3], "flow") + model.edge.add(model.outlet[3], model.terminal[4], "flow") + model.edge.add(model.basin[2], model.user_demand[1], "flow") + model.edge.add(model.basin[2], model.pump[6], "flow") + model.edge.add(model.pump[6], model.basin[9], "flow") + model.edge.add(model.basin[9], model.outlet[8], "flow") + model.edge.add(model.outlet[8], model.basin[7], "flow") + model.edge.add(model.basin[7], model.user_demand[12], "flow") + model.edge.add(model.basin[9], model.tabulated_rating_curve[13], "flow") + model.edge.add(model.tabulated_rating_curve[13], model.basin[15], "flow") + model.edge.add(model.basin[15], model.pump[16], "flow") + model.edge.add(model.pump[16], model.basin[17], "flow") + model.edge.add(model.basin[17], model.user_demand[20], "flow") + model.edge.add(model.basin[15], model.tabulated_rating_curve[19], "flow") + model.edge.add(model.tabulated_rating_curve[19], model.basin[21], "flow") + model.edge.add(model.basin[15], model.user_demand[18], "flow") + model.edge.add(model.user_demand[18], model.basin[21], "flow") + model.edge.add(model.basin[21], model.outlet[22], "flow") + model.edge.add(model.outlet[22], model.terminal[23], "flow") + model.edge.add(model.basin[9], model.outlet[10], "flow") + model.edge.add(model.outlet[10], model.basin[11], "flow") + model.edge.add(model.basin[11], model.tabulated_rating_curve[14], "flow") + model.edge.add(model.tabulated_rating_curve[14], model.basin[17], "flow") + model.edge.add(model.user_demand[1], model.basin[2], "flow") + model.edge.add(model.user_demand[12], model.basin[7], "flow") + model.edge.add(model.user_demand[20], model.basin[17], "flow") + model.edge.add(model.basin[11], model.user_demand[24], "flow") + model.edge.add(model.user_demand[24], model.basin[11], "flow") return model -def minimal_subnetwork_model(): +def minimal_subnetwork_model() -> Model: """Create a subnetwork that is minimal with non-trivial allocation.""" - xy = np.array( - [ - (0.0, 0.0), # 1: FlowBoundary - (0.0, 1.0), # 2: Basin - (0.0, 2.0), # 3: Pump - (0.0, 3.0), # 4: Basin - (-1.0, 4.0), # 5: UserDemand - (1.0, 4.0), # 6: UserDemand - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = ["FlowBoundary", "Basin", "Pump", "Basin", "UserDemand", "UserDemand"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type, "subnetwork_id": 2}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", + allocation=Allocation(use_allocation=True, timestep=86400), ) - # Setup the edges: - from_id = np.array( - [1, 2, 3, 4, 4, 5, 6], - dtype=np.int64, - ) - to_id = np.array( - [2, 3, 4, 5, 6, 4, 4], - dtype=np.int64, - ) - subnetwork_id = len(from_id) * [None] - subnetwork_id[0] = 2 - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - "subnetwork_id": subnetwork_id, - }, - geometry=lines, - crs="EPSG:28992", - ) - ) + basin_data: list[TableModel[Any]] = [ + basin.Profile(area=1000.0, level=[0.0, 1.0]), + basin.State(level=[1.0]), + ] - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [2, 2, 4, 4], - "area": 1000.0, - "level": 2 * [0.0, 1.0], - } + model.flow_boundary.add( + Node(1, Point(0, 0), subnetwork_id=2), + [flow_boundary.Static(flow_rate=[2.0e-3])], ) - - state = pd.DataFrame(data={"node_id": [2, 4], "level": 1.0}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup the flow boundary: - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame( - data={ - "node_id": [1], - "flow_rate": 2.0e-3, - } - ) + model.basin.add(Node(2, Point(0, 1), subnetwork_id=2), basin_data) + model.pump.add( + Node(3, Point(0, 2), subnetwork_id=2), + [pump.Static(flow_rate=[4e-3], max_flow_rate=4e-3)], ) - - # Setup the pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [3], - "flow_rate": [4.0e-3], - "max_flow_rate": [4.0e-3], - } - ) + model.basin.add(Node(4, Point(0, 3), subnetwork_id=2), basin_data) + model.user_demand.add( + Node(5, Point(-1, 4), subnetwork_id=2), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=1 + ) + ], ) - - # Setup the UserDemand: - user_demand = ribasim.UserDemand( - static=pd.DataFrame( - data={ - "node_id": [5], - "demand": 1.0e-3, - "return_factor": 0.9, - "min_level": 0.9, - "priority": 1, - } - ), - time=pd.DataFrame( - data={ - "time": ["2020-01-01 00:00:00", "2021-01-01 00:00:00"], - "node_id": 6, - "demand": [1.0e-3, 2.0e-3], - "return_factor": 0.9, - "min_level": 0.9, - "priority": 1, - } - ), + model.user_demand.add( + Node(6, Point(1, 4), subnetwork_id=2), + [ + user_demand.Time( + time=["2020-01-01 00:00:00", "2021-01-01 00:00:00"], + demand=[1e-3, 2e-3], + return_factor=0.9, + min_level=0.9, + priority=1, + ) + ], ) - # Setup allocation: - allocation = ribasim.Allocation(use_allocation=True, timestep=86400) - - model = ribasim.Model( - network=ribasim.Network( - node=node, - edge=edge, - ), - basin=basin, - flow_boundary=flow_boundary, - pump=pump, - user_demand=user_demand, - allocation=allocation, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", - ) + model.edge.add(model.flow_boundary[1], model.basin[2], "flow", subnetwork_id=2) + model.edge.add(model.basin[2], model.pump[3], "flow") + model.edge.add(model.pump[3], model.basin[4], "flow") + model.edge.add(model.basin[4], model.user_demand[5], "flow") + model.edge.add(model.basin[4], model.user_demand[6], "flow") + model.edge.add(model.user_demand[5], model.basin[4], "flow") + model.edge.add(model.user_demand[6], model.basin[4], "flow") return model -def fractional_flow_subnetwork_model(): +def fractional_flow_subnetwork_model() -> Model: """Create a small subnetwork that contains fractional flow nodes. This model is merged into main_network_with_subnetworks_model. """ - xy = np.array( - [ - (0.0, 0.0), # 1: FlowBoundary - (0.0, 1.0), # 2: Basin - (0.0, 2.0), # 3: TabulatedRatingCurve - (-1.0, 3.0), # 4: FractionalFlow - (-2.0, 4.0), # 5: Basin - (-3.0, 5.0), # 6: UserDemand - (1.0, 3.0), # 7: FractionalFlow - (2.0, 4.0), # 8: Basin - (3.0, 5.0), # 9: UserDemand - (-1.0, 2.0), # 10: DiscreteControl - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "FlowBoundary", - "Basin", - "TabulatedRatingCurve", - "FractionalFlow", - "Basin", - "UserDemand", - "FractionalFlow", - "Basin", - "UserDemand", - "DiscreteControl", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type, "subnetwork_id": 2}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", + allocation=Allocation(use_allocation=True, timestep=86400), ) - # Setup the edges: - from_id = np.array( - [1, 2, 3, 4, 5, 6, 3, 7, 8, 9, 10, 10], - dtype=np.int64, - ) - to_id = np.array( - [2, 3, 4, 5, 6, 5, 7, 8, 9, 8, 4, 7], - dtype=np.int64, - ) - subnetwork_id = len(from_id) * [None] - subnetwork_id[0] = 2 - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": (len(from_id) - 2) * ["flow"] + 2 * ["control"], - "subnetwork_id": subnetwork_id, - }, - geometry=lines, - crs="EPSG:28992", - ) - ) + basin_data: list[TableModel[Any]] = [ + basin.Profile(area=1000.0, level=[0.0, 1.0]), + basin.State(level=[1.0]), + ] - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [2, 2, 5, 5, 8, 8], - "area": 1000.0, - "level": 3 * [0.0, 1.0], - } + model.flow_boundary.add( + Node(1, Point(0, 0), subnetwork_id=2), + [ + flow_boundary.Time( + time=["2020-01-01 00:00:00", "2021-01-01 00:00:00"], + flow_rate=[2.0e-3, 4.0e-3], + ) + ], ) - - state = pd.DataFrame(data={"node_id": [2, 5, 8], "level": 1.0}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup the flow boundary: - flow_boundary = ribasim.FlowBoundary( - time=pd.DataFrame( - data={ - "node_id": [1, 1], - "flow_rate": [2.0e-3, 4.0e-3], - "time": ["2020-01-01 00:00:00", "2021-01-01 00:00:00"], - } - ) + model.basin.add(Node(2, Point(0, 1), subnetwork_id=2), basin_data) + model.tabulated_rating_curve.add( + Node(3, Point(0, 2), subnetwork_id=2), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 1e-4])], ) - - # Setup the tabulated rating curve: - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [3, 3], - "level": [0.0, 1.0], - "flow_rate": [0.0, 1e-4], - } - ) + model.fractional_flow.add( + Node(4, Point(-1, 3), subnetwork_id=2), + [fractional_flow.Static(fraction=[0.25, 0.75], control_state=["A", "B"])], ) - - # Setup the UserDemands - user_demand = ribasim.UserDemand( - static=pd.DataFrame( - data={ - "node_id": [6], - "demand": 1.0e-3, - "return_factor": 0.9, - "min_level": 0.9, - "priority": 1, - } - ), - time=pd.DataFrame( - data={ - "time": ["2020-01-01 00:00:00", "2021-01-01 00:00:00"], - "node_id": 9, - "demand": [1.0e-3, 2.0e-3], - "return_factor": 0.9, - "min_level": 0.9, - "priority": 1, - } - ), + model.basin.add(Node(5, Point(-2, 4), subnetwork_id=2), basin_data) + model.user_demand.add( + Node(6, Point(-3, 5), subnetwork_id=2), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=1 + ) + ], ) - - # Setup allocation: - allocation = ribasim.Allocation(use_allocation=True, timestep=86400) - - # Setup fractional flows: - fractional_flow = ribasim.FractionalFlow( - static=pd.DataFrame( - data={ - "node_id": [4, 7, 4, 7], - "fraction": [0.25, 0.75, 0.75, 0.25], - "control_state": ["A", "A", "B", "B"], - } - ) + model.fractional_flow.add( + Node(7, Point(1, 3), subnetwork_id=2), + [fractional_flow.Static(fraction=[0.75, 0.25], control_state=["A", "B"])], ) - - # Setup discrete control: - condition = pd.DataFrame( - data={ - "node_id": [10], - "listen_feature_id": [1], - "variable": "flow_rate", - "greater_than": [3.0e-3], - } + model.basin.add(Node(8, Point(2, 4), subnetwork_id=2), basin_data) + model.user_demand.add( + Node(9, Point(3, 5), subnetwork_id=2), + [ + user_demand.Time( + time=["2020-01-01 00:00:00", "2021-01-01 00:00:00"], + demand=[1e-3, 2e-3], + return_factor=0.9, + min_level=0.9, + priority=1, + ) + ], ) - - logic = pd.DataFrame( - data={ - "node_id": [10, 10], - "truth_state": ["F", "T"], - "control_state": ["A", "B"], - } + model.discrete_control.add( + Node(10, Point(-1, 2), subnetwork_id=2), + [ + discrete_control.Condition( + listen_node_type="FlowBoundary", + listen_node_id=[1], + variable="flow_rate", + greater_than=3e-3, + ), + discrete_control.Logic(truth_state=["F", "T"], control_state=["A", "B"]), + ], ) - discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) - - model = ribasim.Model( - network=ribasim.Network( - node=node, - edge=edge, - ), - basin=basin, - flow_boundary=flow_boundary, - tabulated_rating_curve=rating_curve, - user_demand=user_demand, - allocation=allocation, - fractional_flow=fractional_flow, - discrete_control=discrete_control, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", - ) + model.edge.add(model.flow_boundary[1], model.basin[2], "flow", subnetwork_id=2) + model.edge.add(model.basin[2], model.tabulated_rating_curve[3], "flow") + model.edge.add(model.tabulated_rating_curve[3], model.fractional_flow[4], "flow") + model.edge.add(model.fractional_flow[4], model.basin[5], "flow") + model.edge.add(model.basin[5], model.user_demand[6], "flow") + model.edge.add(model.user_demand[6], model.basin[5], "flow") + model.edge.add(model.tabulated_rating_curve[3], model.fractional_flow[7], "flow") + model.edge.add(model.fractional_flow[7], model.basin[8], "flow") + model.edge.add(model.basin[8], model.user_demand[9], "flow") + model.edge.add(model.user_demand[9], model.basin[8], "flow") + model.edge.add(model.discrete_control[10], model.fractional_flow[4], "control") + model.edge.add(model.discrete_control[10], model.fractional_flow[7], "control") return model -def allocation_example_model(): +def allocation_example_model() -> Model: """Generate a model that is used as an example of allocation in the docs.""" - xy = np.array( - [ - (0.0, 0.0), # 1: FlowBoundary - (1.0, 0.0), # 2: Basin - (1.0, 1.0), # 3: UserDemand - (2.0, 0.0), # 4: LinearResistance - (3.0, 0.0), # 5: Basin - (3.0, 1.0), # 6: UserDemand - (4.0, 0.0), # 7: TabulatedRatingCurve - (4.5, 0.0), # 8: FractionalFlow - (4.5, 0.5), # 9: FractionalFlow - (5.0, 0.0), # 10: Terminal - (4.5, 0.25), # 11: DiscreteControl - (4.5, 1.0), # 12: Basin - (5.0, 1.0), # 13: UserDemand - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "FlowBoundary", - "Basin", - "UserDemand", - "LinearResistance", - "Basin", - "UserDemand", - "TabulatedRatingCurve", - "FractionalFlow", - "FractionalFlow", - "Terminal", - "DiscreteControl", - "Basin", - "UserDemand", - ] - - # All nodes belong to allocation network id 2 - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type, "subnetwork_id": 2}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-01-20 00:00:00", + allocation=Allocation(use_allocation=True, timestep=86400), ) - from_id = np.array( - [1, 2, 2, 4, 5, 5, 7, 3, 6, 7, 8, 9, 12, 13, 11, 11], - dtype=np.int64, - ) - to_id = np.array( - [2, 3, 4, 5, 6, 7, 8, 2, 5, 9, 10, 12, 13, 10, 8, 9], - dtype=np.int64, - ) - # Denote the first edge, 1 => 2, as a source edge for - # allocation network 1 - subnetwork_id = len(from_id) * [None] - subnetwork_id[0] = 2 - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": (len(from_id) - 2) * ["flow"] + 2 * ["control"], - "subnetwork_id": subnetwork_id, - }, - geometry=lines, - crs="EPSG:28992", - ) - ) + basin_data: list[TableModel[Any]] = [ + basin.Profile(area=300_000.0, level=[0.0, 1.0]), + basin.State(level=[1.0]), + ] - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [2, 2, 5, 5, 12, 12], - "area": 300_000.0, - "level": 3 * [0.0, 1.0], - } + model.flow_boundary.add( + Node(1, Point(0, 0), subnetwork_id=2), [flow_boundary.Static(flow_rate=[2.0])] ) - - state = pd.DataFrame(data={"node_id": [2, 5, 12], "level": 1.0}) - - basin = ribasim.Basin(profile=profile, state=state) - - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame( - data={ - "node_id": [1], - "flow_rate": 2.0, - } - ) + model.basin.add(Node(2, Point(1, 0), subnetwork_id=2), basin_data) + model.user_demand.add( + Node(3, Point(1, 1), subnetwork_id=2), + [ + user_demand.Static( + demand=[1.5], return_factor=0.0, min_level=-1.0, priority=1 + ) + ], ) - - linear_resistance = ribasim.LinearResistance( - static=pd.DataFrame( - data={ - "node_id": [4], - "resistance": 0.06, - } - ) + model.linear_resistance.add( + Node(4, Point(2, 0), subnetwork_id=2), + [linear_resistance.Static(resistance=[0.06])], ) - - tabulated_rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": 7, - "level": [0.0, 0.5, 1.0], - "flow_rate": [0.0, 0.0, 2.0], - } - ) + model.basin.add(Node(5, Point(3, 0), subnetwork_id=2), basin_data) + model.user_demand.add( + Node(6, Point(3, 1), subnetwork_id=2), + [ + user_demand.Static( + demand=[1.0], return_factor=0.0, min_level=-1.0, priority=3 + ) + ], ) - - fractional_flow = ribasim.FractionalFlow( - static=pd.DataFrame( - data={ - "node_id": [8, 8, 9, 9], - "fraction": [0.6, 0.9, 0.4, 0.1], - "control_state": ["divert", "close", "divert", "close"], - } - ) + model.tabulated_rating_curve.add( + Node(7, Point(4, 0), subnetwork_id=2), + [ + tabulated_rating_curve.Static( + level=[0.0, 0.5, 1.0], flow_rate=[0.0, 0.0, 2.0] + ) + ], ) - - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [10], - } - ) + model.fractional_flow.add( + Node(8, Point(4.5, 0), subnetwork_id=2), + [ + fractional_flow.Static( + fraction=[0.6, 0.9], control_state=["divert", "close"] + ) + ], ) - - condition = pd.DataFrame( - data={ - "node_id": [11], - "listen_feature_id": 5, - "variable": "level", - "greater_than": 0.52, - } + model.fractional_flow.add( + Node(9, Point(4.5, 0.5), subnetwork_id=2), + [ + fractional_flow.Static( + fraction=[0.4, 0.1], control_state=["divert", "close"] + ) + ], ) - - logic = pd.DataFrame( - data={ - "node_id": 11, - "truth_state": ["T", "F"], - "control_state": ["divert", "close"], - } + model.terminal.add(Node(10, Point(5, 0), subnetwork_id=2)) + model.discrete_control.add( + Node(11, Point(4.5, 0.25), subnetwork_id=2), + [ + discrete_control.Condition( + listen_node_type="Basin", + listen_node_id=[5], + variable="level", + greater_than=0.52, + ), + discrete_control.Logic( + truth_state=["T", "F"], control_state=["divert", "close"] + ), + ], ) - - discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) - - user_demand = ribasim.UserDemand( - static=pd.DataFrame( - data={ - "node_id": [6, 13], - "demand": [1.5, 1.0], - "return_factor": 0.0, - "min_level": -1.0, - "priority": [1, 3], - } - ), - time=pd.DataFrame( - data={ - "node_id": [3, 3, 3, 3], - "demand": [0.0, 1.0, 1.2, 1.2], - "priority": [1, 1, 2, 2], - "return_factor": 0.0, - "min_level": -1.0, - "time": 2 * ["2020-01-01 00:00:00", "2020-01-20 00:00:00"], - } - ), + model.basin.add(Node(12, Point(4.5, 1), subnetwork_id=2), basin_data) + model.user_demand.add( + Node(13, Point(5, 1), subnetwork_id=2), + [ + user_demand.Time( + time=2 * ["2020-01-01 00:00:00", "2020-01-20 00:00:00"], + demand=[0.0, 1.0, 1.2, 1.2], + return_factor=0.0, + min_level=-1.0, + priority=[1, 1, 2, 2], + ) + ], ) - # Setup allocation: - allocation = ribasim.Allocation(use_allocation=True, timestep=86400) - - model = ribasim.Model( - network=ribasim.Network( - node=node, - edge=edge, - ), - basin=basin, - flow_boundary=flow_boundary, - linear_resistance=linear_resistance, - tabulated_rating_curve=tabulated_rating_curve, - terminal=terminal, - user_demand=user_demand, - discrete_control=discrete_control, - fractional_flow=fractional_flow, - allocation=allocation, - starttime="2020-01-01 00:00:00", - endtime="2020-01-20 00:00:00", - ) + model.edge.add(model.flow_boundary[1], model.basin[2], "flow", subnetwork_id=2) + model.edge.add(model.basin[2], model.user_demand[3], "flow") + model.edge.add(model.basin[2], model.linear_resistance[4], "flow") + model.edge.add(model.linear_resistance[4], model.basin[5], "flow") + model.edge.add(model.basin[5], model.user_demand[6], "flow") + model.edge.add(model.basin[5], model.tabulated_rating_curve[7], "flow") + model.edge.add(model.tabulated_rating_curve[7], model.fractional_flow[8], "flow") + model.edge.add(model.user_demand[3], model.basin[2], "flow") + model.edge.add(model.user_demand[6], model.basin[5], "flow") + model.edge.add(model.tabulated_rating_curve[7], model.fractional_flow[9], "flow") + model.edge.add(model.fractional_flow[8], model.terminal[10], "flow") + model.edge.add(model.fractional_flow[9], model.basin[12], "flow") + model.edge.add(model.basin[12], model.user_demand[13], "flow") + model.edge.add(model.user_demand[13], model.terminal[10], "flow") + model.edge.add(model.discrete_control[11], model.fractional_flow[8], "control") + model.edge.add(model.discrete_control[11], model.fractional_flow[9], "control") return model -def main_network_with_subnetworks_model(): +def main_network_with_subnetworks_model() -> Model: """Generate a model which consists of a main network and multiple connected subnetworks.""" - # Set up the nodes: - xy = np.array( - [ - (0.0, -1.0), - (3.0, 1.0), - (6.0, -1.0), - (9.0, 1.0), - (12.0, -1.0), - (15.0, 1.0), - (18.0, -1.0), - (21.0, 1.0), - (24.0, -1.0), - (27.0, 1.0), - (3.0, 4.0), - (2.0, 4.0), - (1.0, 4.0), - (0.0, 4.0), - (2.0, 5.0), - (2.0, 6.0), - (1.0, 6.0), - (0.0, 6.0), - (2.0, 8.0), - (2.0, 3.0), - (3.0, 6.0), - (0.0, 7.0), - (2.0, 7.0), - (14.0, 3.0), - (14.0, 4.0), - (14.0, 5.0), - (13.0, 6.0), - (12.0, 7.0), - (11.0, 8.0), - (15.0, 6.0), - (16.0, 7.0), - (17.0, 8.0), - (13.0, 5.0), - (26.0, 3.0), - (26.0, 4.0), - (25.0, 4.0), - (24.0, 4.0), - (28.0, 4.0), - (26.0, 5.0), - (24.0, 6.0), - (25.0, 6.0), - (26.0, 6.0), - (27.0, 6.0), - (28.0, 6.0), - (24.0, 7.0), - (26.0, 7.0), - (28.0, 7.0), - (26.0, 8.0), - (27.0, 8.0), - (28.0, 8.0), - (25.0, 9.0), - (26.0, 9.0), - (28.0, 9.0), - (26.0, 10.0), - (26.0, 11.0), - (26.0, 12.0), - (29.0, 6.0), - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-03-01 00:00:00", + allocation=Allocation(use_allocation=True, timestep=86400), + ) - node_type = [ - "FlowBoundary", - "Basin", - "LinearResistance", - "Basin", - "LinearResistance", - "Basin", - "LinearResistance", - "Basin", - "LinearResistance", - "Basin", - "Pump", - "Basin", - "Outlet", - "Terminal", - "Pump", - "Basin", - "Outlet", - "Basin", - "Terminal", - "UserDemand", - "UserDemand", - "UserDemand", - "Outlet", - "Pump", - "Basin", - "TabulatedRatingCurve", - "FractionalFlow", - "Basin", - "UserDemand", - "FractionalFlow", - "Basin", - "UserDemand", - "DiscreteControl", - "UserDemand", - "Basin", - "Outlet", - "Terminal", - "Pump", - "Pump", - "Basin", - "Outlet", - "Basin", - "Outlet", - "Basin", - "UserDemand", - "TabulatedRatingCurve", - "TabulatedRatingCurve", - "Basin", - "Pump", - "Basin", - "UserDemand", - "TabulatedRatingCurve", - "UserDemand", - "Basin", - "Outlet", - "Terminal", - "UserDemand", + basin_data: list[TableModel[Any]] = [ + basin.Profile(area=1000.0, level=[0.0, 1.0]), + basin.State(level=[1.0]), + ] + large_basin_data: list[TableModel[Any]] = [ + basin.Profile(area=100000.0, level=[0.0, 1.0]), + basin.State(level=[10.0]), ] - subnetwork_id = np.ones(57, dtype=int) - subnetwork_id[10:23] = 3 - subnetwork_id[23:33] = 5 - subnetwork_id[33:] = 7 - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={ - "node_type": node_type, - "subnetwork_id": subnetwork_id, - }, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array( + model.flow_boundary.add( + Node(1, Point(0, -1), subnetwork_id=1), [flow_boundary.Static(flow_rate=[1.0])] + ) + model.basin.add(Node(2, Point(3, 1), subnetwork_id=1), basin_data) + model.linear_resistance.add( + Node(3, Point(6, -1), subnetwork_id=1), + [linear_resistance.Static(resistance=[0.001])], + ) + model.basin.add(Node(4, Point(9, 1), subnetwork_id=1), basin_data) + model.linear_resistance.add( + Node(5, Point(12, -1), subnetwork_id=1), + [linear_resistance.Static(resistance=[0.001])], + ) + model.basin.add(Node(6, Point(15, 1), subnetwork_id=1), basin_data) + model.linear_resistance.add( + Node(7, Point(18, -1), subnetwork_id=1), + [linear_resistance.Static(resistance=[0.001])], + ) + model.basin.add(Node(8, Point(21, 1), subnetwork_id=1), basin_data) + model.linear_resistance.add( + Node(9, Point(24, -1), subnetwork_id=1), + [linear_resistance.Static(resistance=[0.001])], + ) + model.basin.add(Node(10, Point(27, 1), subnetwork_id=1), basin_data) + model.pump.add( + Node(11, Point(3, 4), subnetwork_id=3), + [pump.Static(flow_rate=[1e-3], max_flow_rate=1.0)], + ) + model.basin.add(Node(12, Point(2, 4), subnetwork_id=3), large_basin_data) + model.outlet.add( + Node(13, Point(1, 4), subnetwork_id=3), + [outlet.Static(flow_rate=[3.0], max_flow_rate=3.0)], + ) + model.terminal.add(Node(14, Point(0, 4), subnetwork_id=3)) + model.pump.add( + Node(15, Point(2, 5), subnetwork_id=3), + [pump.Static(flow_rate=[4.0], max_flow_rate=4.0)], + ) + model.basin.add(Node(16, Point(2, 6), subnetwork_id=3), large_basin_data) + model.outlet.add( + Node(17, Point(1, 6), subnetwork_id=3), + [outlet.Static(flow_rate=[3.0], max_flow_rate=3.0)], + ) + model.basin.add(Node(18, Point(0, 6), subnetwork_id=3), large_basin_data) + model.terminal.add(Node(19, Point(2, 8), subnetwork_id=3)) + model.user_demand.add( + Node(20, Point(2, 3), subnetwork_id=3), [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 11, - 12, - 13, - 12, - 12, - 15, - 16, - 17, - 16, - 18, - 16, - 23, - 20, - 21, - 22, - 24, - 25, - 26, - 27, - 28, - 29, - 26, - 30, - 31, - 32, - 33, - 33, - 38, - 35, - 36, - 35, - 35, - 39, - 42, - 41, - 40, - 42, - 46, - 48, - 49, - 50, - 48, - 52, - 48, - 51, - 54, - 55, - 42, - 43, - 44, - 47, - 34, - 45, - 53, - 44, - 57, - 2, - 6, - 10, + user_demand.Static( + demand=[4.0], return_factor=0.9, min_level=0.9, priority=2 + ) ], - dtype=np.int64, ) - to_id = np.array( + model.user_demand.add( + Node(21, Point(3, 6), subnetwork_id=3), [ - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 12, - 13, - 14, - 20, - 15, - 16, - 17, - 18, - 21, - 22, - 23, - 19, - 12, - 16, - 18, - 25, - 26, - 27, - 28, - 29, - 28, - 30, - 31, - 32, - 31, - 27, - 30, - 35, - 36, - 37, - 34, - 39, - 42, - 41, - 40, - 45, - 46, - 48, - 49, - 50, - 53, - 52, - 54, - 51, - 54, - 55, - 56, - 43, - 44, - 47, - 50, - 35, - 40, - 50, - 57, - 44, - 11, - 24, - 38, + user_demand.Static( + demand=[5.0], return_factor=0.9, min_level=0.9, priority=1 + ) ], - dtype=np.int64, ) - - edge_type = 68 * ["flow"] - edge_type[34] = "control" - edge_type[35] = "control" - subnetwork_id = 68 * [None] - subnetwork_id[0] = 1 - subnetwork_id[65] = 3 - subnetwork_id[66] = 5 - subnetwork_id[67] = 7 - - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": edge_type, - "subnetwork_id": subnetwork_id, - }, - geometry=lines, - crs="EPSG:28992", - ) + model.user_demand.add( + Node(22, Point(0, 7), subnetwork_id=3), + [ + user_demand.Static( + demand=[3.0], return_factor=0.9, min_level=0.9, priority=2 + ) + ], ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [ - 2, - 2, - 4, - 4, - 6, - 6, - 8, - 8, - 10, - 10, - 12, - 12, - 16, - 16, - 18, - 18, - 25, - 25, - 28, - 28, - 31, - 31, - 35, - 35, - 40, - 40, - 42, - 42, - 44, - 44, - 48, - 48, - 50, - 50, - 54, - 54, - ], - "area": [ - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 100000.0, - 100000.0, - 100000.0, - 100000.0, - 100000.0, - 100000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - ], - "level": [ - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 1.0, - ], - } + model.outlet.add( + Node(23, Point(2, 7), subnetwork_id=3), + [outlet.Static(flow_rate=[3.0], max_flow_rate=3.0)], ) - - state = pd.DataFrame( - data={ - "node_id": [ - 2, - 4, - 6, - 8, - 10, - 12, - 16, - 18, - 25, - 28, - 31, - 35, - 40, - 42, - 44, - 48, - 50, - 54, - ], - "level": [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 10.0, - 10.0, - 10.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - ], - } + model.pump.add( + Node(24, Point(14, 3), subnetwork_id=5), + [pump.Static(flow_rate=[1e-3], max_flow_rate=1.0)], ) - - basin = ribasim.Basin( - profile=profile, - state=state, + model.basin.add(Node(25, Point(14, 4), subnetwork_id=5), basin_data) + model.tabulated_rating_curve.add( + Node(26, Point(14, 5), subnetwork_id=5), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 1e-4])], ) - - # Setup the discrete control: - condition = pd.DataFrame( - data={ - "node_id": [33], - "listen_feature_id": [25], - "variable": ["level"], - "greater_than": [0.003], - } + model.fractional_flow.add( + Node(27, Point(13, 6), subnetwork_id=5), + [fractional_flow.Static(fraction=[0.25, 0.75], control_state=["A", "B"])], ) - - logic = pd.DataFrame( - data={ - "node_id": [33, 33], - "truth_state": ["F", "T"], - "control_state": ["A", "B"], - } + model.basin.add(Node(28, Point(12, 7), subnetwork_id=5), basin_data) + model.user_demand.add( + Node(29, Point(11, 8), subnetwork_id=5), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=1 + ) + ], ) - - discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) - - # Setup flow boundary - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame(data={"node_id": [1], "flow_rate": [1.0]}) + model.fractional_flow.add( + Node(30, Point(15, 6), subnetwork_id=5), + [fractional_flow.Static(fraction=[0.75, 0.25], control_state=["A", "B"])], ) - - # Setup fractional flow - fractional_flow = ribasim.FractionalFlow( - static=pd.DataFrame( - data={ - "node_id": [27, 30, 27, 30], - "fraction": [0.25, 0.75, 0.75, 0.25], - "control_state": ["A", "A", "B", "B"], - } - ) + model.basin.add(Node(31, Point(16, 7), subnetwork_id=5), basin_data) + model.user_demand.add( + Node(32, Point(17, 8), subnetwork_id=5), + [ + user_demand.Time( + time=["2020-01-01 00:00:00", "2021-01-01 00:00:00"], + demand=[1e-3, 2e-3], + return_factor=0.9, + min_level=0.9, + priority=1, + ) + ], ) - - # Setup linear resistance - linear_resistance = ribasim.LinearResistance( - static=pd.DataFrame(data={"node_id": [3, 5, 7, 9], "resistance": 0.001}) + model.discrete_control.add( + Node(33, Point(13, 5), subnetwork_id=5), + [ + discrete_control.Condition( + listen_node_type="Basin", + listen_node_id=[25], + variable="level", + greater_than=0.003, + ), + discrete_control.Logic(truth_state=["F", "T"], control_state=["A", "B"]), + ], ) - - # Setup outlet - outlet = ribasim.Outlet( - static=pd.DataFrame( - data={ - "node_id": [13, 17, 23, 36, 41, 43, 55], - "flow_rate": [3.0, 3.0, 3.0, 0.003, 0.003, 0.003, 0.003], - "max_flow_rate": 3.0, - } - ) + model.user_demand.add( + Node(34, Point(26, 3), subnetwork_id=7), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=2 + ) + ], ) - - # Setup pump - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [15, 39, 49, 11, 24, 38], - "flow_rate": [4.0e00, 4.0e-03, 4.0e-03, 1.0e-03, 1.0e-03, 1.0e-03], - "max_flow_rate": [4.0, 0.004, 0.004, 1.0, 1.0, 1.0], - } - ) + model.basin.add(Node(35, Point(26, 4), subnetwork_id=7), basin_data) + model.outlet.add( + Node(36, Point(25, 4), subnetwork_id=7), + [outlet.Static(flow_rate=[0.003], max_flow_rate=3.0)], + ) + model.terminal.add(Node(37, Point(24, 4), subnetwork_id=7)) + model.pump.add( + Node(38, Point(28, 4), subnetwork_id=7), + [pump.Static(flow_rate=[1e-3], max_flow_rate=1.0)], + ) + model.pump.add( + Node(39, Point(26, 5), subnetwork_id=7), + [pump.Static(flow_rate=[4e-3], max_flow_rate=0.004)], + ) + model.basin.add(Node(40, Point(24, 6), subnetwork_id=7), basin_data) + model.outlet.add( + Node(41, Point(25, 6), subnetwork_id=7), + [outlet.Static(flow_rate=[0.003], max_flow_rate=3.0)], + ) + model.basin.add(Node(42, Point(26, 6), subnetwork_id=7), basin_data) + model.outlet.add( + Node(43, Point(27, 6), subnetwork_id=7), + [outlet.Static(flow_rate=[0.003], max_flow_rate=3.0)], + ) + model.basin.add(Node(44, Point(28, 6), subnetwork_id=7), basin_data) + model.user_demand.add( + Node(45, Point(24, 7), subnetwork_id=7), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=1 + ) + ], ) - - # Setup tabulated rating curve - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [26, 26, 46, 46, 47, 47, 52, 52], - "level": [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - "flow_rate": [ - 0.0e00, - 1.0e-04, - 0.0e00, - 2.0e00, - 0.0e00, - 2.0e00, - 0.0e00, - 2.0e00, - ], - } - ) + model.tabulated_rating_curve.add( + Node(46, Point(26, 7), subnetwork_id=7), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 2.0])], ) - - # Setup terminal node - terminal = ribasim.Terminal(static=pd.DataFrame(data={"node_id": [14, 19, 37, 56]})) - - # Setup the UserDemand - user_demand = ribasim.UserDemand( - static=pd.DataFrame( - data={ - "node_id": [20, 21, 22, 29, 34, 45, 51, 53, 57], - "demand": [ - 4.0e00, - 5.0e00, - 3.0e00, - 1.0e-03, - 1.0e-03, - 1.0e-03, - 1.0e-03, - 1.0e-03, - 1.0e-03, - ], - "return_factor": 0.9, - "min_level": 0.9, - "priority": [2, 1, 2, 1, 2, 1, 3, 3, 2], - } - ), - time=pd.DataFrame( - data={ - "node_id": [32, 32], - "time": ["2020-01-01 00:00:00", "2021-01-01 00:00:00"], - "demand": [0.001, 0.002], - "return_factor": 0.9, - "min_level": 0.9, - "priority": 1, - } - ), + model.tabulated_rating_curve.add( + Node(47, Point(28, 7), subnetwork_id=7), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 2.0])], ) - - # Setup allocation: - allocation = ribasim.Allocation(use_allocation=True, timestep=86400) - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - discrete_control=discrete_control, - flow_boundary=flow_boundary, - fractional_flow=fractional_flow, - linear_resistance=linear_resistance, - outlet=outlet, - pump=pump, - terminal=terminal, - user_demand=user_demand, - tabulated_rating_curve=rating_curve, - allocation=allocation, - starttime="2020-01-01 00:00:00", - endtime="2020-03-01 00:00:00", + model.basin.add(Node(48, Point(26, 8), subnetwork_id=7), basin_data) + model.pump.add( + Node(49, Point(27, 8), subnetwork_id=7), + [pump.Static(flow_rate=[4e-3], max_flow_rate=0.004)], ) - - return model - - -def level_demand_model(): - # Set up the nodes: - xy = np.array( + model.basin.add(Node(50, Point(28, 8), subnetwork_id=7), basin_data) + model.user_demand.add( + Node(51, Point(25, 9), subnetwork_id=7), [ - (0.0, 0.0), # 1: FlowBoundary - (1.0, 0.0), # 2: Basin - (2.0, 0.0), # 3: UserDemand - (1.0, -1.0), # 4: LevelDemand - (2.0, -1.0), # 5: Basin - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["FlowBoundary", "Basin", "UserDemand", "LevelDemand", "Basin"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={ - "node_type": node_type, - "subnetwork_id": 5 * [2], - }, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=3 + ) + ], ) - - # Setup the edges: - from_id = np.array([1, 2, 4, 3, 4]) - to_id = np.array([2, 3, 2, 5, 5]) - edge_type = ["flow", "flow", "control", "flow", "control"] - subnetwork_id = [2, None, None, None, None] - - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": edge_type, - "subnetwork_id": subnetwork_id, - }, - geometry=lines, - crs="EPSG:28992", - ) + model.tabulated_rating_curve.add( + Node(52, Point(26, 9), subnetwork_id=7), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 2.0])], ) - - # Setup basin - profile = pd.DataFrame( - data={"node_id": [2, 2, 5, 5], "area": 1e3, "level": [0.0, 1.0, 0.0, 1.0]} - ) - static = pd.DataFrame( - data={ - "node_id": [5], - "drainage": 0.0, - "potential_evaporation": 0.0, - "infiltration": 0.0, - "precipitation": 0.0, - "urban_runoff": 0.0, - } - ) - time = pd.DataFrame( - data={ - "node_id": 2, - "time": ["2020-01-01 00:00:00", "2020-01-16 00:00:00"], - "drainage": 0.0, - "potential_evaporation": 0.0, - "infiltration": 0.0, - "precipitation": [1e-6, 0.0], - "urban_runoff": 0.0, - }, + model.user_demand.add( + Node(53, Point(28, 9), subnetwork_id=7), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=3 + ) + ], ) - - state = pd.DataFrame(data={"node_id": [2, 5], "level": 0.5}) - basin = ribasim.Basin(profile=profile, static=static, time=time, state=state) - - # Setup flow boundary - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame(data={"node_id": [1], "flow_rate": 1e-3}) + model.basin.add(Node(54, Point(26, 10), subnetwork_id=7), basin_data) + model.outlet.add( + Node(55, Point(26, 11), subnetwork_id=7), + [outlet.Static(flow_rate=[0.003], max_flow_rate=3.0)], ) - - # Setup allocation level control - level_demand = ribasim.LevelDemand( - static=pd.DataFrame( - data={"node_id": [4], "priority": 1, "min_level": 1.0, "max_level": 1.5} - ) + model.terminal.add(Node(56, Point(26, 12), subnetwork_id=7)) + model.user_demand.add( + Node(57, Point(29, 6), subnetwork_id=7), + [ + user_demand.Static( + demand=[1e-3], return_factor=0.9, min_level=0.9, priority=2 + ) + ], ) - # Setup UserDemand - user_demand = ribasim.UserDemand( - static=pd.DataFrame( - data={ - "node_id": [3], - "priority": [2], - "demand": [1.5e-3], - "return_factor": [0.2], - "min_level": [0.2], - } - ) - ) + model.edge.add(model.flow_boundary[1], model.basin[2], "flow", subnetwork_id=1) + model.edge.add(model.basin[2], model.linear_resistance[3], "flow") + model.edge.add(model.linear_resistance[3], model.basin[4], "flow") + model.edge.add(model.basin[4], model.linear_resistance[5], "flow") + model.edge.add(model.linear_resistance[5], model.basin[6], "flow") + model.edge.add(model.basin[6], model.linear_resistance[7], "flow") + model.edge.add(model.linear_resistance[7], model.basin[8], "flow") + model.edge.add(model.basin[8], model.linear_resistance[9], "flow") + model.edge.add(model.linear_resistance[9], model.basin[10], "flow") + model.edge.add(model.pump[11], model.basin[12], "flow") + model.edge.add(model.basin[12], model.outlet[13], "flow") + model.edge.add(model.outlet[13], model.terminal[14], "flow") + model.edge.add(model.basin[12], model.user_demand[20], "flow") + model.edge.add(model.basin[12], model.pump[15], "flow") + model.edge.add(model.pump[15], model.basin[16], "flow") + model.edge.add(model.basin[16], model.outlet[17], "flow") + model.edge.add(model.outlet[17], model.basin[18], "flow") + model.edge.add(model.basin[16], model.user_demand[21], "flow") + model.edge.add(model.basin[18], model.user_demand[22], "flow") + model.edge.add(model.basin[16], model.outlet[23], "flow") + model.edge.add(model.outlet[23], model.terminal[19], "flow") + model.edge.add(model.user_demand[20], model.basin[12], "flow") + model.edge.add(model.user_demand[21], model.basin[16], "flow") + model.edge.add(model.user_demand[22], model.basin[18], "flow") + model.edge.add(model.pump[24], model.basin[25], "flow") + model.edge.add(model.basin[25], model.tabulated_rating_curve[26], "flow") + model.edge.add(model.tabulated_rating_curve[26], model.fractional_flow[27], "flow") + model.edge.add(model.fractional_flow[27], model.basin[28], "flow") + model.edge.add(model.basin[28], model.user_demand[29], "flow") + model.edge.add(model.user_demand[29], model.basin[28], "flow") + model.edge.add(model.tabulated_rating_curve[26], model.fractional_flow[30], "flow") + model.edge.add(model.fractional_flow[30], model.basin[31], "flow") + model.edge.add(model.basin[31], model.user_demand[32], "flow") + model.edge.add(model.user_demand[32], model.basin[31], "flow") + model.edge.add(model.discrete_control[33], model.fractional_flow[27], "control") + model.edge.add(model.discrete_control[33], model.fractional_flow[30], "control") + model.edge.add(model.pump[38], model.basin[35], "flow") + model.edge.add(model.basin[35], model.outlet[36], "flow") + model.edge.add(model.outlet[36], model.terminal[37], "flow") + model.edge.add(model.basin[35], model.user_demand[34], "flow") + model.edge.add(model.basin[35], model.pump[39], "flow") + model.edge.add(model.pump[39], model.basin[42], "flow") + model.edge.add(model.basin[42], model.outlet[41], "flow") + model.edge.add(model.outlet[41], model.basin[40], "flow") + model.edge.add(model.basin[40], model.user_demand[45], "flow") + model.edge.add(model.basin[42], model.tabulated_rating_curve[46], "flow") + model.edge.add(model.tabulated_rating_curve[46], model.basin[48], "flow") + model.edge.add(model.basin[48], model.pump[49], "flow") + model.edge.add(model.pump[49], model.basin[50], "flow") + model.edge.add(model.basin[50], model.user_demand[53], "flow") + model.edge.add(model.basin[48], model.tabulated_rating_curve[52], "flow") + model.edge.add(model.tabulated_rating_curve[52], model.basin[54], "flow") + model.edge.add(model.basin[48], model.user_demand[51], "flow") + model.edge.add(model.user_demand[51], model.basin[54], "flow") + model.edge.add(model.basin[54], model.outlet[55], "flow") + model.edge.add(model.outlet[55], model.terminal[56], "flow") + model.edge.add(model.basin[42], model.outlet[43], "flow") + model.edge.add(model.outlet[43], model.basin[44], "flow") + model.edge.add(model.basin[44], model.tabulated_rating_curve[47], "flow") + model.edge.add(model.tabulated_rating_curve[47], model.basin[50], "flow") + model.edge.add(model.user_demand[34], model.basin[35], "flow") + model.edge.add(model.user_demand[45], model.basin[40], "flow") + model.edge.add(model.user_demand[53], model.basin[50], "flow") + model.edge.add(model.basin[44], model.user_demand[57], "flow") + model.edge.add(model.user_demand[57], model.basin[44], "flow") + model.edge.add(model.basin[2], model.pump[11], "flow", subnetwork_id=3) + model.edge.add(model.basin[6], model.pump[24], "flow", subnetwork_id=5) + model.edge.add(model.basin[10], model.pump[38], "flow", subnetwork_id=7) + + return model - # Setup allocation - allocation = ribasim.Allocation(use_allocation=True, timestep=1e5) - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - flow_boundary=flow_boundary, - level_demand=level_demand, - user_demand=user_demand, - allocation=allocation, +def level_demand_model() -> Model: + model = Model( starttime="2020-01-01 00:00:00", endtime="2020-02-01 00:00:00", + allocation=Allocation(use_allocation=True, timestep=1e5), + ) + model.flow_boundary.add( + Node(1, Point(0, 0), subnetwork_id=2), [flow_boundary.Static(flow_rate=[1e-3])] + ) + model.basin.add( + Node(2, Point(1, 0), subnetwork_id=2), + [ + basin.Profile(area=1000.0, level=[0.0, 1.0]), + basin.Time( + time=["2020-01-01 00:00:00", "2020-01-16 00:00:00"], + precipitation=[1e-6, 0.0], + ), + basin.State(level=[0.5]), + ], + ) + model.user_demand.add( + Node(3, Point(2, 0), subnetwork_id=2), + [ + user_demand.Static( + demand=[1.5e-3], return_factor=0.2, min_level=0.2, priority=2 + ) + ], ) + model.level_demand.add( + Node(4, Point(1, -1), subnetwork_id=2), + [level_demand.Static(min_level=[1.0], max_level=1.5, priority=1)], + ) + model.basin.add( + Node(5, Point(2, -1), subnetwork_id=2), + [basin.Profile(area=1000.0, level=[0.0, 1.0]), basin.State(level=[0.5])], + ) + + model.edge.add(model.flow_boundary[1], model.basin[2], "flow", subnetwork_id=2) + model.edge.add(model.basin[2], model.user_demand[3], "flow") + model.edge.add(model.level_demand[4], model.basin[2], "control") + model.edge.add(model.user_demand[3], model.basin[5], "flow") + model.edge.add(model.level_demand[4], model.basin[5], "control") return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/backwater.py b/python/ribasim_testmodels/ribasim_testmodels/backwater.py index 89622ff5d..5af1eddf3 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/backwater.py +++ b/python/ribasim_testmodels/ribasim_testmodels/backwater.py @@ -1,97 +1,82 @@ -import geopandas as gpd import numpy as np -import pandas as pd import ribasim +from ribasim.config import Node +from ribasim.nodes import ( + basin, + flow_boundary, + level_boundary, + manning_resistance, +) +from shapely.geometry import Point def backwater_model(): """Backwater curve as an integration test for ManningResistance""" - x = np.arange(0.0, 1020.0, 10.0) - node_type = np.full(x.size, "ManningResistance") + node_type = np.full(102, "ManningResistance") node_type[1::2] = "Basin" node_type[0] = "FlowBoundary" node_type[-1] = "LevelBoundary" - node_xy = gpd.points_from_xy(x=x, y=np.zeros_like(x)) - _, counts = np.unique(node_type, return_counts=True) - n_basin = counts[0] + ids = np.arange(1, node_type.size + 1, dtype=np.int64) - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(node_xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - ids = np.arange(1, x.size + 1, dtype=np.int64) - from_id = ids[:-1] - to_id = ids[1:] - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) + model = ribasim.Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", ) - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame( - data={ - "node_id": ids[node_type == "FlowBoundary"], - "flow_rate": [5.0], - } - ) + model.flow_boundary.add( + Node(1, Point(0.0, 0.0)), [flow_boundary.Static(flow_rate=[5.0])] ) # Rectangular profile, width of 1.0 m. basin_ids = ids[node_type == "Basin"] - profile = pd.DataFrame( - data={ - "node_id": np.repeat(basin_ids, 2), - "area": [20.0, 20.0] * n_basin, - "level": [0.0, 1.0] * n_basin, - } - ) - state = pd.DataFrame(data={"node_id": basin_ids, "level": 0.05}) - basin = ribasim.Basin(profile=profile, state=state) - - manning_resistance = ribasim.ManningResistance( - static=pd.DataFrame( - data={ - "node_id": ids[node_type == "ManningResistance"], - "length": 20.0, - "manning_n": 0.04, - "profile_width": 1.0, - "profile_slope": 0.0, - } + basin_x = np.arange(10.0, 1000.0, 20.0) + for id, x in zip(basin_ids, basin_x): + model.basin.add( + Node(id, Point(x, 0.0)), + [ + basin.Profile(area=[20.0, 20.0], level=[0.0, 1.0]), + basin.State(level=[0.05]), + ], ) - ) + model.manning_resistance.add( + Node(id + 1, Point(x + 10.0, 0.0)), + [ + manning_resistance.Static( + length=[20.0], + manning_n=[0.04], + profile_width=[1.0], + profile_slope=[0.0], + ) + ], + ) + if id == 2: + model.edge.add( + model.flow_boundary[1], + model.basin[2], + "flow", + ) + else: + model.edge.add( + model.manning_resistance[id - 1], + model.basin[id], + "flow", + ) - level_boundary = ribasim.LevelBoundary( - static=pd.DataFrame( - data={ - "node_id": ids[node_type == "LevelBoundary"], - "level": [2.0], - } + model.edge.add( + model.basin[id], + model.manning_resistance[id + 1], + "flow", ) - ) - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - level_boundary=level_boundary, - flow_boundary=flow_boundary, - manning_resistance=manning_resistance, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.level_boundary.add( + Node(102, Point(1010.0, 0.0)), [level_boundary.Static(level=[2.0])] + ) + model.edge.add( + model.manning_resistance[101], + model.level_boundary[102], + "flow", ) return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/basic.py b/python/ribasim_testmodels/ribasim_testmodels/basic.py index 4ee41e3b7..f6412550e 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/basic.py +++ b/python/ribasim_testmodels/ribasim_testmodels/basic.py @@ -1,221 +1,183 @@ from pathlib import Path +from typing import Any -import geopandas as gpd import numpy as np import pandas as pd import ribasim +from ribasim.config import Node +from ribasim.input_base import TableModel +from ribasim.nodes import ( + basin, + flow_boundary, + fractional_flow, + level_boundary, + linear_resistance, + manning_resistance, + outlet, + pump, + tabulated_rating_curve, +) +from shapely.geometry import Point def basic_model() -> ribasim.Model: - """Set up a basic model with most node types and static forcing""" - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 3, 3, 6, 6, 9, 9], - "area": [0.01, 1000.0] * 4, - "level": [0.0, 1.0] * 4, - } - ) - - # Convert steady forcing to m/s - # 2 mm/d precipitation, 1 mm/d evaporation - seconds_in_day = 24 * 3600 - precipitation = 0.002 / seconds_in_day - evaporation = 0.001 / seconds_in_day - - static = pd.DataFrame( - data={ - "node_id": [0], - "potential_evaporation": [evaporation], - "precipitation": [precipitation], - } - ) - static = static.iloc[[0, 0, 0, 0]] - static["node_id"] = [1, 3, 6, 9] - state = pd.DataFrame( - data={"node_id": static["node_id"], "level": 0.04471158417652035} - ) - # This is a 1:1 translation. - subgrid = pd.DataFrame( - data={ - "node_id": profile["node_id"], - "subgrid_id": profile["node_id"], - "basin_level": profile["level"], - "subgrid_level": profile["level"], - } - ) - basin = ribasim.Basin(profile=profile, static=static, state=state, subgrid=subgrid) - - # Setup linear resistance: - linear_resistance = ribasim.LinearResistance( - static=pd.DataFrame( - data={"node_id": [12, 10], "resistance": [5e3, (3600.0 * 24) / 100.0]} - ) - ) - - # Setup Manning resistance: - manning_resistance = ribasim.ManningResistance( - static=pd.DataFrame( - data={ - "node_id": [2], - "length": [900.0], - "manning_n": [0.04], - "profile_width": [1.0], - "profile_slope": [3.0], - } - ) - ) - - # Set up a rating curve node: - # Discharge: lose 1% of storage volume per day at storage = 1000.0. - q1000 = 1000.0 * 0.01 / seconds_in_day - - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [4, 4], - "level": [0.0, 1.0], - "flow_rate": [0.0, q1000], - } - ) - ) - - # Setup fractional flows: - fractional_flow = ribasim.FractionalFlow( - static=pd.DataFrame( - data={ - "node_id": [5, 8, 13], - "fraction": [0.3, 0.6, 0.1], - } - ) - ) - - # Setup pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [7], - "flow_rate": [0.5 / 3600], - } - ) + # Setup model + model = ribasim.Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", ) + model.logging = ribasim.Logging(verbosity="debug") - # Setup flow boundary: - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame( - data={ - "node_id": [15, 16], - "flow_rate": [1e-4, 1e-4], - } + # Setup basins + level = [0.0, 1.0] + node_data: list[TableModel[Any]] = [ + basin.Profile(area=[0.01, 1000.0], level=level), + basin.Static( + potential_evaporation=[0.001 / 86400], precipitation=[0.002 / 86400] + ), + basin.State(level=[0.04471158417652035]), + ] + node_ids = [1, 3, 6, 9] + node_geometries = [ + Point(0.0, 0.0), + Point(2.0, 0.0), + Point(3.0, 2.0), + Point(5.0, 0.0), + ] + for node_id, node_geometry in zip(node_ids, node_geometries): + model.basin.add( + Node(node_id, node_geometry), + [ + basin.Subgrid( + subgrid_id=[node_id] * 2, basin_level=level, subgrid_level=level + ), + *node_data, + ], ) - ) - # Setup level boundary: - level_boundary = ribasim.LevelBoundary( - static=pd.DataFrame( - data={ - "node_id": [11, 17], - "level": [1.0, 1.5], - } - ) + # Setup linear resistance + model.linear_resistance.add( + Node(12, Point(2.0, 1.0)), [linear_resistance.Static(resistance=[5e3])] ) - - # Setup terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [14], - } - ) + model.linear_resistance.add( + Node(10, Point(6.0, 0.0)), + [linear_resistance.Static(resistance=[(3600.0 * 24) / 100.0])], ) - # Set up the nodes: - xy = np.array( + # Setup Manning resistance + model.manning_resistance.add( + Node(2, Point(1.0, 0.0)), [ - (0.0, 0.0), # 1: Basin - (1.0, 0.0), # 2: ManningResistance - (2.0, 0.0), # 3: Basin - (3.0, 0.0), # 4: TabulatedRatingCurve - (3.0, 1.0), # 5: FractionalFlow - (3.0, 2.0), # 6: Basin - (4.0, 1.0), # 7: Pump - (4.0, 0.0), # 8: FractionalFlow - (5.0, 0.0), # 9: Basin - (6.0, 0.0), # 10: LinearResistance - (2.0, 2.0), # 11: LevelBoundary - (2.0, 1.0), # 12: LinearResistance - (3.0, -1.0), # 13: FractionalFlow - (3.0, -2.0), # 14: Terminal - (3.0, 3.0), # 15: Flowboundary - (0.0, 1.0), # 16: FlowBoundary - (6.0, 1.0), # 17: LevelBoundary - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_id, node_type = ribasim.Node.node_ids_and_types( - basin, - level_boundary, - flow_boundary, - pump, - terminal, - linear_resistance, - manning_resistance, - rating_curve, - fractional_flow, - ) - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(node_id, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array( - [1, 2, 3, 4, 4, 5, 6, 8, 7, 9, 11, 12, 4, 13, 15, 16, 10], dtype=np.int64 - ) - to_id = np.array( - [2, 3, 4, 5, 8, 6, 7, 9, 9, 10, 12, 3, 13, 14, 6, 1, 17], dtype=np.int64 - ) - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - # Setup logging - logging = ribasim.Logging(verbosity="debug") - - # Setup a model: - model = ribasim.Model( - network=ribasim.Network( - node=node, - edge=edge, - ), - basin=basin, - level_boundary=level_boundary, - flow_boundary=flow_boundary, - pump=pump, - terminal=terminal, - linear_resistance=linear_resistance, - manning_resistance=manning_resistance, - tabulated_rating_curve=rating_curve, - fractional_flow=fractional_flow, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", - logging=logging, + manning_resistance.Static( + length=[900.0], + manning_n=[0.04], + profile_width=[1.0], + profile_slope=[3.0], + ) + ], + ) + + # Setup tabulated rating curve + model.tabulated_rating_curve.add( + Node(4, Point(3.0, 0.0)), + [ + tabulated_rating_curve.Static( + level=[0.0, 1.0], + flow_rate=[ + 0.0, + 1000.0 * 0.01 / 86400, + ], # Discharge: lose 1% of storage volume per day at storage = 1000.0. + ) + ], + ) + + # Setup fractional flows + model.fractional_flow.add( + Node(5, Point(3.0, 1.0)), [fractional_flow.Static(fraction=[0.3])] + ) + model.fractional_flow.add( + Node(8, Point(4.0, 0.0)), [fractional_flow.Static(fraction=[0.6])] + ) + model.fractional_flow.add( + Node(13, Point(3.0, -1.0)), [fractional_flow.Static(fraction=[0.1])] + ) + + # Setup pump + model.pump.add(Node(7, Point(4.0, 1.0)), [pump.Static(flow_rate=[0.5 / 3600])]) + + # Setup flow boundary + flow_boundary_data = [flow_boundary.Static(flow_rate=[1e-4])] + model.flow_boundary.add(Node(15, Point(3.0, 3.0)), flow_boundary_data) + model.flow_boundary.add(Node(16, Point(0.0, 1.0)), flow_boundary_data) + + # Setup level boundary + model.level_boundary.add( + Node(11, Point(2.0, 2.0)), [level_boundary.Static(level=[1.0])] + ) + model.level_boundary.add( + Node(17, Point(6.0, 1.0)), [level_boundary.Static(level=[1.5])] + ) + + # Setup terminal + model.terminal.add(Node(14, Point(3.0, -2.0))) + + # Setup edges + model.edge.add(model.basin[1], model.manning_resistance[2], "flow") + model.edge.add(model.manning_resistance[2], model.basin[3], "flow") + model.edge.add( + model.basin[3], + model.tabulated_rating_curve[4], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[4], + model.fractional_flow[5], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[4], + model.fractional_flow[8], + "flow", + ) + model.edge.add(model.fractional_flow[5], model.basin[6], "flow") + model.edge.add(model.basin[6], model.pump[7], "flow") + model.edge.add(model.fractional_flow[8], model.basin[9], "flow") + model.edge.add(model.pump[7], model.basin[9], "flow") + model.edge.add(model.basin[9], model.linear_resistance[10], "flow") + model.edge.add( + model.level_boundary[11], + model.linear_resistance[12], + "flow", + ) + model.edge.add( + model.linear_resistance[12], + model.basin[3], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[4], + model.fractional_flow[13], + "flow", + ) + model.edge.add( + model.fractional_flow[13], + model.terminal[14], + "flow", + ) + model.edge.add( + model.flow_boundary[15], + model.basin[6], + "flow", + ) + model.edge.add( + model.flow_boundary[16], + model.basin[1], + "flow", + ) + model.edge.add( + model.linear_resistance[10], + model.level_boundary[17], + "flow", ) return model @@ -287,23 +249,26 @@ def tabulated_rating_curve_model() -> ribasim.Model: Only the upstream Basin receives a (constant) precipitation. """ - # Set up a rating curve node: - # Discharge: lose 1% of storage volume per day at storage = 1000.0. - seconds_in_day = 24 * 3600 - q1000 = 1000.0 * 0.01 / seconds_in_day - - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [2, 2], - "level": [0.0, 1.0], - "flow_rate": [0.0, q1000], - } - ), - time=pd.DataFrame( - data={ - "node_id": [3, 3, 3, 3, 3, 3], - "time": [ + # Setup a model: + model = ribasim.Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", + ) + + # Setup tabulated rating curve: + model.tabulated_rating_curve.add( + Node(2, Point(1.0, 1.0)), + [ + tabulated_rating_curve.Static( + level=[0.0, 1.0], flow_rate=[0.0, 10 / 86400] + ), + ], + ) + model.tabulated_rating_curve.add( + Node(3, Point(1.0, -1.0)), + [ + tabulated_rating_curve.Time( + time=[ # test subsecond precision pd.Timestamp("2020-01-01 00:00:00.000001"), pd.Timestamp("2020-01"), @@ -312,193 +277,98 @@ def tabulated_rating_curve_model() -> ribasim.Model: pd.Timestamp("2020-03"), pd.Timestamp("2020-03"), ], - "level": [0.0, 1.0, 0.0, 1.1, 0.0, 1.2], - "flow_rate": [0.0, q1000, 0.0, q1000, 0.0, q1000], - } - ), - ) - - # Set up the nodes: - xy = np.array( + level=[0.0, 1.0, 0.0, 1.1, 0.0, 1.2], + flow_rate=[0.0, 10 / 86400, 0.0, 10 / 86400, 0.0, 10 / 86400], + ), + ], + ) + + # Setup the basins + node_data: list[TableModel[Any]] = [ + basin.Profile(area=[0.01, 1000.0], level=[0.0, 1.0]), + basin.State(level=[0.04471158417652035]), + ] + basin_geometry_1 = Point(0.0, 0.0) + model.basin.add( + Node(1, basin_geometry_1), [ - (0.0, 0.0), # 1: Basin - (1.0, 1.0), # 2: TabulatedRatingCurve (static) - (1.0, -1.0), # 3: TabulatedRatingCurve (time-varying) - (2.0, 0.0), # 4: Basin - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={ - "node_type": [ - "Basin", - "TabulatedRatingCurve", - "TabulatedRatingCurve", - "Basin", - ] - }, - index=pd.Index([1, 2, 3, 4], name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 4, 4], - "area": [0.01, 1000.0] * 2, - "level": [0.0, 1.0] * 2, - } + basin.Static(precipitation=[0.002 / 86400]), + basin.Area(geometry=[basin_geometry_1.buffer(1.0)]), + *node_data, + ], + ) + basin_geometry_2 = Point(2.0, 0.0) + model.basin.add( + Node(4, basin_geometry_2), + [ + basin.Static(precipitation=[0.0]), + basin.Area(geometry=[basin_geometry_2.buffer(1.0)]), + *node_data, + ], + ) + model.edge.add( + model.basin[1], + model.tabulated_rating_curve[2], + "flow", + ) + model.edge.add( + model.basin[1], + model.tabulated_rating_curve[3], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[2], + model.basin[4], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[3], + model.basin[4], + "flow", ) + return model - # Convert steady forcing to m/s - # 2 mm/d precipitation - precipitation = 0.002 / seconds_in_day - # only the upstream basin gets precipitation - static = pd.DataFrame( - data={ - "node_id": [1, 4], - "precipitation": [precipitation, 0.0], - } - ) - state = pd.DataFrame( - data={"node_id": static["node_id"], "level": 0.04471158417652035} - ) - # Turn the basin node point geometries into polygons describing the area - assert node.df is not None - basin_area = ( - node.df[node.df.node_type == "Basin"] - .geometry.buffer(1.0) - .reset_index(drop=True) - ) - area = gpd.GeoDataFrame( - data={"node_id": static.node_id}, - geometry=basin_area, - crs="EPSG:28992", - ) - - basin = ribasim.Basin(profile=profile, static=static, state=state, area=area) - - # Setup the edges: - from_id = np.array([1, 1, 2, 3], dtype=np.int64) - to_id = np.array([2, 3, 4, 4], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - # Setup a model: +def outlet_model(): + """Set up a basic model with an outlet that encounters various physical constraints.""" model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - tabulated_rating_curve=rating_curve, starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", + solver=ribasim.Solver(saveat=0), ) - return model - - -def outlet_model(): - """Set up a basic model with an outlet that encounters various physical constraints.""" - - # Set up the nodes: - xy = np.array( + # Set up the basins + model.basin.add( + Node(3, Point(2.0, 0.0)), [ - (0.0, 0.0), # 1: LevelBoundary - (1.0, 0.0), # 2: Outlet - (2.0, 0.0), # 3: Basin - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["LevelBoundary", "Outlet", "Basin"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + basin.Profile(area=[1000.0, 1000.0], level=[0.0, 1.0]), + basin.State(level=[1e-3]), + ], ) - # Setup the edges: - from_id = np.array([1, 2], dtype=np.int64) - to_id = np.array([2, 3], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": 3, - "area": 1000.0, - "level": [0.0, 1.0], - } - ) - - state = pd.DataFrame(data={"node_id": [3], "level": 1e-3}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup the level boundary: - level_boundary = ribasim.LevelBoundary( - time=pd.DataFrame( - data={ - "node_id": 1, - "time": [ + # Set up the level boundary + model.level_boundary.add( + Node(1, Point(0.0, 0.0)), + [ + level_boundary.Time( + time=[ "2020-01-01 00:00:00", "2020-06-01 00:00:00", "2021-01-01 00:00:00", ], - "level": [1.0, 3.0, 3.0], - } - ) + level=[1.0, 3.0, 3.0], + ) + ], ) # Setup the outlet - outlet = ribasim.Outlet( - static=pd.DataFrame( - data={ - "node_id": [2], - "flow_rate": 1e-3, - "min_crest_level": 2.0, - } - ) + model.outlet.add( + Node(2, Point(1.0, 0.0)), + [outlet.Static(flow_rate=[1e-3], min_crest_level=[2.0])], ) - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - outlet=outlet, - level_boundary=level_boundary, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", - solver=ribasim.Solver(saveat=0), - ) + # Setup the edges + model.edge.add(model.level_boundary[1], model.outlet[2], "flow") + model.edge.add(model.outlet[2], model.basin[3], "flow") return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/bucket.py b/python/ribasim_testmodels/ribasim_testmodels/bucket.py index 8ec69d161..bb3230c63 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/bucket.py +++ b/python/ribasim_testmodels/ribasim_testmodels/bucket.py @@ -1,153 +1,67 @@ -import geopandas as gpd import numpy as np import pandas as pd import ribasim +from ribasim.config import Node +from ribasim.nodes import ( + basin, +) +from shapely.geometry import Point def bucket_model() -> ribasim.Model: """Bucket model with just a single basin.""" - # Set up the nodes: - xy = np.array( - [ - (400.0, 200.0), # Basin - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = ["Basin"] - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the dummy edges: - from_id = np.array([], dtype=np.int64) - to_id = np.array([], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1], - "area": [1000.0, 1000.0], - "level": [0.0, 1.0], - } - ) - - state = pd.DataFrame( - data={ - "node_id": [1], - "level": [1.0], - } - ) - - static = pd.DataFrame( - data={ - "node_id": [1], - "drainage": [np.nan], - "potential_evaporation": [np.nan], - "infiltration": [np.nan], - "precipitation": [np.nan], - "urban_runoff": [np.nan], - } - ) - basin = ribasim.Basin(profile=profile, static=static, state=state) - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", ) - return model - -def leaky_bucket_model() -> ribasim.Model: - """Bucket model with dynamic forcing with missings.""" - - # Set up the nodes: - xy = np.array( + model.basin.add( + Node(1, Point(400.0, 200.0)), [ - (400.0, 200.0), # Basin - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = ["Basin"] - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the dummy edges: - from_id = np.array([], dtype=np.int64) - to_id = np.array([], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1], - "area": [1000.0, 1000.0], - "level": [0.0, 1.0], - } + basin.Profile( + area=[1000.0, 1000.0], + level=[0.0, 1.0], + ), + basin.State(level=[1.0]), + basin.Static( + drainage=[np.nan], + potential_evaporation=[np.nan], + infiltration=[np.nan], + precipitation=[np.nan], + urban_runoff=[np.nan], + ), + ], ) + return model - state = pd.DataFrame( - data={ - "node_id": [1], - "level": [1.0], - } - ) - time = pd.DataFrame( - data={ - "time": pd.date_range("2020-01-01", "2020-01-05"), - "node_id": 1, - "drainage": [0.003, np.nan, 0.001, 0.002, 0.0], - "potential_evaporation": np.nan, - "infiltration": [np.nan, 0.001, 0.002, 0.0, 0.0], - "precipitation": np.nan, - "urban_runoff": 0.0, - } - ) - basin = ribasim.Basin(profile=profile, time=time, state=state) +def leaky_bucket_model() -> ribasim.Model: + """Bucket model with dynamic forcing with missings.""" model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, starttime="2020-01-01 00:00:00", endtime="2020-01-05 00:00:00", ) + + model.basin.add( + Node(1, Point(400.0, 200.0)), + [ + basin.Profile( + area=[1000.0, 1000.0], + level=[0.0, 1.0], + ), + basin.State(level=[1.0]), + basin.Time( + time=pd.date_range("2020-01-01", "2020-01-05"), + node_id=1, + drainage=[0.003, np.nan, 0.001, 0.002, 0.0], + potential_evaporation=np.nan, + infiltration=[np.nan, 0.001, 0.002, 0.0, 0.0], + precipitation=np.nan, + urban_runoff=0.0, + ), + ], + ) + return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py index 70bc4f29f..bfaf25cef 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py @@ -1,418 +1,260 @@ -import geopandas as gpd -import numpy as np -import pandas as pd -import ribasim - - -def pump_discrete_control_model() -> ribasim.Model: +from ribasim.config import Node +from ribasim.model import Model +from ribasim.nodes import ( + basin, + discrete_control, + flow_boundary, + level_boundary, + linear_resistance, + outlet, + pump, + tabulated_rating_curve, +) +from shapely.geometry import Point + + +def pump_discrete_control_model() -> Model: """ Set up a basic model with a pump controlled based on basin levels. The LinearResistance is deactivated when the levels are almost equal. """ - # Set up the nodes: - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, -1.0), # 2: LinearResistance - (2.0, 0.0), # 3: Basin - (1.0, 0.0), # 4: Pump - (1.0, 1.0), # 5: DiscreteControl - (2.0, -1.0), # 6: DiscreteControl - ] - ) - - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "Basin", - "LinearResistance", - "Basin", - "Pump", - "DiscreteControl", - "DiscreteControl", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2, 1, 4, 5, 6], dtype=np.int64) - to_id = np.array([2, 3, 4, 3, 4, 2], dtype=np.int64) - - edge_type = 4 * ["flow"] + 2 * ["control"] - - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={"from_node_id": from_id, "to_node_id": to_id, "edge_type": edge_type}, - geometry=lines, - crs="EPSG:28992", - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", ) - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 3, 3], - "area": [100.0, 100.0] * 2, - "level": [0.0, 1.0] * 2, - } + model.basin.add( + Node(1, Point(0, 0)), + [basin.State(level=[1.0]), basin.Profile(level=[0.0, 1.0], area=100.0)], ) - - state = pd.DataFrame(data={"node_id": [1, 3], "level": [1.0, 1e-5]}) - - static = pd.DataFrame( - data={ - "node_id": [3], - "drainage": [0.0], - "potential_evaporation": [0.0], - "infiltration": [0.0], - "precipitation": [1e-9], - "urban_runoff": [0.0], - } + model.linear_resistance.add( + Node(2, Point(1, -1)), + [ + linear_resistance.Static( + resistance=1e5, + control_state=["active", "inactive"], + active=[True, False], + ) + ], + ) + model.basin.add( + Node(3, Point(2, 0)), + [ + basin.State(level=[1e-5]), + basin.Static(precipitation=[1e-9]), + basin.Profile(level=[0.0, 1.0], area=100.0), + ], ) - - basin = ribasim.Basin(profile=profile, static=static, state=state) - - # Setup the discrete control: - condition = pd.DataFrame( - data={ - "node_id": [5, 5, 6], - "listen_feature_id": [1, 3, 3], - "variable": ["level", "level", "level"], - "greater_than": [0.8, 0.4, 0.45], - } + model.pump.add( + Node(4, Point(1, 0)), + [pump.Static(flow_rate=[0.0, 1e-5], control_state=["off", "on"])], ) - - # False, False -> "on" - # True, False -> "off" - # False, True -> "off" - # True, True -> "on" - # False -> "active" - # True -> "inactive" - - # Truth state as subset of the conditions above and in that order - - logic = pd.DataFrame( - data={ - "node_id": [5, 5, 5, 5, 6, 6], - "truth_state": ["FF", "TF", "FT", "TT", "T", "F"], - "control_state": ["on", "off", "off", "on", "inactive", "active"], - } + model.discrete_control.add( + Node(5, Point(1, 1)), + [ + discrete_control.Condition( + listen_node_type="Basin", + listen_node_id=[1, 3], + variable="level", + greater_than=[0.8, 0.4], + ), + discrete_control.Logic( + truth_state=["FF", "TF", "FT", "TT"], + control_state=["on", "off", "off", "on"], + ), + ], + ) + model.discrete_control.add( + Node(6, Point(2, -1)), + [ + discrete_control.Condition( + listen_node_type="Basin", + listen_node_id=3, + variable="level", + greater_than=[0.45], + ), + discrete_control.Logic( + truth_state=["T", "F"], + control_state=["inactive", "active"], + ), + ], + ) + + model.edge.add( + model.basin[1], + model.linear_resistance[2], + "flow", + ) + model.edge.add( + model.linear_resistance[2], + model.basin[3], + "flow", + ) + model.edge.add( + model.basin[1], + model.pump[4], + "flow", + ) + model.edge.add( + model.pump[4], + model.basin[3], + "flow", + ) + model.edge.add( + model.discrete_control[5], + model.pump[4], + "control", + ) + model.edge.add( + model.discrete_control[6], + model.linear_resistance[2], + "control", ) - discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) + return model - # Setup the pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "control_state": ["off", "on"], - "node_id": [4, 4], - "flow_rate": [0.0, 1e-5], - } - ) - ) - # Setup the linear resistance: - linear_resistance = ribasim.LinearResistance( - static=pd.DataFrame( - data={ - "node_id": [2, 2], - "active": [True, False], - "resistance": [1e5, 1e5], - "control_state": ["active", "inactive"], - } - ) - ) +def flow_condition_model() -> Model: + """Set up a basic model that involves discrete control based on a flow condition""" - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - linear_resistance=linear_resistance, - pump=pump, - discrete_control=discrete_control, + model = Model( starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", ) - return model - - -def flow_condition_model(): - """Set up a basic model that involves discrete control based on a flow condition""" - - # Set up the nodes: - xy = np.array( + model.flow_boundary.add( + Node(1, Point(0, 0)), [ - (0.0, 0.0), # 1: FlowBoundary - (1.0, 0.0), # 2: Basin - (2.0, 0.0), # 3: Pump - (3.0, 0.0), # 4: Terminal - (1.0, 1.0), # 5: DiscreteControl - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "FlowBoundary", - "Basin", - "Pump", - "Terminal", - "DiscreteControl", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2, 3, 5], dtype=np.int64) - to_id = np.array([2, 3, 4, 3], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": (len(from_id) - 1) * ["flow"] + ["control"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [2, 2], - "area": [100.0, 100.0], - "level": [0.0, 1.0], - } - ) - - state = pd.DataFrame(data={"node_id": [2], "level": [2.5]}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [3, 3], - "flow_rate": [0.0, 1e-3], - "control_state": ["off", "on"], - } - ) + flow_boundary.Time( + time=["2020-01-01 00:00:00", "2022-01-01 00:00:00"], + flow_rate=[0.0, 40 / 86400], + ) + ], + ) + model.basin.add( + Node(2, Point(1, 0)), + [basin.Profile(level=[0.0, 1.0], area=100.0), basin.State(level=[2.5])], + ) + model.pump.add( + Node(3, Point(2, 0)), + [pump.Static(flow_rate=[0.0, 1e-3], control_state=["off", "on"])], + ) + model.terminal.add(Node(4, Point(3, 0))) + model.discrete_control.add( + Node(5, Point(1, 1)), + [ + discrete_control.Condition( + listen_node_type="FlowBoundary", + listen_node_id=1, + variable="flow_rate", + greater_than=[20 / (86400)], + look_ahead=60 * 86400, + ), + discrete_control.Logic(truth_state=["T", "F"], control_state=["off", "on"]), + ], + ) + + model.edge.add( + model.flow_boundary[1], + model.basin[2], + "flow", + ) + model.edge.add( + model.basin[2], + model.pump[3], + "flow", + ) + model.edge.add( + model.pump[3], + model.terminal[4], + "flow", + ) + model.edge.add( + model.discrete_control[5], + model.pump[3], + "control", ) - # Setup discrete control: - discrete_control = ribasim.DiscreteControl( - condition=pd.DataFrame( - data={ - "node_id": [5], - "listen_feature_id": [1], - "variable": ["flow_rate"], - "greater_than": [20 / (24 * 60 * 60)], - "look_ahead": [60 * 24 * 60 * 60], - } - ), - logic=pd.DataFrame( - data={ - "node_id": [5, 5], - "truth_state": ["T", "F"], - "control_state": ["off", "on"], - } - ), - ) + return model - # Setup flow boundary: - flow_boundary = ribasim.FlowBoundary( - time=pd.DataFrame( - data={ - "node_id": [1, 1], - "time": ["2020-01-01 00:00:00", "2022-01-01 00:00:00"], - "flow_rate": [0.0, 40 / (24 * 60 * 60)], - } - ) - ) - # Setup terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [4], - } - ) - ) +def level_boundary_condition_model() -> Model: + """Set up a small model with a condition on a level boundary.""" - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - pump=pump, - flow_boundary=flow_boundary, - terminal=terminal, - discrete_control=discrete_control, + model = Model( starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", ) - return model - - -def level_boundary_condition_model(): - """Set up a small model with a condition on a level boundary.""" - - # Set up the nodes - xy = np.array( + model.level_boundary.add( + Node(1, Point(0, 0)), [ - (0.0, 0.0), # 1: LevelBoundary - (1.0, 0.0), # 2: LinearResistance - (2.0, 0.0), # 3: Basin - (3.0, 0.0), # 4: Outlet - (4.0, 0.0), # 5: Terminal - (1.5, 1.0), # 6: DiscreteControl - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "LevelBoundary", - "LinearResistance", - "Basin", - "Outlet", - "Terminal", - "DiscreteControl", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2, 3, 4, 6], dtype=np.int64) - to_id = np.array([2, 3, 4, 5, 4], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": (len(from_id) - 1) * ["flow"] + ["control"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [3, 3], - "area": [100.0, 100.0], - "level": [0.0, 1.0], - } - ) - - state = pd.DataFrame(data={"node_id": [3], "level": [2.5]}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup level boundary: - level_boundary = ribasim.LevelBoundary( - time=pd.DataFrame( - data={ - "node_id": [1, 1], - "time": ["2020-01-01 00:00:00", "2022-01-01 00:00:00"], - "level": [5.0, 10.0], - } - ) - ) - - # Setup linear resistance: - linear_resistance = ribasim.LinearResistance( - static=pd.DataFrame(data={"node_id": [2], "resistance": [5e3]}) - ) - - # Setup outlet: - outlet = ribasim.Outlet( - static=pd.DataFrame( - data={ - "node_id": [4, 4], - "active": [True, False], - "flow_rate": 2 * [0.5 / 3600], - "control_state": ["on", "off"], - } - ) + level_boundary.Time( + time=["2020-01-01 00:00:00", "2022-01-01 00:00:00"], level=[5.0, 10.0] + ) + ], ) - - # Setup terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [5], - } - ) + model.linear_resistance.add( + Node(2, Point(1, 0)), [linear_resistance.Static(resistance=[5e3])] ) - - # Setup discrete control: - discrete_control = ribasim.DiscreteControl( - condition=pd.DataFrame( - data={ - "node_id": [6], - "listen_feature_id": [1], - "variable": ["level"], - "greater_than": [6.0], - "look_ahead": [60 * 24 * 60 * 60], - } - ), - logic=pd.DataFrame( - data={ - "node_id": [6, 6], - "truth_state": ["T", "F"], - "control_state": ["on", "off"], - } - ), + model.basin.add( + Node(3, Point(2, 0)), + [basin.Profile(level=[0.0, 1.0], area=100.0), basin.State(level=[2.5])], ) - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - outlet=outlet, - level_boundary=level_boundary, - linear_resistance=linear_resistance, - terminal=terminal, - discrete_control=discrete_control, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.outlet.add( + Node(4, Point(3, 0)), + [ + outlet.Static( + active=[True, False], flow_rate=0.5 / 3600, control_state=["on", "off"] + ) + ], + ) + model.terminal.add(Node(5, Point(4, 0))) + model.discrete_control.add( + Node(6, Point(1.5, 1)), + [ + discrete_control.Condition( + listen_node_type="LevelBoundary", + listen_node_id=[1], + variable="level", + greater_than=6.0, + look_ahead=60 * 86400, + ), + discrete_control.Logic(truth_state=["T", "F"], control_state=["on", "off"]), + ], + ) + + model.edge.add( + model.level_boundary[1], + model.linear_resistance[2], + "flow", + ) + model.edge.add( + model.linear_resistance[2], + model.basin[3], + "flow", + ) + model.edge.add( + model.basin[3], + model.outlet[4], + "flow", + ) + model.edge.add( + model.outlet[4], + model.terminal[5], + "flow", + ) + model.edge.add( + model.discrete_control[6], + model.outlet[4], + "control", ) return model -def tabulated_rating_curve_control_model() -> ribasim.Model: +def tabulated_rating_curve_control_model() -> Model: """Discrete control on a TabulatedRatingCurve. The Basin drains over a TabulatedRatingCurve into a Terminal. The Control @@ -420,251 +262,156 @@ def tabulated_rating_curve_control_model() -> ribasim.Model: at some threshold level. """ - # Set up the nodes: - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, 0.0), # 2: TabulatedRatingCurve (controlled) - (2.0, 0.0), # 3: Terminal - (1.0, 1.0), # 4: Control - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "Basin", - "TabulatedRatingCurve", - "Terminal", - "DiscreteControl", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2, 4], dtype=np.int64) - to_id = np.array([2, 3, 2], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": ["flow", "flow", "control"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1], - "area": [0.01, 1000.0], - "level": [0.0, 1.0], - } - ) - - # Convert steady forcing to m/s - # 2 mm/d precipitation - seconds_in_day = 24 * 3600 - precipitation = 0.002 / seconds_in_day - # only the upstream basin gets precipitation - static = pd.DataFrame( - data={ - "node_id": [1], - "precipitation": precipitation, - } - ) - - state = pd.DataFrame(data={"node_id": [1], "level": 0.04471158417652035}) - - basin = ribasim.Basin(profile=profile, static=static, state=state) - - # Set up a rating curve node: - # Discharge: lose 1% of storage volume per day at storage = 100.0. - q100 = 100.0 * 0.01 / seconds_in_day - - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [2, 2, 2, 2], - "level": [0.0, 1.2, 0.0, 1.0], - "flow_rate": [0.0, q100, 0.0, q100], - "control_state": ["low", "low", "high", "high"], - } - ), - ) - - terminal = ribasim.Terminal(static=pd.DataFrame(data={"node_id": [3]})) - - # Setup the discrete control: - condition = pd.DataFrame( - data={ - "node_id": [4], - "listen_feature_id": [1], - "variable": ["level"], - "greater_than": [0.5], - } - ) - - logic = pd.DataFrame( - data={ - "node_id": [4, 4], - "truth_state": ["T", "F"], - "control_state": ["low", "high"], - } - ) - - discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) - - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - tabulated_rating_curve=rating_curve, - terminal=terminal, - discrete_control=discrete_control, + model = Model( starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", ) + model.basin.add( + Node(1, Point(0, 0)), + [ + # 2 mm/d precipitation + basin.Static(precipitation=[0.002 / 86400]), + basin.State(level=[0.04471158417652035]), + basin.Profile(area=[0.01, 1000.0], level=[0.0, 1.0]), + ], + ) + model.tabulated_rating_curve.add( + Node(2, Point(1, 0)), + [ + tabulated_rating_curve.Static( + level=[0.0, 1.2, 0.0, 1.0], + flow_rate=[0.0, 1 / 86400, 0.0, 1 / 86400], + control_state=["low", "low", "high", "high"], + ) + ], + ) + model.terminal.add(Node(3, Point(2, 0))) + model.discrete_control.add( + Node(4, Point(1, 1)), + [ + discrete_control.Condition( + listen_node_type="Basin", + listen_node_id=[1], + variable="level", + greater_than=0.5, + ), + discrete_control.Logic( + truth_state=["T", "F"], control_state=["low", "high"] + ), + ], + ) + + model.edge.add( + model.basin[1], + model.tabulated_rating_curve[2], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[2], + model.terminal[3], + "flow", + ) + model.edge.add( + model.discrete_control[4], + model.tabulated_rating_curve[2], + "control", + ) + return model -def level_setpoint_with_minmax_model(): +def level_setpoint_with_minmax_model() -> Model: """ Set up a minimal model in which the level of a basin is kept within an acceptable range around a setpoint while being affected by time-varying forcing. This is done by bringing the level back to the setpoint once the level goes beyond this range. """ - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, 1.0), # 2: Pump - (1.0, -1.0), # 3: Pump - (2.0, 0.0), # 4: LevelBoundary - (-1.0, 0.0), # 5: TabulatedRatingCurve - (-2.0, 0.0), # 6: Terminal - (1.0, 0.0), # 7: DiscreteControl - ] - ) - - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = [ - "Basin", - "Pump", - "Pump", - "LevelBoundary", - "TabulatedRatingCurve", - "Terminal", - "DiscreteControl", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 3, 4, 2, 1, 5, 7, 7], dtype=np.int64) - to_id = np.array([3, 4, 2, 1, 5, 6, 2, 3], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": 6 * ["flow"] + 2 * ["control"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1], - "area": [1000.0, 1000.0], - "level": [0.0, 1.0], - } + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", ) - state = pd.DataFrame(data={"node_id": [1], "level": [20.0]}) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup pump - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": 3 * [2] + 3 * [3], - "control_state": 2 * ["none", "in", "out"], - "flow_rate": [0.0, 2e-3, 0.0, 0.0, 0.0, 2e-3], - } - ) + model.basin.add( + Node(1, Point(0, 0)), + [ + basin.Profile(area=1000.0, level=[0.0, 1.0]), + basin.State(level=[20.0]), + ], ) - - # Setup level boundary - level_boundary = ribasim.LevelBoundary( - static=pd.DataFrame(data={"node_id": [4], "level": [10.0]}) + model.pump.add( + Node(2, Point(1, 1)), + [pump.Static(control_state=["none", "in", "out"], flow_rate=[0.0, 2e-3, 0.0])], ) - - # Setup the rating curve - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={"node_id": 2 * [5], "level": [2.0, 15.0], "flow_rate": [0.0, 1e-3]} - ) + model.pump.add( + Node(3, Point(1, -1)), + [pump.Static(control_state=["none", "in", "out"], flow_rate=[0.0, 0.0, 2e-3])], ) - - # Setup the terminal - terminal = ribasim.Terminal(static=pd.DataFrame(data={"node_id": [6]})) - - # Setup discrete control - condition = pd.DataFrame( - data={ - "node_id": 3 * [7], - "listen_feature_id": 3 * [1], - "variable": 3 * ["level"], - "greater_than": [5.0, 10.0, 15.0], # min, setpoint, max - } + model.level_boundary.add( + Node(4, Point(2, 0)), [level_boundary.Static(level=[10.0])] ) - - logic = pd.DataFrame( - data={ - "node_id": 5 * [7], - "truth_state": ["FFF", "U**", "T*F", "**D", "TTT"], - "control_state": ["in", "in", "none", "out", "out"], - } + model.tabulated_rating_curve.add( + Node(5, Point(-1, 0)), + [tabulated_rating_curve.Static(level=[2.0, 15.0], flow_rate=[0.0, 1e-3])], ) - - discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - pump=pump, - level_boundary=level_boundary, - tabulated_rating_curve=rating_curve, - terminal=terminal, - discrete_control=discrete_control, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.terminal.add(Node(6, Point(-2, 0))) + model.discrete_control.add( + Node(7, Point(1, 0)), + [ + discrete_control.Condition( + listen_node_type="Basin", + listen_node_id=1, + variable="level", + # min, setpoint, max + greater_than=[5.0, 10.0, 15.0], + ), + discrete_control.Logic( + truth_state=["FFF", "U**", "T*F", "**D", "TTT"], + control_state=["in", "in", "none", "out", "out"], + ), + ], + ) + + model.edge.add( + model.basin[1], + model.pump[3], + "flow", + ) + model.edge.add( + model.pump[3], + model.level_boundary[4], + "flow", + ) + model.edge.add( + model.level_boundary[4], + model.pump[2], + "flow", + ) + model.edge.add( + model.pump[2], + model.basin[1], + "flow", + ) + model.edge.add( + model.basin[1], + model.tabulated_rating_curve[5], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[5], + model.terminal[6], + "flow", + ) + model.edge.add( + model.discrete_control[7], + model.pump[2], + "control", + ) + model.edge.add( + model.discrete_control[7], + model.pump[3], + "control", ) return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/dutch_waterways.py b/python/ribasim_testmodels/ribasim_testmodels/dutch_waterways.py index 1a2df5be6..bcc1fc441 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/dutch_waterways.py +++ b/python/ribasim_testmodels/ribasim_testmodels/dutch_waterways.py @@ -1,332 +1,266 @@ -import geopandas as gpd import numpy as np import pandas as pd -import ribasim - - -def dutch_waterways_model(): +from ribasim.config import Node +from ribasim.model import Model +from ribasim.nodes import ( + basin, + discrete_control, + flow_boundary, + level_boundary, + linear_resistance, + pid_control, + pump, + tabulated_rating_curve, +) +from shapely.geometry import Point + + +def dutch_waterways_model() -> Model: """Set up a model that is representative of the main Dutch rivers.""" - # Setup the basins - levels = np.array( - [ - 1.86, - 3.21, - 4.91, - 6.61, - 8.31, - 10.07, - 10.17, - 10.27, - 11.61, - 12.94, - 13.05, - 13.69, - 14.32, - 14.96, - 15.59, - ] - ) - - totalwidth = np.array( - [ - 10.0, - 88.0, - 137.0, - 139.0, - 141.0, - 219.0, - 220.0, - 221.0, - 302.0, - 606.0, - 837.0, - 902.0, - 989.0, - 1008.0, - 1011.0, - ] - ) - - basin_node_ids = np.array([2, 5, 6, 10, 12, 15], dtype=int) - n_basins = len(basin_node_ids) - - length = 1e4 - - profile = pd.DataFrame( - data={ - "node_id": np.repeat(basin_node_ids, len(levels)), - "level": np.tile(levels, n_basins), - "area": np.tile(length * totalwidth, n_basins), - } - ) - - state = pd.DataFrame( - data={"node_id": basin_node_ids, "level": [8.31, 7.5, 7.5, 7.0, 6.0, 5.5]} + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", ) - basin = ribasim.Basin(profile=profile, state=state) + profile_level = np.array([1.86, 3.21, 4.91, 6.61, 8.31, 10.07, 10.17, 10.27, 11.61, 12.94, 13.05, 13.69, 14.32, 14.96, 15.59]) # fmt: skip + width = np.array([10.0, 88.0, 137.0, 139.0, 141.0, 219.0, 220.0, 221.0, 302.0, 606.0, 837.0, 902.0, 989.0, 1008.0, 1011.0]) # fmt: skip + basin_profile = basin.Profile(level=profile_level, area=1e4 * width) - # Setup linear resistance: - linear_resistance = ribasim.LinearResistance( - static=pd.DataFrame( - data={"node_id": [3, 4, 11, 18, 19], "resistance": 5 * [1e-2]} - ) - ) + linear_resistance_shared = [linear_resistance.Static(resistance=[1e-2])] - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "control_state": [ - "pump_low", - "pump_high", - "rating_curve", - "rating_curve", - None, - None, - ], - "node_id": [8, 8, 8, 8, 13, 13], - "active": [False, False, True, True, None, None], - "level": [ - 0.0, - 0.0, - 7.45, - 7.46, - 4.45, - 4.46, - ], # The level and flow rate for "pump_low", "pump_high" are irrelevant - "flow_rate": [0.0, 0.0] - + 2 * [418, 420.15], # since the rating curve is not active here - } - ) - ) - - # Setup pump - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": 3 * [9] + [14], - "active": [True, True, False, None], - "control_state": ["pump_low", "pump_high", "rating_curve", None], - "flow_rate": [15.0, 25.0, 1.0, 1.0], - "min_flow_rate": 3 * [None] + [0.0], - "max_flow_rate": 3 * [None] + [50.0], - } - ) - ) - - # Setup flow boundary + # Flow rate curve from sine series n_times = 250 time = pd.date_range( start="2020-01-01 00:00:00", end="2021-01-01 00:00:00", periods=n_times ).astype("datetime64[s]") - - # Flow rate curve from sine series flow_rate = np.zeros(n_times) x = np.linspace(0, 1, n_times) n_terms = 5 for i in np.arange(1, 2 * n_terms, 2): flow_rate += 4 / (i * np.pi) * np.sin(2 * i * np.pi * x) + # Scale to desired magnitude b = (250 + 800) / 2 a = 800 - b - - # Scale to desired magnitude flow_rate = a * flow_rate + b - flow_boundary = ribasim.FlowBoundary( - time=pd.DataFrame( - data={"node_id": n_times * [1], "time": time, "flow_rate": flow_rate} - ) + # TODO use EPSG:28992 and apply 405 - y to the y coordinates + model.flow_boundary.add( + Node(1, Point(1310, 312)), + [flow_boundary.Time(time=time, flow_rate=flow_rate)], ) - - # Setup the level boundary - level_boundary = ribasim.LevelBoundary( - static=pd.DataFrame(data={"node_id": [7, 16], "level": 2 * [3.0]}) + model.basin.add( + Node(2, Point(1281, 278), name="IJsselkop"), + [basin.State(level=[8.31]), basin_profile], ) - - # Setup PID control - pid_control = ribasim.PidControl( - static=pd.DataFrame( - data={ - "node_id": [20], - "listen_node_id": [12], - "target": [6.0], - "proportional": [-0.005], - "integral": [0.0], - "derivative": [-0.002], - } - ) + model.linear_resistance.add(Node(3, Point(1283, 183)), linear_resistance_shared) + model.linear_resistance.add(Node(4, Point(1220, 186)), linear_resistance_shared) + model.basin.add( + Node(5, Point(1342, 162), name="IJssel Westervoort"), + [basin.State(level=[7.5]), basin_profile], ) - - # Setup discrete control - condition = pd.DataFrame( - data={ - "node_id": 4 * [17], - "listen_feature_id": 4 * [1], - "variable": 4 * ["flow_rate"], - "greater_than": [250, 275, 750, 800], - } + model.basin.add( + Node(6, Point(1134, 184), name="Nederrijn Arnhem"), + [basin.State(level=[7.5]), basin_profile], ) - - logic = pd.DataFrame( - data={ - "node_id": 5 * [17], - "truth_state": ["FFFF", "U***", "T**F", "***D", "TTTT"], - "control_state": [ - "pump_low", - "pump_low", - "pump_high", - "rating_curve", - "rating_curve", - ], - } + model.level_boundary.add( + Node(7, Point(1383, 121)), [level_boundary.Static(level=[3.0])] ) - - discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) - - # Set up the nodes: - node_id, node_type = ribasim.Node.node_ids_and_types( - basin, - linear_resistance, - pump, - flow_boundary, - level_boundary, - rating_curve, - pid_control, - discrete_control, + model.tabulated_rating_curve.add( + Node(8, Point(1052, 201), name="Driel open"), + [ + tabulated_rating_curve.Static( + control_state=["pump_low", "pump_high", "rating_curve", "rating_curve"], + active=[False, False, True, True], + # The level and flow rate for "pump_low", "pump_high" are irrelevant + # since the rating curve is not active here + level=[0.0, 0.0, 7.45, 7.46], + flow_rate=[0.0, 0.0, 418, 420.15], + ) + ], ) - - xy = np.array( + model.pump.add( + Node(9, Point(1043, 188), name="Driel gecontroleerd"), [ - (1310, 312), # 1: LevelBoundary - (1281, 278), # 2: Basin - (1283, 183), # 3: LinearResistance - (1220, 186), # 4: LinearResistance - (1342, 162), # 5: Basin - (1134, 184), # 6: Basin - (1383, 121), # 7: LevelBoundary - (1052, 201), # 8: TabulatedRatingCurve - (1043, 188), # 9: Pump - (920, 197), # 10: Basin - (783, 237), # 11: LinearResistance - (609, 186), # 12: Basin - (430, 176), # 13: TabulatedRatingCurve - (442, 164), # 14: Pump - (369, 185), # 15: Basin - (329, 202), # 16: LevelBoundary - (1187, 276), # 17: DiscreteControl - (1362, 142), # 18: LinearResistance - (349, 194), # 19: LinearResistance - (511, 126), # 20: PidControl - ] + pump.Static( + active=[True, True, False], + control_state=["pump_low", "pump_high", "rating_curve"], + flow_rate=[15.0, 25.0, 1.0], + ) + ], ) - - node_name = [ - "", # 1: LevelBoundary - "IJsselkop", # 2: Basin - "", # 3: LinearResistance - "", # 4: LinearResistance - "IJssel Westervoort", # 5: Basin - "Nederrijn Arnhem", # 6: Basin - "", # 7: LevelBoundary - "Driel open", # 8: TabulatedRatingCurve - "Driel gecontroleerd", # 9: Pump - "", # 10: Basin - "", # 11: LinearResistance - "", # 12: Basin - "Amerongen open", # 13: TabulatedRatingCurve - "Amerongen gecontroleerd", # 14: Pump - "", # 15: Basin - "Kruising ARK", # 16: LevelBoundary - "Controller Driel", # 17: DiscreteControl - "", # 18: LinearResistance - "", # 19: LinearResistance - "Controller Amerongen", # 20: PidControl - ] - - node_xy = gpd.points_from_xy(x=xy[:, 0], y=405 - xy[:, 1]) - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type, "name": node_name}, - index=pd.Index(node_id, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model.basin.add( + Node(10, Point(920, 197)), [basin.State(level=[7.0]), basin_profile] ) - - # Setup the edges: - from_id_flow = np.array( - [1, 2, 3, 2, 4, 6, 9, 10, 11, 12, 14, 6, 8, 12, 13, 5, 18, 15, 19], - dtype=np.int64, + model.linear_resistance.add(Node(11, Point(783, 237)), linear_resistance_shared) + model.basin.add( + Node(12, Point(609, 186)), [basin.State(level=[6.0]), basin_profile] ) - to_id_flow = np.array( - [2, 3, 5, 4, 6, 9, 10, 11, 12, 14, 15, 8, 10, 13, 15, 18, 7, 19, 16], - dtype=np.int64, + model.tabulated_rating_curve.add( + Node(13, Point(430, 176), name="Amerongen open"), + [tabulated_rating_curve.Static(level=[4.45, 4.46], flow_rate=[418, 420.15])], ) - - from_id_control = np.array([20, 17, 17], dtype=np.int64) - to_id_control = np.array([14, 8, 9], dtype=np.int64) - - from_id = np.concatenate([from_id_flow, from_id_control]) - to_id = np.concatenate([to_id_flow, to_id_control]) - - edge_name = [ - # flow - "Pannerdensch Kanaal", # 1 -> 2 - "Start IJssel", # 2 -> 3 - "", # 3 -> 5 - "Start Nederrijn", # 2 -> 4 - "", # 4 -> 6 - "", # 6 -> 9 - "", # 9 -> 10 - "", # 10 -> 11 - "", # 11 -> 12 - "", # 12 -> 14 - "", # 14 -> 15 - "", # 6 -> 8 - "", # 8 -> 10 - "", # 12 -> 13 - "", # 13 -> 15 - "", # 5 -> 18 - "", # 18 -> 7 - "", # 15 -> 19 - "", # 19 -> 16 - # control - "", # 20 -> 14 - "", # 17 -> 8 - "", # 17 -> 9 - ] - - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id_flow) * ["flow"] - + len(from_id_control) * ["control"], - "name": edge_name, - }, - geometry=lines, - crs="EPSG:28992", - ) + model.pump.add( + Node(14, Point(442, 164), name="Amerongen gecontroleerd"), + [pump.Static(flow_rate=[1.0], min_flow_rate=0.0, max_flow_rate=50.0)], + ) + model.basin.add( + Node(15, Point(369, 185)), [basin.State(level=[5.5]), basin_profile] + ) + model.level_boundary.add( + Node(16, Point(329, 202), name="Kruising ARK"), + [level_boundary.Static(level=[3.0])], + ) + model.discrete_control.add( + Node(17, Point(1187, 276), name="Controller Driel"), + [ + discrete_control.Condition( + listen_node_type="FlowBoundary", + listen_node_id=1, + variable="flow_rate", + greater_than=[250, 275, 750, 800], + ), + discrete_control.Logic( + truth_state=["FFFF", "U***", "T**F", "***D", "TTTT"], + control_state=[ + "pump_low", + "pump_low", + "pump_high", + "rating_curve", + "rating_curve", + ], + ), + ], + ) + model.linear_resistance.add(Node(18, Point(1362, 142)), linear_resistance_shared) + model.linear_resistance.add(Node(19, Point(349, 194)), linear_resistance_shared) + model.pid_control.add( + Node(20, Point(511, 126), name="Controller Amerongen"), + [ + pid_control.Static( + listen_node_type="Basin", + listen_node_id=[12], + target=6.0, + proportional=-0.005, + integral=0.0, + derivative=-0.002, + ) + ], ) - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - linear_resistance=linear_resistance, - pump=pump, - flow_boundary=flow_boundary, - level_boundary=level_boundary, - tabulated_rating_curve=rating_curve, - pid_control=pid_control, - discrete_control=discrete_control, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.edge.add( + model.flow_boundary[1], + model.basin[2], + "flow", + name="Pannerdensch Kanaal", + ) + model.edge.add( + model.basin[2], + model.linear_resistance[3], + "flow", + name="Start IJssel", + ) + model.edge.add( + model.linear_resistance[3], + model.basin[5], + "flow", + ) + model.edge.add( + model.basin[2], + model.linear_resistance[4], + "flow", + name="Start Nederrijn", + ) + model.edge.add( + model.linear_resistance[4], + model.basin[6], + "flow", + ) + model.edge.add( + model.basin[6], + model.pump[9], + "flow", + ) + model.edge.add( + model.pump[9], + model.basin[10], + "flow", + ) + model.edge.add( + model.basin[10], + model.linear_resistance[11], + "flow", + ) + model.edge.add( + model.linear_resistance[11], + model.basin[12], + "flow", + ) + model.edge.add( + model.basin[12], + model.pump[14], + "flow", + ) + model.edge.add( + model.pump[14], + model.basin[15], + "flow", + ) + model.edge.add( + model.basin[6], + model.tabulated_rating_curve[8], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[8], + model.basin[10], + "flow", + ) + model.edge.add( + model.basin[12], + model.tabulated_rating_curve[13], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[13], + model.basin[15], + "flow", + ) + model.edge.add( + model.basin[5], + model.linear_resistance[18], + "flow", + ) + model.edge.add( + model.linear_resistance[18], + model.level_boundary[7], + "flow", + ) + model.edge.add( + model.basin[15], + model.linear_resistance[19], + "flow", + ) + model.edge.add( + model.linear_resistance[19], + model.level_boundary[16], + "flow", + ) + model.edge.add( + model.pid_control[20], + model.pump[14], + "control", + ) + model.edge.add( + model.discrete_control[17], + model.tabulated_rating_curve[8], + "control", + ) + model.edge.add( + model.discrete_control[17], + model.pump[9], + "control", ) return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/equations.py b/python/ribasim_testmodels/ribasim_testmodels/equations.py index c60142df5..30e7ebcd2 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/equations.py +++ b/python/ribasim_testmodels/ribasim_testmodels/equations.py @@ -1,509 +1,237 @@ -import geopandas as gpd -import numpy as np -import pandas as pd -import ribasim - +from typing import Any -def linear_resistance_model(): +import numpy as np +from ribasim.config import Node, Solver +from ribasim.input_base import TableModel +from ribasim.model import Model +from ribasim.nodes import ( + basin, + flow_boundary, + fractional_flow, + level_boundary, + linear_resistance, + manning_resistance, + pid_control, + pump, + tabulated_rating_curve, +) +from shapely.geometry import Point + + +def linear_resistance_model() -> Model: """Set up a minimal model which uses a linear_resistance node.""" - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, 0.0), # 2: LinearResistance - (2.0, 0.0), # 3: LevelBoundary - ] + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["Basin", "LinearResistance", "LevelBoundary"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model.basin.add( + Node(1, Point(0, 0)), + [basin.Profile(area=100.0, level=[0.0, 10.0]), basin.State(level=[10.0])], ) - - # Setup the edges: - from_id = np.array([1, 2], dtype=np.int64) - to_id = np.array([2, 3], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) + model.linear_resistance.add( + Node(2, Point(1, 0)), + [linear_resistance.Static(resistance=[5e4], max_flow_rate=[6e-5])], ) + model.level_boundary.add(Node(3, Point(2, 0)), [level_boundary.Static(level=[5.0])]) - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1], - "area": [100.0, 100.0], - "level": [0.0, 10.0], - } + model.edge.add( + model.basin[1], + model.linear_resistance[2], + "flow", ) - - state = pd.DataFrame( - data={ - "node_id": [1], - "level": [10.0], - } + model.edge.add( + model.linear_resistance[2], + model.level_boundary[3], + "flow", ) - basin = ribasim.Basin(profile=profile, state=state) + return model - # setup linear resistance: - linear_resistance = ribasim.LinearResistance( - static=pd.DataFrame( - data={"node_id": [2], "resistance": [5e4], "max_flow_rate": [6e-5]} - ) - ) - # Setup level boundary: - level_boundary = ribasim.LevelBoundary( - static=pd.DataFrame( - data={ - "node_id": [3], - "level": [5.0], - } - ) - ) +def rating_curve_model() -> Model: + """Set up a minimal model which uses a tabulated_rating_curve node.""" - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - level_boundary=level_boundary, - linear_resistance=linear_resistance, + model = Model( starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", ) - return model - - -def rating_curve_model(): - """Set up a minimal model which uses a tabulated_rating_curve node.""" - xy = np.array( + model.basin.add( + Node(1, Point(0, 0)), [ - (0.0, 0.0), # 1: Basin - (1.0, 0.0), # 2: TabulatedRatingCurve - (2.0, 0.0), # 3: Terminal - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["Basin", "TabulatedRatingCurve", "Terminal"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + basin.Profile(area=[0.01, 100.0, 100.0], level=[0.0, 1.0, 2.0]), + basin.State(level=[10.5]), + ], ) - # Setup the edges: - from_id = np.array([1, 2], dtype=np.int64) - to_id = np.array([2, 3], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 1], - "area": [0.01, 100.0, 100.0], - "level": [0.0, 1.0, 2.0], - } - ) - - state = pd.DataFrame( - data={ - "node_id": [1], - "level": [10.5], - } - ) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup the rating curve - n_datapoints = 100 level_min = 1.0 - node_id = np.full(n_datapoints, 2) level = np.linspace(0, 12, 100) flow_rate = np.square(level - level_min) / (60 * 60 * 24) - - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": node_id, - "level": level, - "flow_rate": flow_rate, - } - ) + model.tabulated_rating_curve.add( + Node(2, Point(1, 0)), + [tabulated_rating_curve.Static(level=level, flow_rate=flow_rate)], ) - # Setup terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [3], - } - ) - ) + model.terminal.add(Node(3, Point(2, 0))) - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - terminal=terminal, - tabulated_rating_curve=rating_curve, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.edge.add( + model.basin[1], + model.tabulated_rating_curve[2], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[2], + model.terminal[3], + "flow", ) return model -def manning_resistance_model(): +def manning_resistance_model() -> Model: """Set up a minimal model which uses a manning_resistance node.""" - # Set up the nodes: - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, 0.0), # 2: ManningResistance - (2.0, 0.0), # 3: Basin - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["Basin", "ManningResistance", "Basin"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2], dtype=np.int64) - to_id = np.array([2, 3], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", ) - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 1, 3, 3, 3], - "area": 2 * [0.01, 100.0, 100.0], - "level": 2 * [0.0, 1.0, 2.0], - } - ) + basin_profile = basin.Profile(area=[0.01, 100.0, 100.0], level=[0.0, 1.0, 2.0]) - state = pd.DataFrame( - data={ - "node_id": [1, 3], - "level": [9.5, 4.5], - } + model.basin.add(Node(1, Point(0, 0)), [basin_profile, basin.State(level=[9.5])]) + model.manning_resistance.add( + Node(2, Point(1, 0)), + [ + manning_resistance.Static( + manning_n=[1e7], profile_width=50.0, profile_slope=0.0, length=2000.0 + ) + ], ) + model.basin.add(Node(3, Point(2, 0)), [basin_profile, basin.State(level=[4.5])]) - basin = ribasim.Basin(profile=profile, state=state) - - # Setup the Manning resistance: - manning_resistance = ribasim.ManningResistance( - static=pd.DataFrame( - data={ - "node_id": [2], - "length": [2000.0], - "manning_n": [1e7], - "profile_width": [50.0], - "profile_slope": [0.0], - } - ) + model.edge.add( + model.basin[1], + model.manning_resistance[2], + "flow", ) - - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - manning_resistance=manning_resistance, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.edge.add( + model.manning_resistance[2], + model.basin[3], + "flow", ) return model -def misc_nodes_model(): +def misc_nodes_model() -> Model: """Set up a minimal model using flow_boundary, fractional_flow and pump nodes.""" - xy = np.array( - [ - (0.0, 0.0), # 1: FlowBoundary - (0.0, 1.0), # 2: FractionalFlow - (0.0, 2.0), # 3: Basin - (0.0, 3.0), # 4: Pump - (0.0, 4.0), # 5: Basin - (1.0, 0.0), # 6: FractionalFlow - (2.0, 0.0), # 7: Terminal - ] + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", + solver=Solver(dt=24 * 60 * 60, algorithm="Euler"), ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "FlowBoundary", - "FractionalFlow", - "Basin", - "Pump", - "Basin", - "FractionalFlow", - "Terminal", - ] - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) + basin_shared: list[TableModel[Any]] = [ + basin.Profile(area=[0.01, 100.0, 100.0], level=[0.0, 1.0, 2.0]), + basin.State(level=[10.5]), + ] - # Setup the edges: - from_id = np.array([1, 2, 3, 4, 1, 6], dtype=np.int64) - to_id = np.array([2, 3, 4, 5, 6, 7], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) + model.flow_boundary.add( + Node(1, Point(0, 0)), [flow_boundary.Static(flow_rate=[3e-4])] ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": 3 * [3] + 3 * [5], - "area": 2 * [0.01, 100.0, 100.0], - "level": 2 * [0.0, 1.0, 2.0], - } + model.fractional_flow.add( + Node(2, Point(0, 1)), [fractional_flow.Static(fraction=[0.5])] ) - - state = pd.DataFrame( - data={ - "node_id": [3, 5], - "level": 2 * [10.5], - } + model.basin.add(Node(3, Point(0, 2)), basin_shared) + model.pump.add(Node(4, Point(0, 3)), [pump.Static(flow_rate=[1e-4])]) + model.basin.add(Node(5, Point(0, 4)), basin_shared) + model.fractional_flow.add( + Node(6, Point(1, 0)), [fractional_flow.Static(fraction=[0.5])] ) + model.terminal.add(Node(7, Point(2, 0))) - basin = ribasim.Basin(profile=profile, state=state) - - # Setup flow boundary: - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame( - data={ - "node_id": [1], - "flow_rate": [3e-4], - } - ) + model.edge.add( + model.flow_boundary[1], + model.fractional_flow[2], + "flow", ) - - # Setup fractional flows: - fractional_flow = ribasim.FractionalFlow( - static=pd.DataFrame( - data={ - "node_id": [2, 6], - "fraction": [0.5, 0.5], - } - ) + model.edge.add( + model.fractional_flow[2], + model.basin[3], + "flow", ) - - # Setup pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [4], - "flow_rate": [1e-4], - } - ) + model.edge.add( + model.basin[3], + model.pump[4], + "flow", ) - - # Setup terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [7], - } - ) + model.edge.add( + model.pump[4], + model.basin[5], + "flow", ) - - # Setup solver: - solver = ribasim.Solver( - dt=24 * 24 * 60, - algorithm="Euler", + model.edge.add( + model.flow_boundary[1], + model.fractional_flow[6], + "flow", ) - - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - flow_boundary=flow_boundary, - pump=pump, - terminal=terminal, - fractional_flow=fractional_flow, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", - solver=solver, + model.edge.add( + model.fractional_flow[6], + model.terminal[7], + "flow", ) return model -def pid_control_equation_model(): +def pid_control_equation_model() -> Model: """Set up a model with pid control for an analytical solution test""" - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, 0.0), # 2: Pump - (2.0, 0.0), # 3: Terminal - (0.5, 1.0), # 4: PidControl - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["Basin", "Pump", "Terminal", "PidControl"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2, 4], dtype=np.int64) - to_id = np.array([2, 3, 2], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": ["flow", "flow", "control"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 1], - "area": [0.01, 100.0, 100.0], - "level": [0.0, 1.0, 2.0], - } - ) - - state = pd.DataFrame( - data={ - "node_id": [1], - "level": [10.5], - } - ) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [2], - "flow_rate": [0.0], # irrelevant, will be overwritten - } - ) - ) - - # Setup terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [3], - } - ) - ) - - # Setup PID control - pid_control = ribasim.PidControl( - static=pd.DataFrame( - data={ - "node_id": [4], - "listen_node_id": [1], - "target": [10.0], - "proportional": [-2.5], - "integral": [-0.001], - "derivative": [10.0], - } - ) - ) - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - pump=pump, - terminal=terminal, - pid_control=pid_control, + model = Model( starttime="2020-01-01 00:00:00", endtime="2020-01-01 00:05:00", ) + model.basin.add( + Node(1, Point(0, 0)), + [ + basin.Profile(area=[0.01, 100.0, 100.0], level=[0.0, 1.0, 2.0]), + basin.State(level=[10.5]), + ], + ) + # Pump flow_rate will be overwritten by the PidControl + model.pump.add(Node(2, Point(1, 0)), [pump.Static(flow_rate=[0.0])]) + model.terminal.add(Node(3, Point(2, 0))) + model.pid_control.add( + Node(4, Point(0.5, 1)), + [ + pid_control.Static( + listen_node_type="Basin", + listen_node_id=[1], + target=10.0, + proportional=-2.5, + integral=-0.001, + derivative=10.0, + ) + ], + ) + + model.edge.add( + model.basin[1], + model.pump[2], + "flow", + ) + model.edge.add( + model.pump[2], + model.terminal[3], + "flow", + ) + model.edge.add( + model.pid_control[4], + model.pump[2], + "control", + ) return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/invalid.py b/python/ribasim_testmodels/ribasim_testmodels/invalid.py index 7655418e7..9718d83d5 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/invalid.py +++ b/python/ribasim_testmodels/ribasim_testmodels/invalid.py @@ -1,394 +1,223 @@ -import geopandas as gpd -import numpy as np -import pandas as pd -import ribasim - - -def invalid_qh_model(): - xy = np.array( - [ - (0.0, 0.0), # 1: TabulatedRatingCurve - (0.0, 1.0), # 2: TabulatedRatingCurve, - (0.0, 2.0), # 3: Basin - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = 2 * ["TabulatedRatingCurve"] + ["Basin"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([], dtype=np.int64) - to_id = np.array([], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) +from typing import Any - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [3, 3], - "area": [0.01, 1.0], - "level": [0.0, 1.0], - } +import pandas as pd +from ribasim.config import Node +from ribasim.input_base import TableModel +from ribasim.model import Model +from ribasim.nodes import ( + basin, + discrete_control, + flow_boundary, + fractional_flow, + pump, + tabulated_rating_curve, +) +from shapely.geometry import Point + + +def invalid_qh_model() -> Model: + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-12-01 00:00:00", ) - state = pd.DataFrame(data={"node_id": [3], "level": 1.4112729908597084}) - - basin = ribasim.Basin(profile=profile, state=state) - - rating_curve_static = pd.DataFrame( + model.tabulated_rating_curve.add( + Node(1, Point(0, 0)), # Invalid: levels must not be repeated - data={"node_id": [1, 1], "level": [0.0, 0.0], "flow_rate": [1.0, 2.0]} - ) - rating_curve_time = pd.DataFrame( - data={ - "node_id": [2, 2], - "time": [ - pd.Timestamp("2020-01"), - pd.Timestamp("2020-01"), - ], - # Invalid: levels must not be repeated - "level": [0.0, 0.0], - "flow_rate": [1.0, 2.0], - } + [tabulated_rating_curve.Static(level=[0, 0], flow_rate=[1, 2])], ) - - rating_curve = ribasim.TabulatedRatingCurve( - static=rating_curve_static, time=rating_curve_time - ) - - model = ribasim.Model( - network=ribasim.Network( - edge=edge, - node=node, - ), - basin=basin, - tabulated_rating_curve=rating_curve, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.tabulated_rating_curve.add( + Node(2, Point(0, 1)), + [ + tabulated_rating_curve.Time( + time=[ + pd.Timestamp("2020-01-01 00:00:00"), + pd.Timestamp("2020-01-01 00:00:00"), + ], + # Invalid: levels must not be repeated + level=[0, 0], + flow_rate=[1, 2], + ) + ], + ) + model.basin.add( + Node(3, Point(0, 2)), + [ + basin.State(level=[1.4112729908597084]), + basin.Profile(area=[0.01, 1], level=[0, 1]), + ], ) return model -def invalid_fractional_flow_model(): - xy = np.array( - [ - (0.0, 1.0), # 1: Basin - (-1.0, 0.0), # 2: Basin - (0.0, -1.0), # 3: FractionalFlow - (1.0, 0.0), # 4: FractionalFlow - (0.0, -2.0), # 5: Terminal - (0.0, 2.0), # 6: Terminal - (0.0, 0.0), # 7: TabulatedRatingCurve - (-1.0, -1.0), # 8: FractionalFlow - ] +def invalid_fractional_flow_model() -> Model: + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-12-01 00:00:00", ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = [ - "Basin", - "Basin", - "FractionalFlow", - "FractionalFlow", - "Terminal", - "Terminal", - "TabulatedRatingCurve", - "FractionalFlow", + basin_shared: list[TableModel[Any]] = [ + basin.Profile(area=[0.01, 1.0], level=[0.0, 1.0]), + basin.State(level=[1.4112729908597084]), ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model.basin.add(Node(1, Point(0, 1)), basin_shared) + model.basin.add(Node(2, Point(-1, 0)), basin_shared) + # Invalid: fractions must be non-negative and add up to approximately 1 + model.fractional_flow.add( + Node(3, Point(0, -1)), [fractional_flow.Static(fraction=[-0.1])] ) - - # Setup the edges: - # Invalid: TabulatedRatingCurve #7 combines FractionalFlow outneighbors with other outneigbor types. - from_id = np.array([1, 7, 7, 3, 7, 4, 2], dtype=np.int64) - to_id = np.array([7, 2, 3, 5, 4, 6, 8], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) + model.fractional_flow.add( + Node(4, Point(1, 0)), [fractional_flow.Static(fraction=[0.5])] ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 2, 2], - "area": 2 * [0.01, 1.0], - "level": 2 * [0.0, 1.0], - } + model.terminal.add(Node(5, Point(0, -2))) + model.terminal.add(Node(6, Point(0, 2))) + model.tabulated_rating_curve.add( + Node(7, Point(0, 0)), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 50.0])], ) - - state = pd.DataFrame( - data={ - "node_id": [1, 2], - "level": 1.4112729908597084, - } + # Invalid: #8 comes from a Basin + model.fractional_flow.add( + Node(8, Point(-1, -1)), [fractional_flow.Static(fraction=[1.0])] ) - basin = ribasim.Basin(profile=profile, state=state) - - # Setup terminal: - terminal = ribasim.Terminal(static=pd.DataFrame(data={"node_id": [5, 6]})) - - # Setup the fractional flow: - fractional_flow = ribasim.FractionalFlow( - # Invalid: fractions must be non-negative and add up to approximately 1 - # Invalid: #8 comes from a Basin - static=pd.DataFrame(data={"node_id": [3, 4, 8], "fraction": [-0.1, 0.5, 1.0]}) + model.edge.add( + model.basin[1], + model.tabulated_rating_curve[7], + "flow", ) - - # Setup the tabulated rating curve: - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={"node_id": [7, 7], "level": [0.0, 1.0], "flow_rate": [0.0, 50.0]} - ) + # Invalid: TabulatedRatingCurve #7 combines FractionalFlow outneighbors with other outneigbor types. + model.edge.add( + model.tabulated_rating_curve[7], + model.basin[2], + "flow", ) - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - fractional_flow=fractional_flow, - tabulated_rating_curve=rating_curve, - terminal=terminal, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.edge.add( + model.tabulated_rating_curve[7], + model.fractional_flow[3], + "flow", ) - - return model - - -def invalid_discrete_control_model(): - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, 0.0), # 2: Pump - (2.0, 0.0), # 3: Basin - (3.0, 0.0), # 4: FlowBoundary - (1.0, 1.0), # 5: DiscreteControl - ] + model.edge.add( + model.fractional_flow[3], + model.terminal[5], + "flow", ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["Basin", "Pump", "Basin", "FlowBoundary", "DiscreteControl"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model.edge.add( + model.tabulated_rating_curve[7], + model.fractional_flow[4], + "flow", ) - - # Setup the edges: - from_id = np.array([1, 2, 4, 5], dtype=np.int64) - to_id = np.array([2, 3, 3, 2], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": ["flow", "flow", "flow", "control"], - }, - geometry=lines, - crs="EPSG:28992", - ) + model.edge.add( + model.fractional_flow[4], + model.terminal[6], + "flow", ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 3, 3], - "area": 2 * [0.01, 1.0], - "level": 2 * [0.0, 1.0], - } - ) - - state = pd.DataFrame( - data={ - "node_id": [1, 3], - "level": 1.4112729908597084, - } + model.edge.add( + model.basin[2], + model.fractional_flow[8], + "flow", ) - basin = ribasim.Basin(profile=profile, state=state) - - # Setup pump: - pump = ribasim.Pump( - static=pd.DataFrame( - # Invalid: DiscreteControl node #4 with control state 'foo' - # points to this pump but this control state is not defined for - # this pump. The pump having a control state that is not defined - # for DiscreteControl node #4 is fine. - data={ - "control_state": ["bar"], - "node_id": [2], - "flow_rate": [0.5 / 3600], - } - ) - ) + return model - # Setup level boundary: - flow_boundary = ribasim.FlowBoundary( - time=pd.DataFrame( - data={ - "node_id": 2 * [4], - "flow_rate": [1.0, 2.0], - "time": ["2020-01-01 00:00:00", "2020-11-01 00:00:00"], - } - ) - ) - # Setup the discrete control: - condition = pd.DataFrame( - data={ - "node_id": 3 * [5], - "listen_feature_id": [1, 4, 4], - "variable": ["level", "flow_rate", "flow_rate"], - "greater_than": [0.5, 1.5, 1.5], - # Invalid: look_ahead can only be specified for timeseries variables. - # Invalid: this look_ahead will go past the provided timeseries during simulation. - # Invalid: look_ahead must be non-negative. - "look_ahead": [100.0, 40 * 24 * 60 * 60, -10.0], - } +def invalid_discrete_control_model() -> Model: + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-12-01 00:00:00", ) - logic = pd.DataFrame( - data={ - "node_id": [5], + basin_shared: list[TableModel[Any]] = [ + basin.Profile(area=[0.01, 1.0], level=[0.0, 1.0]), + basin.State(level=[1.4112729908597084]), + ] + model.basin.add(Node(1, Point(0, 0)), basin_shared) + # Invalid: DiscreteControl node #4 with control state 'foo' + # points to this pump but this control state is not defined for + # this pump. The pump having a control state that is not defined + # for DiscreteControl node #4 is fine. + model.pump.add( + Node(2, Point(1, 0)), [pump.Static(control_state="bar", flow_rate=[0.5 / 3600])] + ) + model.basin.add(Node(3, Point(2, 0)), basin_shared) + model.flow_boundary.add( + Node(4, Point(3, 0)), + [ + flow_boundary.Time( + time=["2020-01-01 00:00:00", "2020-11-01 00:00:00"], + flow_rate=[1.0, 2.0], + ) + ], + ) + model.discrete_control.add( + Node(5, Point(1, 1)), + [ + discrete_control.Condition( + listen_node_type=["Basin", "FlowBoundary", "FlowBoundary"], + listen_node_id=[1, 4, 4], + variable=["level", "flow_rate", "flow_rate"], + greater_than=[0.5, 1.5, 1.5], + # Invalid: look_ahead can only be specified for timeseries variables. + # Invalid: this look_ahead will go past the provided timeseries during simulation. + # Invalid: look_ahead must be non-negative. + look_ahead=[100.0, 40 * 24 * 60 * 60, -10.0], + ), # Invalid: DiscreteControl node #4 has 2 conditions so # truth states have to be of length 2 - "truth_state": ["FFFF"], - "control_state": ["foo"], - } + discrete_control.Logic(truth_state=["FFFF"], control_state=["foo"]), + ], ) - discrete_control = ribasim.DiscreteControl(condition=condition, logic=logic) - - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - pump=pump, - flow_boundary=flow_boundary, - discrete_control=discrete_control, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.edge.add( + model.basin[1], + model.pump[2], + "flow", + ) + model.edge.add( + model.pump[2], + model.basin[3], + "flow", + ) + model.edge.add( + model.flow_boundary[4], + model.basin[3], + "flow", + ) + model.edge.add( + model.discrete_control[5], + model.pump[2], + "control", ) return model -def invalid_edge_types_model(): +def invalid_edge_types_model() -> Model: """Set up a minimal model with invalid edge types.""" - xy = np.array( - [ - (0.0, 0.0), # 1: Basin - (1.0, 0.0), # 2: Pump - (2.0, 0.0), # 3: Basin - ] - ) - - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["Basin", "Pump", "Basin"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2], dtype=np.int64) - to_id = np.array([2, 3], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": ["foo", "bar"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [1, 1, 3, 3], - "area": [0.01, 1000.0] * 2, - "level": [0.0, 1.0] * 2, - } + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-12-01 00:00:00", ) - state = pd.DataFrame( - data={ - "node_id": [1, 3], - "level": 0.04471158417652035, - } - ) + basin_shared: list[TableModel[Any]] = [ + basin.Profile(area=[0.01, 1000.0], level=[0.0, 1.0]), + basin.State(level=[0.04471158417652035]), + ] - basin = ribasim.Basin(profile=profile, state=state) + model.basin.add(Node(1, Point(0, 0)), basin_shared) + model.pump.add(Node(2, Point(1, 0)), [pump.Static(flow_rate=[0.5 / 3600])]) + model.basin.add(Node(3, Point(2, 0)), basin_shared) - # Setup pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [2], - "flow_rate": [0.5 / 3600], - } - ) + model.edge.add( + model.basin[1], + model.pump[2], + "foo", ) - - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - pump=pump, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.edge.add( + model.pump[2], + model.basin[3], + "bar", ) return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/pid_control.py b/python/ribasim_testmodels/ribasim_testmodels/pid_control.py index 0a8496092..5846f1b06 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/pid_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/pid_control.py @@ -1,310 +1,177 @@ -import geopandas as gpd -import numpy as np -import pandas as pd -import ribasim - - -def pid_control_model(): +from ribasim.config import Node +from ribasim.model import Model +from ribasim.nodes import ( + basin, + discrete_control, + flow_boundary, + level_boundary, + outlet, + pid_control, + pump, + tabulated_rating_curve, +) +from shapely.geometry import Point + + +def pid_control_model() -> Model: """Set up a basic model with a PID controlled pump controlling a basin with abundant inflow.""" - xy = np.array( - [ - (0.0, 0.0), # 1: FlowBoundary - (1.0, 0.0), # 2: Basin - (2.0, 0.5), # 3: Pump - (3.0, 0.0), # 4: LevelBoundary - (1.5, 1.0), # 5: PidControl - (2.0, -0.5), # 6: Outlet - (1.5, -1.0), # 7: PidControl - ] - ) - - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "FlowBoundary", - "Basin", - "Pump", - "LevelBoundary", - "PidControl", - "Outlet", - "PidControl", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2, 3, 4, 6, 5, 7], dtype=np.int64) - to_id = np.array([2, 3, 4, 6, 2, 3, 6], dtype=np.int64) - - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": 5 * ["flow"] + 2 * ["control"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={"node_id": [2, 2], "level": [0.0, 1.0], "area": [1000.0, 1000.0]} - ) - - state = pd.DataFrame( - data={ - "node_id": [2], - "level": [6.0], - } - ) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup pump: - pump = ribasim.Pump( - static=pd.DataFrame( - data={ - "node_id": [3], - "flow_rate": [0.0], # Will be overwritten by PID controller - } - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-12-01 00:00:00", ) - # Setup outlet: - outlet = ribasim.Outlet( - static=pd.DataFrame( - data={ - "node_id": [6], - "flow_rate": [0.0], # Will be overwritten by PID controller - } - ) + model.flow_boundary.add( + Node(1, Point(0, 0)), [flow_boundary.Static(flow_rate=[1e-3])] ) - - # Setup flow boundary: - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame(data={"node_id": [1], "flow_rate": [1e-3]}) + model.basin.add( + Node(2, Point(1, 0)), + [basin.Profile(area=1000.0, level=[0.0, 1.0]), basin.State(level=[6.0])], ) + # Flow rate will be overwritten by PID controller + model.pump.add(Node(3, Point(2, 0.5)), [pump.Static(flow_rate=[0.0])]) - # Setup level boundary: - level_boundary = ribasim.LevelBoundary( - static=pd.DataFrame( - data={ - "node_id": [4], - "level": [5.0], # Not relevant - } - ) - ) + model.level_boundary.add(Node(4, Point(3, 0)), [level_boundary.Static(level=[5.0])]) - # Setup PID control: - pid_control = ribasim.PidControl( - time=pd.DataFrame( - data={ - "node_id": 4 * [5, 7], - "time": [ - "2020-01-01 00:00:00", + model.pid_control.add( + Node(5, Point(1.5, 1)), + [ + pid_control.Time( + time=[ "2020-01-01 00:00:00", "2020-05-01 00:00:00", - "2020-05-01 00:00:00", - "2020-07-01 00:00:00", "2020-07-01 00:00:00", "2020-12-01 00:00:00", + ], + listen_node_type="Basin", + listen_node_id=2, + target=[5.0, 5.0, 7.5, 7.5], + proportional=-1e-3, + integral=-1e-7, + derivative=0.0, + ) + ], + ) + + # Flow rate will be overwritten by PID controller + model.outlet.add(Node(6, Point(2, -0.5)), [outlet.Static(flow_rate=[0.0])]) + model.pid_control.add( + Node(7, Point(1.5, -1)), + [ + pid_control.Time( + time=[ + "2020-01-01 00:00:00", + "2020-05-01 00:00:00", + "2020-07-01 00:00:00", "2020-12-01 00:00:00", ], - "listen_node_id": 4 * [2, 2], - "target": [5.0, 5.0, 5.0, 5.0, 7.5, 7.5, 7.5, 7.5], - "proportional": 4 * [-1e-3, 1e-3], - "integral": 4 * [-1e-7, 1e-7], - "derivative": 4 * [0.0, 0.0], - } - ) - ) - - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - flow_boundary=flow_boundary, - level_boundary=level_boundary, - pump=pump, - outlet=outlet, - pid_control=pid_control, - starttime="2020-01-01 00:00:00", - endtime="2020-12-01 00:00:00", - ) + listen_node_type="Basin", + listen_node_id=2, + target=[5.0, 5.0, 7.5, 7.5], + proportional=1e-3, + integral=1e-7, + derivative=0.0, + ) + ], + ) + + model.edge.add(model.flow_boundary[1], model.basin[2], "flow") + model.edge.add(model.basin[2], model.pump[3], "flow") + model.edge.add(model.pump[3], model.level_boundary[4], "flow") + model.edge.add(model.level_boundary[4], model.outlet[6], "flow") + model.edge.add(model.pid_control[5], model.pump[3], "control") + model.edge.add(model.outlet[6], model.basin[2], "flow") + model.edge.add(model.pid_control[7], model.outlet[6], "control") return model -def discrete_control_of_pid_control_model(): +def discrete_control_of_pid_control_model() -> Model: """Set up a basic model where a discrete control node sets the target level of a pid control node.""" - xy = np.array( - [ - (0.0, 0.0), # 1: LevelBoundary - (1.0, 0.0), # 2: Pump - (2.0, 0.0), # 3: Basin - (3.0, 0.0), # 4: TabulatedRatingCurve - (4.0, 0.0), # 5: Terminal - (1.0, 1.0), # 6: PidControl - (0.0, 1.0), # 7: DiscreteControl - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = [ - "LevelBoundary", - "Outlet", - "Basin", - "TabulatedRatingCurve", - "Terminal", - "PidControl", - "DiscreteControl", - ] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 2, 3, 4, 6, 7], dtype=np.int64) - to_id = np.array([2, 3, 4, 5, 2, 6], dtype=np.int64) - - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": 4 * ["flow"] + 2 * ["control"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={"node_id": [3, 3], "level": [0.0, 1.0], "area": [1000.0, 1000.0]} - ) - - state = pd.DataFrame( - data={ - "node_id": [3], - "level": [6.0], - } - ) - - basin = ribasim.Basin(profile=profile, state=state) - - # Setup pump: - outlet = ribasim.Outlet( - static=pd.DataFrame( - data={ - "node_id": [2], - "flow_rate": [0.0], # Will be overwritten by PID controller - } - ) - ) - - # Set up a rating curve node: - # Discharge: lose 1% of storage volume per day at storage = 1000.0. - seconds_in_day = 24 * 3600 - q1000 = 1000.0 * 0.01 / seconds_in_day - - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [4, 4], - "level": [0.0, 1.0], - "flow_rate": [0.0, q1000], - } - ) - ) - - # Setup level boundary: - level_boundary = ribasim.LevelBoundary( - time=pd.DataFrame( - data={ - "node_id": [1, 1], - "time": ["2020-01-01 00:00:00", "2021-01-01 00:00:00"], - "level": [7.0, 3.0], - } - ) + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2020-12-01 00:00:00", ) - # Setup terminal: - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [5], - } - ) + model.level_boundary.add( + Node(1, Point(0, 0)), + [ + level_boundary.Time( + time=["2020-01-01 00:00:00", "2021-01-01 00:00:00"], level=[7.0, 3.0] + ) + ], ) - # Setup PID control: - pid_control = ribasim.PidControl( - static=pd.DataFrame( - data={ - "node_id": [6, 6], - "control_state": ["target_high", "target_low"], - "listen_node_id": [3, 3], - "target": [5.0, 3.0], - "proportional": 2 * [1e-2], - "integral": 2 * [1e-8], - "derivative": 2 * [-1e-1], - } - ) + # The flow_rate will be overwritten by PID controller + model.outlet.add(Node(2, Point(1, 0)), [outlet.Static(flow_rate=[0.0])]) + model.basin.add( + Node(3, Point(2, 0)), + [basin.State(level=[6.0]), basin.Profile(area=1000.0, level=[0.0, 1.0])], ) - - # Setup discrete control: - discrete_control = ribasim.DiscreteControl( - condition=pd.DataFrame( - data={ - "node_id": [7], - "listen_feature_id": [1], - "variable": ["level"], - "greater_than": [5.0], - } - ), - logic=pd.DataFrame( - data={ - "node_id": [7, 7], - "truth_state": ["T", "F"], - "control_state": ["target_high", "target_low"], - } - ), + model.tabulated_rating_curve.add( + Node(4, Point(3, 0)), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 10 / 86400])], ) - - # Setup a model: - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - outlet=outlet, - tabulated_rating_curve=rating_curve, - level_boundary=level_boundary, - terminal=terminal, - pid_control=pid_control, - discrete_control=discrete_control, - starttime="2020-01-01 00:00:00", - endtime="2020-12-01 00:00:00", + model.terminal.add(Node(5, Point(4, 0))) + model.pid_control.add( + Node(6, Point(1, 1)), + [ + pid_control.Static( + listen_node_type="Basin", + listen_node_id=3, + control_state=["target_high", "target_low"], + target=[5.0, 3.0], + proportional=1e-2, + integral=1e-8, + derivative=-1e-1, + ) + ], + ) + model.discrete_control.add( + Node(7, Point(0, 1)), + [ + discrete_control.Condition( + listen_node_type="LevelBoundary", + listen_node_id=[1], + variable="level", + greater_than=5.0, + ), + discrete_control.Logic( + truth_state=["T", "F"], control_state=["target_high", "target_low"] + ), + ], + ) + + model.edge.add( + model.level_boundary[1], + model.outlet[2], + "flow", + ) + model.edge.add( + model.outlet[2], + model.basin[3], + "flow", + ) + model.edge.add( + model.basin[3], + model.tabulated_rating_curve[4], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[4], + model.terminal[5], + "flow", + ) + model.edge.add( + model.pid_control[6], + model.outlet[2], + "control", + ) + model.edge.add( + model.discrete_control[7], + model.pid_control[6], + "control", ) return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/time.py b/python/ribasim_testmodels/ribasim_testmodels/time.py index 814a8272c..5b30a5815 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/time.py +++ b/python/ribasim_testmodels/ribasim_testmodels/time.py @@ -1,98 +1,53 @@ -import geopandas as gpd import numpy as np import pandas as pd -import ribasim +from ribasim.config import Node +from ribasim.model import Model +from ribasim.nodes import basin, flow_boundary +from shapely.geometry import Point -def flow_boundary_time_model(): +def flow_boundary_time_model() -> Model: """Set up a minimal model with time-varying flow boundary""" - # Set up the nodes: - - xy = np.array( - [ - (0.0, 0.0), # 1: FlowBoundary - (1.0, 0.0), # 2: Basin - (2.0, 0.0), # 3: FlowBoundary - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - node_type = ["FlowBoundary", "Basin", "FlowBoundary"] - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(np.arange(len(xy)) + 1, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([1, 3], dtype=np.int64) - to_id = np.array([2, 2], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id, to_id) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [2, 2], - "area": [0.01, 1000.0], - "level": [0.0, 1.0], - } + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", ) - state = pd.DataFrame( - data={ - "node_id": [2], - "level": 0.04471158417652035, - } + model.flow_boundary.add( + Node(3, Point(0, 0)), [flow_boundary.Static(flow_rate=[1.0])] ) - basin = ribasim.Basin(profile=profile, state=state) - n_times = 100 time = pd.date_range( start="2020-03-01 00:00:00", end="2020-10-01 00:00:00", periods=n_times ).astype("datetime64[s]") flow_rate = 1 + np.sin(np.pi * np.linspace(0, 0.5, n_times)) ** 2 - # Setup flow boundary: - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame( - data={ - "node_id": [3], - "flow_rate": [1.0], - } - ), - time=pd.DataFrame( - data={ - "node_id": n_times * [1], - "time": time, - "flow_rate": flow_rate, - } - ), + model.flow_boundary.add( + Node(1, Point(2, 0)), [flow_boundary.Time(time=time, flow_rate=flow_rate)] ) - model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - flow_boundary=flow_boundary, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.basin.add( + Node(2, Point(1, 0)), + [ + basin.Profile( + area=[0.01, 1000.0], + level=[0.0, 1.0], + ), + basin.State(level=[0.04471158417652035]), + ], + ) + + model.edge.add( + model.flow_boundary[1], + model.basin[2], + "flow", + ) + model.edge.add( + model.flow_boundary[3], + model.basin[2], + "flow", ) return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/trivial.py b/python/ribasim_testmodels/ribasim_testmodels/trivial.py index 4a35b6c70..813434835 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/trivial.py +++ b/python/ribasim_testmodels/ribasim_testmodels/trivial.py @@ -1,126 +1,61 @@ -import geopandas as gpd -import numpy as np -import pandas as pd -import ribasim +from ribasim.config import Node, Results +from ribasim.model import Model +from ribasim.nodes import basin, tabulated_rating_curve +from shapely.geometry import Point -def trivial_model() -> ribasim.Model: +def trivial_model() -> Model: """Trivial model with just a basin, tabulated rating curve and terminal node""" - # largest signed 64 bit integer, to check encoding - terminal_id = 9223372036854775807 - xy = np.array( - [ - (400.0, 200.0), # 6: Basin - (450.0, 200.0), # 0: TabulatedRatingCurve - (500.0, 200.0), # : Terminal - ] - ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - node_type = [ - "Basin", - "TabulatedRatingCurve", - "Terminal", - ] - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index([6, 0, terminal_id], name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) - ) - - # Setup the edges: - from_id = np.array([6, 0], dtype=np.int64) - to_id = np.array([0, terminal_id], dtype=np.int64) - lines = node.geometry_from_connectivity(from_id.tolist(), to_id.tolist()) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - index=pd.Index([11, 9], name="fid"), - geometry=lines, - crs="EPSG:28992", - ) - ) - - # Setup the basins: - profile = pd.DataFrame( - data={ - "node_id": [6, 6], - "area": [0.01, 1000.0], - "level": [0.0, 1.0], - } + model = Model( + starttime="2020-01-01 00:00:00", + endtime="2021-01-01 00:00:00", + results=Results(subgrid=True, compression=False), ) # Convert steady forcing to m/s # 2 mm/d precipitation, 1 mm/d evaporation - seconds_in_day = 24 * 3600 - precipitation = 0.002 / seconds_in_day - evaporation = 0.001 / seconds_in_day - - static = pd.DataFrame( - data={ - "node_id": [6], - "potential_evaporation": [evaporation], - "precipitation": [precipitation], - } - ) - - state = pd.DataFrame(data={"node_id": [6], "level": 0.04471158417652035}) + precipitation = 0.002 / 86400 + potential_evaporation = 0.001 / 86400 # Create a subgrid level interpolation from one basin to three elements. Scale one to one, but: - # # 22. start at -1.0 # 11. start at 0.0 # 33. start at 1.0 - # - subgrid = pd.DataFrame( - data={ - "subgrid_id": [22, 22, 11, 11, 33, 33], - "node_id": [6, 6, 6, 6, 6, 6], - "basin_level": [0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - "subgrid_level": [-1.0, 0.0, 0.0, 1.0, 1.0, 2.0], - } - ) - basin = ribasim.Basin(profile=profile, static=static, state=state, subgrid=subgrid) - - # Set up a rating curve node: - # Discharge: lose 1% of storage volume per day at storage = 1000.0. - q1000 = 1000.0 * 0.01 / seconds_in_day - - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [0, 0], - "level": [0.0, 1.0], - "flow_rate": [0.0, q1000], - } - ) - ) - - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [terminal_id], - } - ) + model.basin.add( + Node(6, Point(400, 200)), + [ + basin.Static( + precipitation=[precipitation], + potential_evaporation=[potential_evaporation], + ), + basin.Profile(area=[0.01, 1000.0], level=[0.0, 1.0]), + basin.State(level=[0.04471158417652035]), + basin.Subgrid( + subgrid_id=[22, 22, 11, 11, 33, 33], + basin_level=[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], + subgrid_level=[-1.0, 0.0, 0.0, 1.0, 1.0, 2.0], + ), + ], + ) + + # TODO largest signed 64 bit integer, to check encoding + terminal_id = 922 # 3372036854775807 + model.terminal.add(Node(terminal_id, Point(500, 200))) + model.tabulated_rating_curve.add( + Node(0, Point(450, 200)), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 10 / 86400])], + ) + + model.edge.add( + model.basin[6], + model.tabulated_rating_curve[0], + "flow", + ) + model.edge.add( + model.tabulated_rating_curve[0], + model.terminal[terminal_id], + "flow", ) - model = ribasim.Model( - network=ribasim.Network( - node=node, - edge=edge, - ), - basin=basin, - terminal=terminal, - tabulated_rating_curve=rating_curve, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", - results=ribasim.Results(subgrid=True, compression=False), - ) return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/two_basin.py b/python/ribasim_testmodels/ribasim_testmodels/two_basin.py index 7063c7c57..1a4021646 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/two_basin.py +++ b/python/ribasim_testmodels/ribasim_testmodels/two_basin.py @@ -1,10 +1,13 @@ -import geopandas as gpd -import numpy as np -import pandas as pd -import ribasim +from typing import Any +from ribasim.config import Node +from ribasim.input_base import TableModel +from ribasim.model import Model +from ribasim.nodes import basin, flow_boundary, tabulated_rating_curve +from shapely.geometry import Point -def two_basin_model() -> ribasim.Model: + +def two_basin_model() -> Model: """ Create a model of two basins. @@ -15,102 +18,57 @@ def two_basin_model() -> ribasim.Model: infiltrates in the left basin, and exfiltrates in the right basin. The right basin fills up and discharges over the rating curve. """ - flow_boundary = ribasim.FlowBoundary( - static=pd.DataFrame( - data={ - "node_id": [1], - "flow_rate": [1e-2], - } - ) - ) - xy = np.array( - [ - (0, 0.0), # FlowBoundary - (250.0, 0.0), # Basin 1 - (750.0, 0.0), # Basin 2 - (1000.00, 0.0), # TabulatedRatingCurve - (1100.00, 0.0), # Terminal - ] - ) - # Rectangular profile - profile = pd.DataFrame( - data={ - "node_id": [2, 2, 3, 3], - "area": [400.0, 400.0, 400.0, 400.0], - "level": [0.0, 1.0, 0.0, 1.0], - } - ) - state = pd.DataFrame(data={"node_id": [2, 3], "level": [0.01, 0.01]}) - subgrid = pd.DataFrame( - data={ - "node_id": [2, 2, 3, 3], - "subgrid_id": [1, 1, 2, 2], - "basin_level": [0.0, 1.0, 0.0, 1.0], - "subgrid_level": [0.0, 1.0, 0.0, 1.0], - "meta_x": [250.0, 250.0, 750.0, 750.0], - "meta_y": [0.0, 0.0, 0.0, 0.0], - } - ) - basin = ribasim.Basin(profile=profile, state=state, subgrid=subgrid) + model = Model(starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00") - rating_curve = ribasim.TabulatedRatingCurve( - static=pd.DataFrame( - data={ - "node_id": [4, 4], - "level": [0.0, 1.0], - "flow_rate": [0.0, 0.01], - } - ) + model.flow_boundary.add( + Node(1, Point(0, 0)), [flow_boundary.Static(flow_rate=[1e-2])] ) - - terminal = ribasim.Terminal( - static=pd.DataFrame( - data={ - "node_id": [5], - } - ) + basin_shared: list[TableModel[Any]] = [ + basin.Profile(area=400.0, level=[0.0, 1.0]), + basin.State(level=[0.01]), + ] + model.basin.add( + Node(2, Point(250, 0)), + [ + *basin_shared, + basin.Subgrid( + subgrid_id=1, + basin_level=[0.0, 1.0], + subgrid_level=[0.0, 1.0], + meta_x=250.0, + meta_y=0.0, + ), + ], ) - node_id, node_type = ribasim.Node.node_ids_and_types( - basin, - rating_curve, - flow_boundary, - terminal, + model.basin.add( + Node(3, Point(750, 0)), + [ + *basin_shared, + basin.Subgrid( + subgrid_id=2, + basin_level=[0.0, 1.0], + subgrid_level=[0.0, 1.0], + meta_x=750.0, + meta_y=0.0, + ), + ], ) - node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1]) - - # Make sure the feature id starts at 1: explicitly give an index. - node = ribasim.Node( - df=gpd.GeoDataFrame( - data={"node_type": node_type}, - index=pd.Index(node_id, name="fid"), - geometry=node_xy, - crs="EPSG:28992", - ) + model.tabulated_rating_curve.add( + Node(4, Point(1000, 0)), + [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 0.01])], ) + model.terminal.add(Node(5, Point(1100, 0))) - from_id = np.array([1, 3, 4], dtype=np.int64) - to_id = np.array([2, 4, 5], dtype=np.int64) - lines = node.geometry_from_connectivity([1, 3, 4], [2, 4, 5]) - edge = ribasim.Edge( - df=gpd.GeoDataFrame( - data={ - "from_node_id": from_id, - "to_node_id": to_id, - "edge_type": len(from_id) * ["flow"], - }, - geometry=lines, - crs="EPSG:28992", - ) + model.edge.add(model.flow_boundary[1], model.basin[2], "flow") + model.edge.add( + model.basin[3], + model.tabulated_rating_curve[4], + "flow", ) - - ribasim_model = ribasim.Model( - network=ribasim.Network(node=node, edge=edge), - basin=basin, - flow_boundary=flow_boundary, - tabulated_rating_curve=rating_curve, - terminal=terminal, - starttime="2020-01-01 00:00:00", - endtime="2021-01-01 00:00:00", + model.edge.add( + model.tabulated_rating_curve[4], + model.terminal[5], + "flow", ) - return ribasim_model + return model diff --git a/utils/templates/model.py.jinja b/utils/templates/model.py.jinja index b02a9f8fe..db2a8df50 100644 --- a/utils/templates/model.py.jinja +++ b/utils/templates/model.py.jinja @@ -13,7 +13,11 @@ class _BaseSchema(pa.DataFrameModel): {% for m in models %} class {{m[:name]}}Schema(_BaseSchema): -{% for f in m[:fields] %} + {% for f in m[:fields] %} + {% if (f[2] == "Series[int]") %} + {{ f[1] }}: {{ f[2] }} = pa.Field(nullable={{ f[3] }}, default=0) + {% else %} {{ f[1] }}: {{ f[2] }} = pa.Field(nullable={{ f[3] }}) -{% end %} + {% end %} + {% end %} {% end %}