From cae6838bf3917b0dd861ff80ebb186f62987134b Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Thu, 19 Sep 2024 23:07:13 +0200 Subject: [PATCH] Run quick start in docs (#1811) Fixes #1807. Rather than having docs with code that doesn't run and output in static images, this intends to run the notebook as part of the docs generation just like example.ipynb. I am also making more edits which I'll describe later. --------- Co-authored-by: Bart de Koning --- .pre-commit-config.yaml | 1 - .vscode/settings.json | 5 +- docs/.gitignore | 1 + docs/_quarto.yml | 11 +- docs/guide/examples.ipynb | 13 - docs/guide/quickstart.qmd | 761 -------------------------- docs/install.qmd | 3 +- docs/tutorial/irrigation-demand.ipynb | 440 +++++++++++++++ docs/tutorial/natural-flow.ipynb | 515 +++++++++++++++++ docs/tutorial/reservoir.ipynb | 369 +++++++++++++ pixi.toml | 10 +- 11 files changed, 1340 insertions(+), 789 deletions(-) delete mode 100644 docs/guide/quickstart.qmd create mode 100644 docs/tutorial/irrigation-demand.ipynb create mode 100644 docs/tutorial/natural-flow.ipynb create mode 100644 docs/tutorial/reservoir.ipynb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 545ad054f..00ab53872 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,6 @@ repos: hooks: - id: check-added-large-files - id: check-case-conflict - - id: check-json - id: check-yaml - id: check-toml - id: check-merge-conflict diff --git a/.vscode/settings.json b/.vscode/settings.json index b634ff85e..3865d5886 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,8 +4,9 @@ }, "notebook.formatOnSave.enabled": false, "notebook.codeActionsOnSave": { - "source.fixAll.ruff": true, - "source.organizeImports.ruff": true + // https://github.com/astral-sh/ruff-vscode/issues/593 + "source.fixAll.ruff": "never", + "source.organizeImports.ruff": "never" }, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", diff --git a/docs/.gitignore b/docs/.gitignore index cd2d14289..f9379ecd7 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -2,6 +2,7 @@ /_site/ /site_libs/ /reference/python/ +/tutorial/crystal-basin/ guide/data/ *.html objects.json diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 741e0fe6b..11645a750 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -10,8 +10,10 @@ website: left: - text: "Overview" file: index.qmd + - text: "Tutorials" + file: tutorial/natural-flow.ipynb - text: "How-to guides" - file: guide/quickstart.qmd + file: guide/examples.ipynb - text: "Concepts" file: concept/concept.qmd - text: "Reference" @@ -31,9 +33,14 @@ website: - changelog.qmd - known_issues.qmd + - title: "Tutorials" + contents: + - tutorial/natural-flow.ipynb + - tutorial/irrigation-demand.ipynb + - tutorial/reservoir.ipynb + - title: "How-to guides" contents: - - guide/quickstart.qmd - guide/examples.ipynb - guide/qgis.qmd - guide/coupling.qmd diff --git a/docs/guide/examples.ipynb b/docs/guide/examples.ipynb index 0c967f7d0..4bdcfcbf1 100644 --- a/docs/guide/examples.ipynb +++ b/docs/guide/examples.ipynb @@ -405,19 +405,6 @@ "ribasim basic/ribasim.toml\n", "```\n", "\n", - "From Python you can run it with:\n", - "\n", - "```python\n", - "import subprocess\n", - "result = subprocess.run([cli_path, toml_path], capture_output=True, encoding=\"utf-8\")\n", - "print(result.stderr)\n", - "result.check_returncode()\n", - "```\n", - "\n", - "Where `cli_path` is a string with either the full path to the Ribasim executable, like `r\"c:\\ribasim_windows\\ribasim\"`, or just `\"ribasim\"` in case you added the `ribasim_windows` folder to your PATH.\n", - "\n", - "The `print(result.stderr)` ensures you see the same logging and error messages that you would see in the terminal. And `result.check_returncode()` will throw an error when the simulation was not successful.\n", - "\n", "After running the model, read back the results:" ] }, diff --git a/docs/guide/quickstart.qmd b/docs/guide/quickstart.qmd deleted file mode 100644 index 1b90e0c05..000000000 --- a/docs/guide/quickstart.qmd +++ /dev/null @@ -1,761 +0,0 @@ ---- -title: "Quick start guide" ---- - -![](https://s3.deltares.nl/ribasim/doc-image/quickstart/cover.png){fig-align="left"} - -# Introduction -Welcome to Ribasim! -This guide will help you get started with the basics of installing and using Ribasim for river basin simulation. -In this guide, the schematization of models will be implemented in Python using the Ribasim Python package. -The Ribasim package (named `ribasim`) simplifies the process of building, updating, and analyzing Ribasim model programmatically. -It also allows for the creation of entire models from base data, ensuring that your model setup is fully reproducible. -This package is available on PyPI. - -## Learning objectives -In this guide, we will focus on a fictional river basin called Crystal, which will serve as our case study. -The guide is divided into different modules, each covering various scenarios. -These include simulating natural flow, implementing reservoirs, and observing the impact of other structures. -While not all node types and possibilities will be demonstrated, the focus will be on the most commonly used and significant situations. -By the end of the guide, users will be able to: - -- **Set Up a Basic Ribasim Model**: Understand how to create a new model for a river basin using the Ribasim Python package. -- **Evaluate the Impact of Demands**: Introduce water demand (such as irrigation) and assess their effects on the river basin. -- **Modify and Update Models**: Learn how to update existing models with new data and changes. -- **Analyze Simulation Results**: Use built-in tools to analyze and interpret the results of your simulations. - -# Starting RIBASIM -## System requirements -Before installing Ribasim, ensure your system meets the following requirements: - -- Operating System: Windows 10 or later, or Linux (latest distributions) -- Processor: x86-64 (64-bit) -- RAM: 4 GB minimum, 8 GB recommended -- Hard Drive: 1GB of free space - -## Installation -1. Download Ribasim: Obtain the Ribasim 9 installation package from the official website: [Ribasim - Installation](https://ribasim.org/install.html) under chapter '2 Download': - -* For Windows download: `ribasim_windows.zip` -* For Linux: `ribasim_linux.zip` - -2. Unpack the `.zip` archive: It is important to keep the contents of the zip file organized within a single directory. The Ribasim executable can be found in the directory; -3. Check installation: To check whether the installation was performed successfully, in `cmd` go to the executable path and type `ribasim` with no arguments in the command line. This will give the following message: - -```batch -error: the following required arguments were not provided: - -Usage: ribasim -For more information, try '--help'. -``` - -4. We use a command line interface (CLI) to install our Ribasim packages. To install Ribasim open PowerShell or Windows command prompt and write: - -```sh -conda install ribasim -``` -or - -```sh -mamba install ribasim -``` -## Data preparation -Download the `Crystal_Basin.zip` file from the website. Extract `Crystal_Basin.zip` and place it in the same directory as your Ribasim installation. This folder includes: - -- `QuickStartGuide.pdf` -- `data`: Contains data inputs such as time series needed for running the case. -Additionally, your Python model (`.py`) and eventually the output files will also be saved in this folder. - -# Modual 1 - Crystal River Basin -We will examine a straightforward example of the Crystal River Basin, which includes a main river and a single tributary flowing into the sea (see @fig-crystal-basin). -An average discharge of $44.45 \text{ m}^3/\text{s}$ is measured at the confluence. -In this module, the basin is free of any activities, allowing the model to simulate the natural flow. -The next step is to include a demand (irrigation) that taps from a canal out of the main river. - -![Crystal Basin based on natural flow](https://s3.deltares.nl/ribasim/doc-image/quickstart/Crystal-Basin-based-on-natural-flow.png){fig-align="left" #fig-crystal-basin} - -After this module the user will be able to: - -- Build a river basin model from scratch -- Understand the functionality of the ‘demand’ and ‘basin’ nodes -- Generate overview of results -- Evaluate the simulation results - -## Modual 1.1 - Natural Flow -### Step 1: Import packages -Before building the model we need to import some modules. -Open your python platform (Spyder, VS code etc.), created a new file and name it `Crystal_1.1` and save it into your model folder `Crystal_Basin`. -Import the following modules in python: - -```python -import shutil -from pathlib import Path -import pathlib -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from ribasim import Allocation, Model, Node # The main library used for river basin modeling. -from ribasim.nodes import ( - flow_boundary, - basin, - tabulated_rating_curve, - terminal -) -from shapely.geometry import Point -import subprocess # For running the model -``` -### Step 2: Setup paths and model configuration -Reference the paths of the Ribasim installation and model directory and define the time period (2022-01-01 until 2023-01-01) for the model simulation: -```python -base_dir = Path("c:/Ribasim") -model_dir = base_dir / "Crystal_Basin" -data_path = model_dir / "data/input/ACTINFLW.csv" - -starttime = "2022-01-01" -endtime = "2023-01-01" -model = Model( - starttime=starttime, - endtime=endtime, - crs="EPSG:4326", -) -``` -### Step 3: Flow boundary nodes -Crystal Basin consists of two inflow points, the tributary and the main Crystal river, we will call them `Minor` and `Main` respectively. -In order to define the time series flow rate ($\text{m}^3/\text{s}$) we read the discharge data from `ACTINFLW.csv`. -This inflow data goes monthly from 2014 to 2023. -However, for this exercise actual runtime is already defined in step 2. - -```python -data = pd.read_csv(data_path, sep=";") -data['sum']= data['minor']+data['main'] -#Average inflow and max. of the whole summed inflow data timeseries -#From 2014 - 2023 -print('Average inflowQ m3/s:',data['sum'].mean()) -print('Average inflowQ m3/s:',data['sum'].max) - -model.flow_boundary.add( - Node(1, Point(0.0, 0.0), name='Main'), - [flow_boundary.Time(time=data.time, flow_rate=data.main, - )] -) - -model.flow_boundary.add( - Node(2, Point(-3.0, 0.0), name='Minor'), - [flow_boundary.Time(time=data.time, flow_rate=data.minor, - )] -) -``` -### Step 4: Basin node (confluence) -To schematize the confluence from the tributary we will use the Basin node. -The node by itself portrays as a bucket with a certain volume of water and can be used for different purposes, such as a reservoir, a lake or in this case a confluence. -@fig-confluence visualizes a cross section of the confluence point in our model. - -![Basin node concept for the confluence](https://s3.deltares.nl/ribasim/doc-image/quickstart/Basin-node-concept-for-the-confluence.png){fig-align="left" #fig-confluence} - -@tbl-input1 shows the input data for the basin node profile. - -: Profile data for the basin node {#tbl-input1} - -| Area [$\text{m}^2$] | Level [$\text{m}$] | -|---------------------|--------------------| -| $672000.0$ | $0.0$ | -| $5600000.0$ | $6.0$ | - -To specify the basin profile, the following code is used: -```python -model.basin.add( - Node(3, Point(-1.5, -1), name='Conf'), - [basin.Profile(area=[672000, 5600000], level=[0, 6]), - basin.State(level=[4]), - basin.Time(time=[starttime, endtime]), - ], -) -``` -### Step 5: Tabulated rating curve -In the previous step we implemented a Basin node that functions as a confluence. -Conceptually, the basin acts like a bucket of water, accumulating inflows and then releasing them. -However, the model does not run if the basin is directly connected to the terminal node. -This is because, for the model to function properly, we need to define a relation between the water level ($h$) in the basin and the outflow ($Q$) from the basin. -This relation is defined by the `Tabulated Rating Curve` and thus serves as a critical component. -This setup mimics the behavior of a gate or spillway, allowing us to model how varying water levels influence flow rates at the confluence. - -As the two inflows come together at the confluence, we expect, as mentioned and coded before, a discharge average of $44.45 \text{ m}^3/\text{s}$. -It is therefore expected that the confluence basin reaches a level where the outflow is equal to the inflow via the rating curve. -Only then is the confluence basin in equilibrium. -To ensure that inflow equals outflow (IN=OUT) and keeping in mind the maximum depth of the river is $6 \text{ m}$, the $Q(h)$ relationship in @tbl-input2 will be used as input. - -: Input data for the Tabulated Rating Curve {#tbl-input2} - -| Water Level ($h$) [$\text{m}$] | Outflow ($Q$) [$\text{m}^3/\text{s}$] | -| -------------------------------|---------------------------------------| -| $0.0$ | $0.0$ | -| $2.0$ | $50.0$ | -| $5.0$ | $200.0$ | - -In Ribasim, the $Q(h)$ relation is a linear function, so the points in between will be linearly interpolated. -@fig-discharge illustrates the visual process and shows a progressive increase in discharge with rising water levels. -In this case this means: - -- At level $0.0$: No discharge occurs. This represents a condition where the water level is too low for any flow to be discharged. -- At level $2.0$: Discharge is max. $50.0 \text{ m}^3/\text{s}$. This is a bit above the average discharge rate, corresponding to the water level where normal flow conditions are established. -- At level $5.0$: Discharge rate reaches $200.0 \text{ m}^3/\text{s}$. This discharge rate occurs at the water level during wet periods, indicating higher flow capacity. - -![Discharge at corresponding water levels](https://s3.deltares.nl/ribasim/doc-image/quickstart/Discharge-at-corresponding-water-levels.png){fig-align="left" #fig-discharge} - -Taking this into account, add the `Tabulated Rating Curve` as follows: - -```python -model.tabulated_rating_curve.add( - Node(4, Point(-1.5, -1.5), name='MainConf'), - [tabulated_rating_curve.Static( - level=[0.0, 2, 5], - flow_rate=[0.0, 50, 200], - ) - ] -) -``` -### Step 6: Terminal node -Finally all the water will discharge into the ocean. -Schematize this with the terminal node as it portrays the end point of the model. -Besides the node number/name and location, no further input is needed. - -```python -model.terminal.add(Node(5, Point(-1.5, -3.0), name="Terminal")) -``` -### Step 7: Defining edges -Implement the connections (edges) between the nodes, in the following order: - -1. Flow boundaries to the basin; -2. Basin to the rating curve; -3. Tabulated rating curve to the terminal. - -```python -model.edge.add(model.flow_boundary[1], model.basin[3]) -model.edge.add(model.flow_boundary[2], model.basin[3]) -model.edge.add(model.basin[3], model.tabulated_rating_curve[4]) -model.edge.add(model.tabulated_rating_curve[4], model.terminal[5]) -``` -### Step 8: Visualization and model execution -Plot the schematization, write the model configuration to the `TOML` file. -Name the output file `Crystal_1.1/ribasim.toml`: - -```python -model.plot() - -toml_path = model_dir/ "Crystal_1.1/ribasim.toml" -model.write(toml_path) -rib_path = base_dir / "ribasim_windows/ribasim.exe" -``` -The schematization should look like @fig-cs11. - -![Schematization of the Crystal basin 1.1](https://s3.deltares.nl/ribasim/doc-image/quickstart/Schematization-of-the-Crystal-basin-1.1.png){fig-align="left" #fig-cs11} - -After writing model.write a subfolder `Crystal_1.1` is created, which contains the model input data and configuration: - -- ribasim.toml: The model configuration -- database.gpkg: A geopackage containing the shapes of your schematization and the input data of the nodes used. - -Now run the model: -```python -subprocess.run([rib_path, toml_path], check=True) -``` - -### Step 9: Post-processing results -Read the arrow files and plot the simulated flows from different edges and the levels and storages at our confluence point: - -```python -df_basin = pd.read_feather(model_dir / "Crystal_1.1/results/basin.arrow") - -# Create pivot tables and plot for basin data -df_basin_wide = df_basin.pivot_table( -index="time", columns="node_id", values=["storage", "level"] -) - -# Skip the first timestep as it’s the initialization step -df_basin_wide = df_basin_wide.iloc[1:] - -# Plot level and storage on the same graph with dual y-axes -fig, ax1 = plt.subplots(figsize=(12, 6)) - -# Plot level on the primary y-axis -color = 'b' -ax1.set_xlabel('Time') -ax1.set_ylabel('Level [m]', color=color) -ax1.plot(df_basin_wide.index, df_basin_wide["level"], color=color) -ax1.tick_params(axis='y', labelcolor=color) - -# Create a secondary y-axis for storage -ax2 = ax1.twinx() -color = 'r' -ax2.set_ylabel('Storage [m³]', color='r') -ax2.plot(df_basin_wide.index, df_basin_wide["storage"],linestyle='--', color=color) -ax2.tick_params(axis='y', labelcolor=color) - -fig.tight_layout() # Adjust layout to fit labels -plt.title('Basin Level and Storage Over Time') -plt.show() - - -# Plot flow data -# Read the data from feather format -df_flow = pd.read_feather(model_dir / "Crystal_1.1/results/flow.arrow") -# Create 'edge' and 'flow_m3d' columns -df_flow["edge"] = list(zip(df_flow.from_node_id, df_flow.to_node_id)) - -# Create a pivot table -pivot_flow = df_flow.pivot_table(index="time", columns="edge", values="flow_rate") - -# Skip the first timestep -pivot_flow = pivot_flow.iloc[1:] - -line_styles = ['-', '--', '-', '-.'] -num_styles = len(line_styles) - -fig, ax = plt.subplots(figsize=(12, 6)) -for i, column in enumerate(pivot_flow.columns): - pivot_flow[column].plot(ax=ax, linestyle=line_styles[i % num_styles],linewidth=1.5, alpha=0.8) - -# Set labels and title -ax.set_xlabel('Time') -ax.set_ylabel('Flow [m³/s]') -ax.legend(bbox_to_anchor=(1.15, 1), title="Edge") -plt.title('Flow Over Time') -plt.grid(True) -plt.show() -``` - -@fig-sim1 shows the storage and levels in the basin node. - -In this configuration the basin node is designed to ensure that inflow equals outflow, effectively simulating a controlled junction where water flow is managed rather than stored. -To accurately represent the relationship between water levels and discharge rates at this confluence, a rating curve node is implemented. -This setup mimics the behavior of a gate or spillway, allowing us to model how varying water levels influence flow rates at the confluence. -Since the basin node is functioning as a confluence rather than a storage reservoir, the simulated water levels and storage trends will closely follow the inflow patterns. -This is because there is no net change in storage; all incoming water is balanced by outgoing flow. - -![Simulated basin level and storage](https://s3.deltares.nl/ribasim/doc-image/quickstart/Simulated-basin-level-and-storage.png){fig-align="left" #fig-sim1} - -@fig-sim2 shows the discharges in $\text{m}^3/\text{s}$ on each edge. -Edge (3,4) represents the flow from the confluence to the tabulated rating curve and edge (4,5) represents the flow from the tabulated rating curve to the terminal. -Both show the same discharge over time. -Which is expected in a natural flow environment, as what is coming into the confluence must come out. - -![Simulated flows on each edge](https://s3.deltares.nl/ribasim/doc-image/quickstart/Simulated-flows-on-each-edge.jpg){fig-align="left" #fig-sim2} - -## Modual 1.2 - Irrigation demand - -Let us modify the environment to include agricultural activities within the basin, which necessitate irrigation. -In a conventional irrigation setup, some water is diverted from the Main River through a canal, with a portion of it eventually returning to the main river (see @fig-irrigation). - -![Crystal basin with irrigation](https://s3.deltares.nl/ribasim/doc-image/quickstart/Crystal-basin-with-irrigation.png){fig-align="left" #fig-irrigation} - -For this update schematization, we need to incorporate three additional nodes: - -- Basin: Represents a cross-sectional point where water is diverted. -- User Demand: Represents the irrigation demand. -- Tabulates Rating Curve: Defines the remaining water flow from the main river at the diversion point. - -### Step 1: Setup the model & adjust Import Packages -Copy and paste the python script `Crystal_1.1` and rename it `Crystal_1.2`. -De import modules remain the same, except a demand needs to be added and if you want to have a more interactive plot then importing `plotly` can be useful. - -```python -import shutilfrom -import pathlib -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from ribasim import Allocation, Model, Node # The main library used for river basin modeling. -from ribasim.nodes import ( - flow_boundary, - basin, - tabulated_rating_curve, - user_demand, - terminal -) -from shapely.geometry import Point -import subprocess # For running the model -import plotly.express as px -``` - -### Step 2: Add a second Basin node -Schematically we are dealing with two basins. -To keep the order of flow from upstream to downstream it is recommended to adjust the node_id numbers accordingly. -In this case `node_id = 3` will be `node_id = 4`. -Basin 3 will portray as the point in the river where the diversion takes place, getting the name `Div`. -Its profile area at this intersection is slightly smaller than at the confluence. - -```python -model.basin.add( - Node(3, Point(-0.75, -0.5), name='Div'), - [basin.Profile(area=[500000, 5000000], level=[0, 6]), - basin.State(level=[3]), - basin.Time(time=[starttime, endtime]), - ], -) - -model.basin.add( - Node(4, Point(-1.5, -1), name='Conf'), - [basin.Profile(area=[672000, 5600000], level=[0, 6]), - basin.State(level=[4]), - basin.Time(time=[starttime, endtime]), - ], -) -``` - -### Step 3: Add the irrigation demand -A big farm company needs to apply irrigation to its field starting from April to September. -The irrigated field is $> 17000 \text{ ha}$ and requires around $5 \text{ mm/day}$. -In this case the farm company diverts from the main river an average flow rate of $10 \text{ m}^3/\text{s}$ and $12 \text{ m}^3/\text{s}$ during spring and summer, respectively. -Start of irrigation takes place on the 1st of April until the end of August. -The farmer taps water through a canal (demand). - -For now, let’s assume the return flow remains $0.0$ (`return_factor`). -Meaning all the supplied water to fulfill the demand is consumed and does not return back to the river. -The user demand node interpolates the demand values. Thus the following code needs to be implemented: - -```python -model.user_demand.add( - Node(6, Point(-1.5, 1.0), name='IrrA'), - [user_demand.Time( - demand=[0.0, 0.0, 10, 12, 12, 0.0], - return_factor=0, - min_level=0, - priority=1, - time=[starttime, "2022-03-31", "2022-04-01", "2022-07-01", "2022-09-30", "2022-10-01"] - ) - ] -) -``` - -### Step 4: Add a tabulated rating curve -The second Tabulated Rating Curve node will simulate the rest of the water that is left after diverting a part from the main river to the farm field. -The rest of the water will flow naturally towards the confluence: - -```python -model.tabulated_rating_curve.add( - Node(7, Point(-1.125, -0.75), name='MainDiv'), - [tabulated_rating_curve.Static( - level=[0.0, 1.5, 5], - flow_rate=[0.0, 45, 200], - ) - ] -) -``` - -It is up to the user to renumber the ID’s of the nodes. -Applying the ID number based on the order of the nodes from up- to downstream keeps it more organized, but not necessary. - -### Step 5: Adjust the terminal node id and edges -Adjust the terminal node id. -Since we added more nodes we have more edges. Add and adjust the edges: - -```python -model.terminal.add(Node(8, Point(-1.5, -3.0), name="Terminal")) - -model.edge.add(model.flow_boundary[1], model.basin[3]) -model.edge.add(model.flow_boundary[2], model.basin[4]) -model.edge.add(model.basin[3], model.user_demand[6]) -model.edge.add(model.user_demand[6], model.basin[4]) -model.edge.add(model.basin[3], model.tabulated_rating_curve[7]) -model.edge.add(model.tabulated_rating_curve[7], model.basin[4]) -model.edge.add(model.basin[4], model.tabulated_rating_curve[5]) -model.edge.add(model.tabulated_rating_curve[5], model.terminal[8]) -``` -### Step 6: Plot model and run -Plot the schematization and run the model. -This time the new outputs should be written in a new folder called `Crystal_1.2`: - -```python -model.plot() - -toml_path = model_dir/ "Crystal_1.2/ribasim.toml" -model.write(toml_path) -rib_path = base_dir / "ribasim_windows/ribasim.exe" - -subprocess.run([rib_path, toml_path], check=True) -``` - -The schematization should look like @fig-cs12. - -![Schematization of the Crystal basin with irrigation](https://s3.deltares.nl/ribasim/doc-image/quickstart/Schematization-of-the-Crystal-basin-with-irrigation.png){fig-align="left" #fig-cs12} - -### Step 7: Name the edges and basins -The names of each nodes are defined and saved in the geopackage. -However, in the dataframe this needs to be added by creating a dictionary and map it within the dataframe. - -```python -# Dictionary mapping node_ids to names -edge_names = { - (1,3): 'Main', - (2,4): 'Minor', - (3,6): 'IrrA Demand', - (6,4): 'IrrA Drain', - (3,7): 'Div2Main', - (7,4): 'Main2Conf', - (4,5): 'Conf2TRC', - (5,8): 'TRC2Term', -} - -# Dictionary mapping basins (node_ids) to names -node_names = { - 3: 'Div', - 4: 'Conf', -} -``` - -### Step 8: Plot and compare the basin results -Plot the simulated levels and storages at the diverted section (basin 3) and at the confluence (basin 4). - -```python - -df_basin_div = df_basin_wide.xs('Div', axis=1, level=1, drop_level=False) -df_basin_conf = df_basin_wide.xs('Conf', axis=1, level=1, drop_level=False) - -def plot_basin_data(ax, ax_twin, df_basin, level_color='b', storage_color='r', title='Basin'): - # Plot level data - for idx, column in enumerate(df_basin["level"].columns): - ax.plot(df_basin.index, df_basin["level"][column], - linestyle='-', color=level_color, - label=f'Level - {column}') - - # Plot storage data - for idx, column in enumerate(df_basin["storage"].columns): - ax_twin.plot(df_basin.index, df_basin["storage"][column], - linestyle='--', color=storage_color, - label=f'Storage - {column}') - - ax.set_ylabel('Level [m]', color=level_color) - ax_twin.set_ylabel('Storage [m³]', color=storage_color) - - ax.tick_params(axis='y', labelcolor=level_color) - ax_twin.tick_params(axis='y', labelcolor=storage_color) - - ax.set_title(title) - - # Combine legends from both axes - lines, labels = ax.get_legend_handles_labels() - lines_twin, labels_twin = ax_twin.get_legend_handles_labels() - ax.legend(lines + lines_twin, labels + labels_twin, loc='upper left') - -# Create subplots -fig, (ax1, ax3) = plt.subplots(2, 1, figsize=(12, 12), sharex=True) - -# Plot Div basin data -ax2 = ax1.twinx() # Secondary y-axis for storage -plot_basin_data(ax1, ax2, df_basin_div, title='Div Basin Level and Storage over Time') - -# Plot Conf basin data -ax4 = ax3.twinx() # Secondary y-axis for storage -plot_basin_data(ax3, ax4, df_basin_conf, title='Conf Basin Level and Storage over Time') - -# Common X label -ax3.set_xlabel('Time') - -fig.tight_layout() # Adjust layout to fit labels -plt.show() -``` - -@fig-sim3 illustrates the water levels and storage capacities for each basin. -At the diverted section, where the profile is narrower than at the confluence, we anticipate lower storage and water levels compared to the confluence section. - -When compared to the natural flow conditions, where no water is abstracted for irrigation (See Crystal 1.1), there is a noticeable decrease in both storage and water levels at the confluence downstream. -This reduction is attributed to the irrigation demand upstream with no return flow, which decreases the amount of available water in the main river, resulting in lower water levels at the confluence. - -![Simulated basin levels and storages](https://s3.deltares.nl/ribasim/doc-image/quickstart/Simulated-basin-levels-and-storages.png){fig-align="left" #fig-sim3} - -### Step 9: Plot and compare the flow results -Plot the flow results in an interactive plotting tool. - -```python -df_flow = pd.read_feather(model_dir / "Crystal_1.2/results/flow.arrow") -df_flow["edge"] = list(zip(df_flow.from_node_id, df_flow.to_node_id)) -df_flow["name"] = df_flow["edge"].map(edge_names) - -# Plot the flow data, interactive plot with Plotly -pivot_flow = df_flow.pivot_table(index="time", columns="name", values="flow_rate").reset_index() -fig = px.line(pivot_flow, x="time", y=pivot_flow.columns[1:], title="Flow Over Time [m3/s]") - -fig.update_layout(legend_title_text='Edge') -fig.write_html(model_dir/ "Crystal_1.2/plot_edges.html") -fig.show() - -``` - -The plot will be saved as an HTML file, which can be viewed by dragging the file into an internet browser (@fig-sim4). - -![Simulated flows of each edge](https://s3.deltares.nl/ribasim/doc-image/quickstart/Simulated-flows-of-each-edge.png){fig-align="left" #fig-sim4} - -When selecting only the flow demanded by the User Demand node, or in other words the supply for irrigation increases at times when it is required (@fig-sim5) and the return flow remains zero, as the assumption defined before was that there is no drain. - -![Supplied irrigation and return flow](https://s3.deltares.nl/ribasim/doc-image/quickstart/Supplied-irrigation-and-return-flow.png){fig-align="left" #fig-sim5} - -@fig-sim6 shows the flow to the ocean (Terminal). -Compared to Crystal 1.1 the flow has decreased during the irrigated period. -Indicating the impact of irrigation without any drain. - -![Simulated flow to Terminal](https://s3.deltares.nl/ribasim/doc-image/quickstart/Simulated-flow-to-Terminal.png){fig-align="left" #fig-sim6} - -# Modual 2 – Reservoirs and Public Water Supply -Due to the increase of population and climate change Crystal city has implemented a reservoir upstream to store water for domestic use (See @fig-reservoir). -The reservoir is to help ensure a reliable supply during dry periods. -In this module, the user will update the model to incorporate the reservoir's impact on the whole Crystal Basin. - -![Crystal basin with demands and a reservoir](https://s3.deltares.nl/ribasim/doc-image/quickstart/Crystal-basin-with-demands-and-a-reservoir.png){fig-align="left" #fig-reservoir} - -## Modual 2.1 – Reservoir -### Step 1: Add a basin -This time the basin 3 will function as a reservoir instead of a diversion, meaning it's storage and levels will play an important role for the users (the city and the farmer). -The reservoir has a max. area of $32.3 \text{ km}^2$ and a max. depth of $7 \text{ m}$. -The profile of basin 3 should change to: - -```python -model.basin.add( - Node(3, Point(-0.75, -0.5), name='Rsv'), - [basin.Profile(area=[20000000, 32300000], level=[0, 7]), - basin.State(level=[3.5]), - basin.Time(time=[starttime, endtime]), - ], -) -``` - -### Step 2: Adjust the code -Adjust the naming of the basin in the dictionary mapping and the saving file should be `Crystal_2.1` instead of `*_1.2`. - -```python -toml_path = model_dir/ "Crystal_2.1/ribasim.toml" -model.write(toml_path) -rib_path = base_dir / "ribasim_windows/ribasim.exe" -``` - -```python -# Dictionary mapping node_ids to names -edge_names = { - (1,3): 'Main', - (2,4): 'Minor', - (3,6): 'IrrA Demand', - (6,4): 'IrrA Drain', - (3,7): 'Rsv2Main', - (7,4): 'Main2Conf', - (4,5): 'Conf2TRC', - (5,8): 'TRC2Term', -} - -# Dictionary mapping basins (node_ids) to names -node_names = { - 3: 'Rsv', - 4: 'Conf', -} - -df_basin = pd.read_feather(model_dir / "Crystal_2.1/results/basin.arrow") -``` - -```python -# Create pivot tables and plot for basin data -df_basin_rsv = df_basin_wide.xs('Rsv', axis=1, level=1, drop_level=False) -df_basin_conf = df_basin_wide.xs('Conf', axis=1, level=1, drop_level=False) -``` - -```python -# Plot Rsv basin data -ax2 = ax1.twinx() # Secondary y-axis for storage -plot_basin_data(ax1, ax2, df_basin_rsv, title='Reservoir Level and Storage Over Time') -``` - -```python -# Sample data loading and preparation -df_flow = pd.read_feather(model_dir / "Crystal_2.1/results/flow.arrow") -df_flow["edge"] = list(zip(df_flow.from_node_id, df_flow.to_node_id)) -df_flow["name"] = df_flow["edge"].map(edge_names) - -# Plot the flow data, interactive plot with Plotly -pivot_flow = df_flow.pivot_table(index="time", columns="name", values="flow_rate").reset_index() -fig = px.line(pivot_flow, x="time", y=pivot_flow.columns[1:], title="Flow Over Time [m3/s]") - -fig.update_layout(legend_title_text='Edge') -fig.write_html(model_dir/ "Crystal_2.1/plot_edges.html") -fig.show() -``` - -### Step 3: Plotting results -@fig-sim7 illustrates the new storage and water level at the reservoir. -As expected, after increasing the profile of basin 3 to mimic the reservoir, its storage capacity increased as well. - -![Simulated basin storages and levels](https://s3.deltares.nl/ribasim/doc-image/quickstart/Simulated-basin-storages-and-levels.png){fig-align="left" #fig-sim7} - -## Module 2.2 – Public Water Supply - -### Step 1: Rename the saving files -Rename the files to `Crystal_2.2` - -### Step 2: Add a demand node -$50.000$ people live in Crystal City. -To represents the total flow rate or abstraction rate required to meet the water demand of $50,000$ people, another demand node needs to be added assuming a return flow of $60%$. - -```python -model.user_demand.add( - Node(9, Point(0.0, -0.25), name='PWS'), - [user_demand.Time( - demand=[0.07, 0.08, 0.09, 0.10, 0.12, 0.14, 0.15, 0.14, 0.12, 0.10, 0.09, 0.08], # Total demand in m³/s - return_factor=0.6, - min_level=0, - priority=1, - time=[ - starttime, - "2022-02-01", - "2022-03-01", - "2022-04-01", - "2022-05-01", - "2022-06-01", - "2022-07-01", - "2022-08-01", - "2022-09-01", - "2022-10-01", - "2022-11-01", - "2022-12-01" - ] - )] -) -``` - -### Step 3: Add the edges -The connection between the reservoir and the demand node needs to be defined: - -```python -model.edge.add(model.flow_boundary[1], model.basin[3]) -model.edge.add(model.flow_boundary[2], model.basin[4]) -model.edge.add(model.basin[3], model.user_demand[6]) -model.edge.add(model.basin[3], model.user_demand[9]) -model.edge.add(model.user_demand[6], model.basin[4]) -model.edge.add(model.user_demand[9], model.basin[4]) -model.edge.add(model.basin[3], model.tabulated_rating_curve[7]) -model.edge.add(model.tabulated_rating_curve[7], model.basin[4]) -model.edge.add(model.basin[4], model.tabulated_rating_curve[5]) -model.edge.add(model.tabulated_rating_curve[5], model.terminal[8]) -``` - -### Step 4: Adjust the name dictionaries - -```python -# Dictionary mapping node_ids to names -edge_names = { - (1,3): 'Main', - (2,4): 'Minor', - (3,6): 'IrrA Demand', - (6,4): 'IrrA Drain', - (3,9): 'PWS Demand', - (9,4): 'PWS Return', - (3,7): 'Rsv2Main', - (7,4): 'Main2Conf', - (4,5): 'Conf2TRC', - (5,8): 'TRC2Term', -} -``` -### Step 5: Check the simulated demands -@fig-sim8 shows the flow to (PWS Demand) and out (PWS Return) of the PWS node. -@fig-sim9 shows the downstream flow to the ocean. -The impact is clear. -Due to the demands upstream (irrigation and public water supply) an expected decrease of discharge is shown downstream. - -![Simulated flows to and from the city](https://s3.deltares.nl/ribasim/doc-image/quickstart/Simulated-flows-to-and-from-the-city.png){fig-align="left" #fig-sim8} - -![Simulated basin storages and levels](https://s3.deltares.nl/ribasim/doc-image/quickstart/Simulated-basin-storages-and-levels-reservoir.png){fig-align="left" #fig-sim9} diff --git a/docs/install.qmd b/docs/install.qmd index 650776765..e5ecaba9a 100644 --- a/docs/install.qmd +++ b/docs/install.qmd @@ -101,7 +101,8 @@ One can also use Ribasim Python to build entire models from base data, such that setup is fully reproducible. The Ribasim Python package is [registered in PyPI](https://pypi.org/project/ribasim/) and [conda-forge](https://prefix.dev/channels/conda-forge/packages/ribasim) and can therefore be installed with [pip](https://docs.python.org/3/installing/index.html), [conda](https://docs.conda.io/) or [pixi](https://pixi.sh/): -``` + +```sh pip install ribasim ``` diff --git a/docs/tutorial/irrigation-demand.ipynb b/docs/tutorial/irrigation-demand.ipynb new file mode 100644 index 000000000..768608cf2 --- /dev/null +++ b/docs/tutorial/irrigation-demand.ipynb @@ -0,0 +1,440 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: \"Irrigation demand\"\n", + "---" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import plotly.express as px\n", + "from ribasim import Model, Node\n", + "from ribasim.nodes import (\n", + " basin,\n", + " flow_boundary,\n", + " tabulated_rating_curve,\n", + " user_demand,\n", + ")\n", + "from shapely.geometry import Point" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "base_dir = Path(\"crystal-basin\")\n", + "\n", + "starttime = \"2022-01-01\"\n", + "endtime = \"2023-01-01\"\n", + "model = Model(\n", + " starttime=starttime,\n", + " endtime=endtime,\n", + " crs=\"EPSG:4326\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These nodes are identical to the previous tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# FlowBoundary\n", + "data = pd.DataFrame({\n", + " \"time\": pd.date_range(start=\"2022-01-01\", end=\"2023-01-01\", freq=\"MS\"),\n", + " \"main\": [74.7, 57.9, 63.2, 183.9, 91.8, 47.5, 32.6, 27.6, 26.5, 25.1, 39.3, 37.8, 57.9],\n", + " \"minor\": [16.3, 3.8, 3.0, 37.6, 18.2, 11.1, 12.9, 12.2, 11.2, 10.8, 15.1, 14.3, 11.8]\n", + "}) # fmt: skip\n", + "data[\"total\"] = data[\"minor\"] + data[\"main\"]\n", + "main = model.flow_boundary.add(\n", + " Node(1, Point(0.0, 0.0), name=\"main\"),\n", + " [\n", + " flow_boundary.Time(\n", + " time=data.time,\n", + " flow_rate=data.main,\n", + " )\n", + " ],\n", + ")\n", + "minor = model.flow_boundary.add(\n", + " Node(2, Point(-3.0, 0.0), name=\"minor\"),\n", + " [\n", + " flow_boundary.Time(\n", + " time=data.time,\n", + " flow_rate=data.minor,\n", + " )\n", + " ],\n", + ")\n", + "\n", + "# Basin\n", + "confluence = model.basin.add(\n", + " Node(3, Point(-1.5, -1), name=\"confluence\"),\n", + " [\n", + " basin.Profile(area=[672000, 5600000], level=[0, 6]),\n", + " basin.State(level=[4]),\n", + " basin.Time(time=[starttime, endtime]),\n", + " ],\n", + ")\n", + "\n", + "# TabulatedRatingCurve\n", + "weir = model.tabulated_rating_curve.add(\n", + " Node(4, Point(-1.5, -1.5), name=\"weir\"),\n", + " [\n", + " tabulated_rating_curve.Static(\n", + " level=[0.0, 2, 5],\n", + " flow_rate=[0.0, 50, 200],\n", + " )\n", + " ],\n", + ")\n", + "\n", + "# Terminal\n", + "sea = model.terminal.add(Node(5, Point(-1.5, -3.0), name=\"sea\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Irrigation demand\n", + "\n", + "Let us modify the environment to include agricultural activities within the basin, which necessitate irrigation.\n", + "Water is diverted from the main river through an irrigation canal, with a portion of it eventually returning to the main river (see @fig-irrigation).\n", + "\n", + "![Crystal basin with irrigation](https://s3.deltares.nl/ribasim/doc-image/quickstart/Crystal-basin-with-irrigation.png){fig-align=\"left\" #fig-irrigation}\n", + "\n", + "For this schematization update, we need to incorporate three additional nodes:\n", + "\n", + "- Basin: Represents a cross-sectional point where water is diverted.\n", + "- UserDemand: Represents the irrigation demand.\n", + "- TabulatedRatingCurve: Defines the remaining water flow from the main river at the diversion point." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Add a second Basin node\n", + "This Basin will portray as the point in the river where the diversion takes place, getting the name `diversion`.\n", + "Its profile area at this intersection is slightly smaller than at the confluence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "diversion_basin = model.basin.add(\n", + " Node(6, Point(-0.75, -0.5), name=\"diversion_basin\"),\n", + " [\n", + " basin.Profile(area=[500000, 5000000], level=[0, 6]),\n", + " basin.State(level=[3]),\n", + " basin.Time(time=[starttime, endtime]),\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Add the irrigation demand\n", + "An irrigation district needs to apply irrigation to its field starting from April to September.\n", + "The irrigated area is $> 17000 \\text{ ha}$ and requires around $5 \\text{ mm/day}$.\n", + "In this case the irrigation district diverts from the main river an average flow rate of $10 \\text{ m}^3/\\text{s}$ and $12 \\text{ m}^3/\\text{s}$ during spring and summer, respectively.\n", + "Start of irrigation takes place on the 1st of April until the end of August.\n", + "The water intake is through a canal (demand).\n", + "\n", + "For now, let's assume the return flow remains $0.0$ (`return_factor`).\n", + "Meaning all the supplied water to fulfill the demand is consumed and does not return back to the river.\n", + "The user demand node interpolates the demand values. Thus the following code needs to be implemented:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "irrigation = model.user_demand.add(\n", + " Node(7, Point(-1.5, 0.5), name=\"irrigation\"),\n", + " [\n", + " user_demand.Time(\n", + " demand=[0.0, 0.0, 10, 12, 12, 0.0],\n", + " return_factor=0,\n", + " min_level=0,\n", + " priority=1,\n", + " time=[\n", + " starttime,\n", + " \"2022-03-31\",\n", + " \"2022-04-01\",\n", + " \"2022-07-01\",\n", + " \"2022-09-30\",\n", + " \"2022-10-01\",\n", + " ],\n", + " )\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Add a TabulatedRatingCurve\n", + "The second TabulatedRatingCurve node will simulate the rest of the water that is left after diverting a part from the main river to the irrigation disctrict.\n", + "The rest of the water will flow naturally towards the confluence:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "diversion_weir = model.tabulated_rating_curve.add(\n", + " Node(8, Point(-1.125, -0.75), name=\"diversion_weir\"),\n", + " [\n", + " tabulated_rating_curve.Static(\n", + " level=[0.0, 1.5, 5],\n", + " flow_rate=[0.0, 45, 200],\n", + " )\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Add edges" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.edge.add(main, diversion_basin, name=\"main\")\n", + "model.edge.add(minor, confluence, name=\"minor\")\n", + "model.edge.add(diversion_basin, irrigation, name=\"irrigation\")\n", + "model.edge.add(irrigation, confluence)\n", + "model.edge.add(diversion_basin, diversion_weir, name=\"not diverted\")\n", + "model.edge.add(diversion_weir, confluence)\n", + "model.edge.add(confluence, weir)\n", + "model.edge.add(weir, sea, name=\"sea\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "toml_path = base_dir / \"Crystal-2/ribasim.toml\"\n", + "model.write(toml_path)\n", + "cli_path = \"ribasim\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot model and run\n", + "Plot the schematization and run the model.\n", + "This time the new outputs should be written in a new folder called `Crystal-2`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.plot();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# | include: false\n", + "from subprocess import run\n", + "\n", + "run(\n", + " [\n", + " \"julia\",\n", + " \"--project=../../core\",\n", + " \"--eval\",\n", + " f'using Ribasim; Ribasim.main(\"{toml_path.as_posix()}\")',\n", + " ],\n", + " check=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot and compare the Basin results\n", + "Plot the simulated levels and storages at the diverted section and at the confluence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_basin = pd.read_feather(base_dir / \"Crystal-2/results/basin.arrow\")\n", + "\n", + "# Create pivot tables and plot for basin data\n", + "df_basin_wide = df_basin.pivot_table(\n", + " index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n", + ")\n", + "\n", + "df_basin_div = df_basin_wide.loc[:, pd.IndexSlice[:, diversion_basin.node_id]]\n", + "df_basin_conf = df_basin_wide.loc[:, pd.IndexSlice[:, confluence.node_id]]\n", + "\n", + "\n", + "def plot_basin_data(\n", + " ax, ax_twin, df_basin, level_color=\"b\", storage_color=\"r\", title=\"Basin\"\n", + "):\n", + " # Plot level data\n", + " for column in df_basin[\"level\"].columns:\n", + " ax.plot(\n", + " df_basin.index,\n", + " df_basin[\"level\"][column],\n", + " linestyle=\"-\",\n", + " color=level_color,\n", + " label=f\"Level - {column}\",\n", + " )\n", + "\n", + " # Plot storage data\n", + " for column in df_basin[\"storage\"].columns:\n", + " ax_twin.plot(\n", + " df_basin.index,\n", + " df_basin[\"storage\"][column],\n", + " linestyle=\"--\",\n", + " color=storage_color,\n", + " label=f\"Storage - {column}\",\n", + " )\n", + "\n", + " ax.set_ylabel(\"Level [m]\", color=level_color)\n", + " ax_twin.set_ylabel(\"Storage [m³]\", color=storage_color)\n", + "\n", + " ax.tick_params(axis=\"y\", labelcolor=level_color)\n", + " ax_twin.tick_params(axis=\"y\", labelcolor=storage_color)\n", + "\n", + " ax.set_title(title)\n", + "\n", + " # Combine legends from both axes\n", + " lines, labels = ax.get_legend_handles_labels()\n", + " lines_twin, labels_twin = ax_twin.get_legend_handles_labels()\n", + " ax.legend(lines + lines_twin, labels + labels_twin, loc=\"upper left\")\n", + "\n", + "\n", + "# Create subplots\n", + "fig, (ax1, ax3) = plt.subplots(2, 1, figsize=(12, 12), sharex=True)\n", + "\n", + "# Plot Div basin data\n", + "ax2 = ax1.twinx() # Secondary y-axis for storage\n", + "plot_basin_data(ax1, ax2, df_basin_div, title=\"Diversion Basin level and storage\")\n", + "\n", + "# Plot Conf basin data\n", + "ax4 = ax3.twinx() # Secondary y-axis for storage\n", + "plot_basin_data(ax3, ax4, df_basin_conf, title=\"Confluence Basin level and storage\")\n", + "\n", + "# Common X label\n", + "ax3.set_xlabel(\"Time\")\n", + "fig.tight_layout() # Adjust layout to fit labels\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The figure above illustrates the water levels and storage capacities for each Basin.\n", + "\n", + "When compared to the natural flow conditions, where no water is abstracted for irrigation (See Crystal 1), there is a noticeable decrease in both storage and water levels at the confluence downstream.\n", + "This reduction is attributed to the irrigation demand upstream with no return flow, which decreases the amount of available water in the main river, resulting in lower water levels at the confluence.\n", + "\n", + "### Plot and compare the flow results\n", + "Plot the flow results in an interactive plotting tool." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_flow = pd.read_feather(base_dir / \"Crystal-2/results/flow.arrow\")\n", + "# Add the edge names and then remove unnamed edges\n", + "df_flow[\"name\"] = model.edge.df[\"name\"].loc[df_flow[\"edge_id\"]].to_numpy()\n", + "df_flow = df_flow[df_flow[\"name\"].astype(bool)]\n", + "\n", + "# Plot the flow data, interactive plot with Plotly\n", + "pivot_flow = df_flow.pivot_table(\n", + " index=\"time\", columns=\"name\", values=\"flow_rate\"\n", + ").reset_index()\n", + "fig = px.line(pivot_flow, x=\"time\", y=pivot_flow.columns[1:], title=\"Flow [m3/s]\")\n", + "\n", + "fig.update_layout(legend_title_text=\"Edge\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Try toggling the edges on and off by clicking on them in the edges." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorial/natural-flow.ipynb b/docs/tutorial/natural-flow.ipynb new file mode 100644 index 000000000..a4e817a0a --- /dev/null +++ b/docs/tutorial/natural-flow.ipynb @@ -0,0 +1,515 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: \"Getting started\"\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction\n", + "Welcome to Ribasim!\n", + "This tutorial will help you get started with the basics of using Ribasim for river basin simulation.\n", + "In this tutorial, the schematization of models is done in Python using the Ribasim Python package.\n", + "The Ribasim Python package (named `ribasim`) simplifies the process of building, updating, and analyzing Ribasim model programmatically.\n", + "It also allows for the creation of entire models from base data, ensuring that your model setup is fully reproducible.\n", + "\n", + "## Learning objectives\n", + "In this tutorial, we will focus on a fictional river basin called Crystal, which will serve as our case study.\n", + "The guide is divided into different modules, each covering various scenarios.\n", + "These include simulating natural flow, implementing reservoirs, and observing the impact of other structures.\n", + "While not all node types and possibilities will be demonstrated, the focus will be on the most commonly used and significant situations.\n", + "By the end of the tutorial, users will be able to:\n", + "\n", + "- **Set up a basic Ribasim model**: Understand how to create a new model for a river basin using the Ribasim Python package.\n", + "- **Evaluate the impact of demands**: Introduce water demand (such as irrigation) and assess their effects on the river basin.\n", + "- **Modify and update models**: Learn how to update existing models with new data and changes.\n", + "- **Analyze simulation results**: Use built-in tools to analyze and interpret the results of your simulations.\n", + "\n", + "## Prerequisites\n", + "First install the latest release of Ribasim as documented in [the installation guide](/install.qmd).\n", + "\n", + "Download the `Crystal_Basin.zip` file from the website. Extract `Crystal_Basin.zip` and place it in the same directory as your Ribasim installation. This folder includes:\n", + "\n", + "- `QuickStartGuide.pdf`\n", + "- `data`: Contains data inputs such as time series needed for running the case.\n", + "Additionally, your Python model (`.py`) and the results will also be saved in this folder." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Crystal River Basin\n", + "We will examine a straightforward example of the Crystal river basin, which includes a main river and a single tributary flowing into the sea (see @fig-crystal-basin).\n", + "An average discharge of $44.45 \\text{ m}^3/\\text{s}$ is measured at the confluence.\n", + "In this module, the basin is free of any activities, allowing the model to simulate the natural flow.\n", + "The next step is to include a demand (irrigation) that taps from a canal out of the main river.\n", + "\n", + "![Crystal Basin based on natural flow](https://s3.deltares.nl/ribasim/doc-image/quickstart/Crystal-Basin-based-on-natural-flow.png){fig-align=\"left\" #fig-crystal-basin}\n", + "\n", + "After this module the user will be able to:\n", + "\n", + "- Build a river basin model from scratch\n", + "- Understand the functionality of the Demand and Basin nodes\n", + "- Generate overview of results\n", + "- Evaluate the simulation results\n", + "\n", + "## Natural flow\n", + "\n", + "### Import packages\n", + "Before building the model we need to import some modules.\n", + "Open your favorite Python editor (Visual Studio Code, Jupyter, ...) and create a new script or notebook and name it `Crystal_1.1` and save it into your model folder `Crystal_Basin`.\n", + "Import the following modules in Python:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from ribasim import Model, Node\n", + "from ribasim.nodes import basin, flow_boundary, tabulated_rating_curve\n", + "from shapely.geometry import Point" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Setup paths and model configuration\n", + "Reference the paths of the Ribasim installation and model directory and define the time period (2022-01-01 until 2023-01-01) for the model simulation.\n", + "The coordinate reference system (CRS) is also required, and set to [EPSG:4326](https://epsg.io/4326), which means all coordinates are interpreted as latitude and longitude values.\n", + "The CRS is important for correctly placing Ribasim models on the map, but since this is a fictional model, it is not important." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "base_dir = Path(\"crystal-basin\")\n", + "\n", + "starttime = \"2022-01-01\"\n", + "endtime = \"2023-01-01\"\n", + "model = Model(\n", + " starttime=starttime,\n", + " endtime=endtime,\n", + " crs=\"EPSG:4326\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### FlowBoundary nodes\n", + "The Crystal basin consists of two inflow points, the tributary and the main Crystal river, we will call them Minor and Main respectively.\n", + "This is a monthly inflow timeseries from 2014 to 2023.\n", + "The used simulation period is defined by the `starttime` and `endtime` of the model, not by the input timeseries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.DataFrame({\n", + " \"time\": pd.date_range(start=\"2022-01-01\", end=\"2023-01-01\", freq=\"MS\"),\n", + " \"main\": [74.7, 57.9, 63.2, 183.9, 91.8, 47.5, 32.6, 27.6, 26.5, 25.1, 39.3, 37.8, 57.9],\n", + " \"minor\": [16.3, 3.8, 3.0, 37.6, 18.2, 11.1, 12.9, 12.2, 11.2, 10.8, 15.1, 14.3, 11.8]\n", + "}) # fmt: skip\n", + "data[\"total\"] = data[\"minor\"] + data[\"main\"]\n", + "display(data)\n", + "\n", + "# Average and max inflow of the total inflow data timeseries\n", + "# From 2014 - 2023\n", + "print(\"Average inflow [m3/s]:\", data[\"total\"].mean())\n", + "print(\"Maximum inflow [m3/s]:\", data[\"total\"].max())\n", + "\n", + "main = model.flow_boundary.add(\n", + " Node(1, Point(0.0, 0.0), name=\"main\"),\n", + " [\n", + " flow_boundary.Time(\n", + " time=data.time,\n", + " flow_rate=data.main,\n", + " )\n", + " ],\n", + ")\n", + "\n", + "minor = model.flow_boundary.add(\n", + " Node(2, Point(-3.0, 0.0), name=\"minor\"),\n", + " [\n", + " flow_boundary.Time(\n", + " time=data.time,\n", + " flow_rate=data.minor,\n", + " )\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Basin node (confluence)\n", + "To schematize the confluence from the tributary we will use the Basin node.\n", + "The node by itself portrays as water storage with a certain volume of water and can be used for different purposes, such as a reservoir, river reach, lake or in this case a confluence.\n", + "@fig-confluence visualizes a cross section of the confluence point in our model.\n", + "\n", + "![Basin node concept for the confluence](https://s3.deltares.nl/ribasim/doc-image/quickstart/Basin-node-concept-for-the-confluence.png){fig-align=\"left\" #fig-confluence}\n", + "\n", + "@tbl-input1 shows the input data for the Basin node profile.\n", + "\n", + ": Profile data for the basin node {#tbl-input1}\n", + "\n", + "| Area [$\\text{m}^2$] | Level [$\\text{m}$] |\n", + "|---------------------|--------------------|\n", + "| $672000.0$ | $0.0$ |\n", + "| $5600000.0$ | $6.0$ |\n", + "\n", + "Whilst in this case the level starts at $0.0$ and therefore happens to be the same as the depth, it should never be interpreted as a depth.\n", + "All water levels in Ribasim are assumed to be with respect to a shared reference datum, like mean sea level (MSL).\n", + "The first water level in the profile is the height of the Basin bottom above this reference datum.\n", + "\n", + "To specify the Basin profile, the following code is used:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "confluence = model.basin.add(\n", + " Node(3, Point(-1.5, -1), name=\"confluence\"),\n", + " [\n", + " basin.Profile(area=[672000, 5600000], level=[0, 6]),\n", + " basin.State(level=[4]),\n", + " basin.Time(time=[starttime, endtime]),\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TabulatedRatingCurve\n", + "In the previous step we implemented a Basin node that functions as a confluence.\n", + "Conceptually, the Basin acts as a store of water, accumulating inflows and then releasing them.\n", + "A Basin cannot directly connect to another Basin, because the rules for water exchange between them need to be defined.\n", + "Connector nodes take care of this.\n", + "The first such node we introduce is the TabulatedRatingCurve.\n", + "It defines a relation between the water level ($h$) in the Basin and the outflow ($Q$) from the Basin.\n", + "This setup mimics the behavior of a gate or spillway, allowing us to model how varying water levels influence flow rates at the confluence.\n", + "\n", + "As the two inflows come together at the confluence, we expect, as mentioned above, a discharge average of $44.45 \\text{ m}^3/\\text{s}$.\n", + "It is therefore expected that the confluence Basin goes towards a level where the outflow is equal to the inflow via the rating curve.\n", + "Only then is the confluence Basin in equilibrium.\n", + "The maximum depth of the river is $6 \\text{ m}$, and the maximum inflow is $221.5 \\text{ m}^3/\\text{s}$\n", + "The $Q(h)$ relationship in @tbl-input2 allows such inflows with reasonable water levels.\n", + "\n", + ": Input data for the Tabulated Rating Curve {#tbl-input2}\n", + "\n", + "| Water Level ($h$) [$\\text{m}$] | Outflow ($Q$) [$\\text{m}^3/\\text{s}$] |\n", + "| -------------------------------|---------------------------------------|\n", + "| $0.0$ | $0.0$ |\n", + "| $2.0$ | $50.0$ |\n", + "| $5.0$ | $200.0$ |\n", + "\n", + "In Ribasim, the $Q(h)$ relation is a piecewise linear function, so the points in between will be linearly interpolated.\n", + "@fig-discharge illustrates the visual process and shows a progressive increase in discharge with rising water levels.\n", + "In this case this means:\n", + "\n", + "- At level $0.0$: No discharge occurs. This represents a condition where the water level is too low for any flow to be discharged.\n", + "- At level $2.0$: Discharge is $50.0 \\text{ m}^3/\\text{s}$. This is a bit above the average discharge rate, corresponding to the water level where normal flow conditions are established.\n", + "- At level $5.0$: Discharge rate reaches $200.0 \\text{ m}^3/\\text{s}$. This discharge rate occurs at the water level during wet periods, indicating higher flow capacity.\n", + "\n", + "![Discharge at corresponding water levels](https://s3.deltares.nl/ribasim/doc-image/quickstart/Discharge-at-corresponding-water-levels.png){fig-align=\"left\" #fig-discharge}\n", + "\n", + "Taking this into account, add the `TabulatedRatingCurve` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "weir = model.tabulated_rating_curve.add(\n", + " Node(4, Point(-1.5, -1.5), name=\"weir\"),\n", + " [\n", + " tabulated_rating_curve.Static(\n", + " level=[0.0, 2, 5],\n", + " flow_rate=[0.0, 50, 200],\n", + " )\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Terminal node\n", + "Finally all the water will discharge into the sea.\n", + "We schematize this with the Terminal node, as it portrays the end point of the model, that can receive but not give water.\n", + "Besides the node number/name and location, no further input is needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sea = model.terminal.add(Node(5, Point(-1.5, -3.0), name=\"sea\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Defining edges\n", + "Implement the connections (edges) between the nodes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.edge.add(main, confluence, name=\"main\")\n", + "model.edge.add(minor, confluence, name=\"minor\")\n", + "model.edge.add(confluence, weir)\n", + "model.edge.add(weir, sea, name=\"sea\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualization and model execution\n", + "Plot the schematization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.plot();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Write the model configuration to the `TOML` file.\n", + "Name the output file `Crystal-1/ribasim.toml`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "toml_path = base_dir / \"Crystal-1/ribasim.toml\"\n", + "model.write(toml_path)\n", + "cli_path = \"ribasim\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After running `model.write` a subfolder `Crystal-1` is created, which contains the model input data and configuration:\n", + "\n", + "- ribasim.toml: The model configuration\n", + "- database.gpkg: A GeoPackage containing the network geometry and input data of the nodes used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# | include: false\n", + "from subprocess import run\n", + "\n", + "run(\n", + " [\n", + " \"julia\",\n", + " \"--project=../../core\",\n", + " \"--eval\",\n", + " f'using Ribasim; Ribasim.main(\"{toml_path.as_posix()}\")',\n", + " ],\n", + " check=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now run the model. You can open a terminal and run it from there. For example:\n", + "\n", + "```bash\n", + "ribasim Crystal-1/ribasim.toml\n", + "```\n", + "\n", + "From Python you can run it with:\n", + "\n", + "```python\n", + "import subprocess\n", + "result = subprocess.run([cli_path, toml_path], capture_output=True, encoding=\"utf-8\")\n", + "print(result.stderr)\n", + "result.check_returncode()\n", + "```\n", + "\n", + "Where `cli_path` is a string with either the full path to the Ribasim executable, like `r\"c:\\bin\\ribasim\\ribasim\"`, or just `\"ribasim\"` in case you added the `ribasim` folder to your PATH.\n", + "\n", + "The `print(result.stderr)` ensures you see the same logging and error messages that you would see in the terminal. And `result.check_returncode()` will throw an error when the simulation was not successful." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Post-processing results\n", + "Read the Arrow files and plot the simulated flows from different edges and the levels and storages at our confluence point:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_basin = pd.read_feather(base_dir / \"Crystal-1/results/basin.arrow\")\n", + "\n", + "# Create pivot tables and plot for Basin data\n", + "df_basin_wide = df_basin.pivot_table(\n", + " index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n", + ")\n", + "\n", + "# Plot level and storage on the same graph with dual y-axes\n", + "fig, ax1 = plt.subplots(figsize=(12, 6))\n", + "\n", + "# Plot level on the primary y-axis\n", + "color = \"b\"\n", + "ax1.set_xlabel(\"Time\")\n", + "ax1.set_ylabel(\"Level [m]\", color=color)\n", + "ax1.plot(df_basin_wide.index, df_basin_wide[\"level\"], color=color)\n", + "ax1.tick_params(axis=\"y\", labelcolor=color)\n", + "\n", + "# Create a secondary y-axis for storage\n", + "ax2 = ax1.twinx()\n", + "color = \"r\"\n", + "ax2.set_ylabel(\"Storage [m³]\", color=\"r\")\n", + "ax2.plot(df_basin_wide.index, df_basin_wide[\"storage\"], linestyle=\"--\", color=color)\n", + "ax2.tick_params(axis=\"y\", labelcolor=color)\n", + "\n", + "fig.tight_layout() # Adjust layout to fit labels\n", + "plt.title(\"Basin level and storage\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The figure above shows the storage and levels in the Basin node.\n", + "\n", + "To accurately represent the relationship between water levels and discharge rates at this confluence, a TabulatedRatingCurve is used.\n", + "This setup mimics the behavior of a gate or spillway, allowing us to model how varying water levels influence flow rates at the confluence.\n", + "Since the basin node is functioning as a confluence rather than a storage reservoir, the simulated water levels and storage trends will closely follow the inflow patterns.\n", + "This is because there is no net change in storage; all incoming water is balanced by outgoing flow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot flow data\n", + "# Read the flow results\n", + "df_flow = pd.read_feather(base_dir / \"Crystal-1/results/flow.arrow\")\n", + "# Add the edge names and then remove unnamed edges\n", + "df_flow[\"name\"] = model.edge.df[\"name\"].loc[df_flow[\"edge_id\"]].to_numpy()\n", + "df_flow = df_flow[df_flow[\"name\"].astype(bool)]\n", + "\n", + "# Create a pivot table\n", + "pivot_flow = df_flow.pivot_table(index=\"time\", columns=\"name\", values=\"flow_rate\")\n", + "\n", + "line_styles = [\"-\", \"--\", \"-\", \"-.\"]\n", + "num_styles = len(line_styles)\n", + "\n", + "fig, ax = plt.subplots(figsize=(12, 6))\n", + "for i, column in enumerate(pivot_flow.columns):\n", + " pivot_flow[column].plot(\n", + " ax=ax, linestyle=line_styles[i % num_styles], linewidth=1.5, alpha=0.8\n", + " )\n", + "\n", + "# Set labels and title\n", + "ax.set_xlabel(\"Time\")\n", + "ax.set_ylabel(\"Flow [m³/s]\")\n", + "ax.legend(bbox_to_anchor=(1.15, 1), title=\"Edge\")\n", + "plt.title(\"Flow\")\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The figure above shows the discharges in $\\text{m}^3/\\text{s}$ on each edge.\n", + "\n", + "Edge (3,4) represents the flow from the confluence to the TabulatedRatingCurve and edge (4,5) represents the flow from the TabulatedRatingCurve to the Terminal.\n", + "Both show the same discharge over time.\n", + "Which is expected in a natural flow environment, as what is coming into the confluence must come out." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorial/reservoir.ipynb b/docs/tutorial/reservoir.ipynb new file mode 100644 index 000000000..bcd262745 --- /dev/null +++ b/docs/tutorial/reservoir.ipynb @@ -0,0 +1,369 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: \"Reservoir\"\n", + "---" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import plotly.express as px\n", + "from ribasim import Model, Node\n", + "from ribasim.nodes import (\n", + " basin,\n", + " flow_boundary,\n", + " tabulated_rating_curve,\n", + " user_demand,\n", + ")\n", + "from shapely.geometry import Point" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "base_dir = Path(\"crystal-basin\")\n", + "\n", + "starttime = \"2022-01-01\"\n", + "endtime = \"2023-01-01\"\n", + "model = Model(\n", + " starttime=starttime,\n", + " endtime=endtime,\n", + " crs=\"EPSG:4326\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These nodes are identical to the previous tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# FlowBoundary\n", + "data = pd.DataFrame({\n", + " \"time\": pd.date_range(start=\"2022-01-01\", end=\"2023-01-01\", freq=\"MS\"),\n", + " \"main\": [74.7, 57.9, 63.2, 183.9, 91.8, 47.5, 32.6, 27.6, 26.5, 25.1, 39.3, 37.8, 57.9],\n", + " \"minor\": [16.3, 3.8, 3.0, 37.6, 18.2, 11.1, 12.9, 12.2, 11.2, 10.8, 15.1, 14.3, 11.8]\n", + "}) # fmt: skip\n", + "data[\"total\"] = data[\"minor\"] + data[\"main\"]\n", + "main = model.flow_boundary.add(\n", + " Node(1, Point(0.0, 0.0), name=\"main\"),\n", + " [\n", + " flow_boundary.Time(\n", + " time=data.time,\n", + " flow_rate=data.main,\n", + " )\n", + " ],\n", + ")\n", + "minor = model.flow_boundary.add(\n", + " Node(2, Point(-3.0, 0.0), name=\"minor\"),\n", + " [\n", + " flow_boundary.Time(\n", + " time=data.time,\n", + " flow_rate=data.minor,\n", + " )\n", + " ],\n", + ")\n", + "\n", + "# Basin\n", + "confluence = model.basin.add(\n", + " Node(3, Point(-1.5, -1), name=\"confluence\"),\n", + " [\n", + " basin.Profile(area=[672000, 5600000], level=[0, 6]),\n", + " basin.State(level=[4]),\n", + " basin.Time(time=[starttime, endtime]),\n", + " ],\n", + ")\n", + "\n", + "# TabulatedRatingCurve\n", + "weir = model.tabulated_rating_curve.add(\n", + " Node(4, Point(-1.5, -1.5), name=\"weir\"),\n", + " [\n", + " tabulated_rating_curve.Static(\n", + " level=[0.0, 2, 5],\n", + " flow_rate=[0.0, 50, 200],\n", + " )\n", + " ],\n", + ")\n", + "diversion_weir = model.tabulated_rating_curve.add(\n", + " Node(8, Point(-1.125, -0.75), name=\"diversion_weir\"),\n", + " [\n", + " tabulated_rating_curve.Static(\n", + " level=[0.0, 1.5, 5],\n", + " flow_rate=[0.0, 45, 200],\n", + " )\n", + " ],\n", + ")\n", + "\n", + "# UserDemand\n", + "irrigation = model.user_demand.add(\n", + " Node(7, Point(-1.5, 0.5), name=\"irrigation\"),\n", + " [\n", + " user_demand.Time(\n", + " demand=[0.0, 0.0, 10, 12, 12, 0.0],\n", + " return_factor=0,\n", + " min_level=0,\n", + " priority=1,\n", + " time=[\n", + " starttime,\n", + " \"2022-03-31\",\n", + " \"2022-04-01\",\n", + " \"2022-07-01\",\n", + " \"2022-09-30\",\n", + " \"2022-10-01\",\n", + " ],\n", + " )\n", + " ],\n", + ")\n", + "\n", + "# Terminal\n", + "sea = model.terminal.add(Node(5, Point(-1.5, -3.0), name=\"sea\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Due to the increase of population and climate change Crystal city has implemented a reservoir upstream to store water for domestic use (See @fig-reservoir).\n", + "The reservoir is to help ensure a reliable supply during dry periods.\n", + "In this module, the user will update the model to incorporate the reservoir's impact on the whole Crystal basin.\n", + "\n", + "![Crystal basin with demands and a reservoir](https://s3.deltares.nl/ribasim/doc-image/quickstart/Crystal-basin-with-demands-and-a-reservoir.png){fig-align=\"left\" #fig-reservoir}\n", + "\n", + "## Reservoir\n", + "### Add a Basin\n", + "The `diversion_basin` from the previous tutorial is not used, but replaced by a larger `reservoir` Basin.\n", + "Its water will play an important role for the users (the city and the irrigation district).\n", + "The reservoir has a maximum area of $32.3 \\text{ km}^2$ and a maximum depth of $7 \\text{ m}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "reservoir = model.basin.add(\n", + " Node(6, Point(-0.75, -0.5), name=\"reservoir\"),\n", + " [\n", + " basin.Profile(area=[20000000, 32300000], level=[0, 7]),\n", + " basin.State(level=[3.5]),\n", + " basin.Time(time=[starttime, endtime]),\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Add a demand node\n", + "$50.000$ people live in Crystal City.\n", + "To represents the total flow rate or abstraction rate required to meet the water demand of $50.000$ people, another demand node needs to be added assuming a return flow of $60%$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "city = model.user_demand.add(\n", + " Node(9, Point(0, -1), name=\"city\"),\n", + " [\n", + " user_demand.Time(\n", + " # Total demand in m³/s\n", + " demand=[0.07, 0.08, 0.09, 0.10, 0.12, 0.14, 0.15, 0.14, 0.12, 0.10, 0.09, 0.08],\n", + " return_factor=0.6,\n", + " min_level=0,\n", + " priority=1,\n", + " time=pd.date_range(start=\"2022-01-01\", periods=12, freq=\"MS\"),\n", + " )\n", + " ],\n", + ") # fmt: skip" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.edge.add(main, reservoir, name=\"main\")\n", + "model.edge.add(minor, confluence, name=\"minor\")\n", + "model.edge.add(reservoir, irrigation, name=\"irrigation\")\n", + "model.edge.add(irrigation, confluence)\n", + "model.edge.add(reservoir, city, name=\"city\")\n", + "model.edge.add(city, confluence, name=\"city returnflow\")\n", + "model.edge.add(reservoir, diversion_weir, name=\"not diverted\")\n", + "model.edge.add(diversion_weir, confluence)\n", + "model.edge.add(confluence, weir)\n", + "model.edge.add(weir, sea, name=\"sea\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.plot();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "toml_path = base_dir / \"Crystal-3/ribasim.toml\"\n", + "model.write(toml_path)\n", + "cli_path = \"ribasim\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adjust the code\n", + "Adjust the naming of the Basin in the dictionary mapping and the saving file should be `Crystal-3`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# | include: false\n", + "from subprocess import run\n", + "\n", + "run(\n", + " [\n", + " \"julia\",\n", + " \"--project=../../core\",\n", + " \"--eval\",\n", + " f'using Ribasim; Ribasim.main(\"{toml_path.as_posix()}\")',\n", + " ],\n", + " check=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot reservoir storage and level" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_basin = pd.read_feather(base_dir / \"Crystal-3/results/basin.arrow\")\n", + "\n", + "# Create pivot tables and plot for Basin data\n", + "df_basin_wide = df_basin.pivot_table(\n", + " index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n", + ")\n", + "df_basin_wide = df_basin_wide.loc[:, pd.IndexSlice[:, reservoir.node_id]]\n", + "\n", + "# Plot level and storage on the same graph with dual y-axes\n", + "fig, ax1 = plt.subplots(figsize=(12, 6))\n", + "\n", + "# Plot level on the primary y-axis\n", + "color = \"b\"\n", + "ax1.set_xlabel(\"Time\")\n", + "ax1.set_ylabel(\"Level [m]\", color=color)\n", + "ax1.plot(df_basin_wide.index, df_basin_wide[\"level\"], color=color)\n", + "ax1.tick_params(axis=\"y\", labelcolor=color)\n", + "\n", + "# Create a secondary y-axis for storage\n", + "ax2 = ax1.twinx()\n", + "color = \"r\"\n", + "ax2.set_ylabel(\"Storage [m³]\", color=\"r\")\n", + "ax2.plot(df_basin_wide.index, df_basin_wide[\"storage\"], linestyle=\"--\", color=color)\n", + "ax2.tick_params(axis=\"y\", labelcolor=color)\n", + "\n", + "fig.tight_layout() # Adjust layout to fit labels\n", + "plt.title(\"Basin level and storage\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The figure above illustrates the storage and water level at the reservoir.\n", + "As expected, after increasing the profile of the Basin, its storage capacity increased as well.\n", + "\n", + "## Plot flows" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_flow = pd.read_feather(base_dir / \"Crystal-3/results/flow.arrow\")\n", + "# Add the edge names and then remove unnamed edges\n", + "df_flow[\"name\"] = model.edge.df[\"name\"].loc[df_flow[\"edge_id\"]].to_numpy()\n", + "df_flow = df_flow[df_flow[\"name\"].astype(bool)]\n", + "\n", + "# Plot the flow data, interactive plot with Plotly\n", + "pivot_flow = df_flow.pivot_table(\n", + " index=\"time\", columns=\"name\", values=\"flow_rate\"\n", + ").reset_index()\n", + "fig = px.line(pivot_flow, x=\"time\", y=pivot_flow.columns[1:], title=\"Flow [m3/s]\")\n", + "\n", + "fig.update_layout(legend_title_text=\"Edge\")\n", + "fig.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pixi.toml b/pixi.toml index de17158a5..347ec664a 100644 --- a/pixi.toml +++ b/pixi.toml @@ -38,14 +38,6 @@ initialize-julia = { depends_on = [ "instantiate-julia", ] } # Docs -build-julia-docs = { cmd = "julia --project docs/make.jl", depends_on = [ - "initialize-julia", -], inputs = [ - "core", - "docs/make.jl", -], outputs = [ - "docs/build", -] } quartodoc-build = { cmd = "quartodoc build && rm objects.json", cwd = "docs", inputs = [ "docs/_quarto.yml", "python/ribasim", @@ -61,7 +53,7 @@ quarto-render = { cmd = "julia --project --eval 'using Pkg; Pkg.build(\"IJulia\" "quartodoc-build", "generate-testmodels", ] } -docs = { depends_on = ["build-julia-docs", "quarto-preview"] } +docs = { depends_on = ["quarto-preview"] } # Lint mypy-ribasim-python = "mypy python/ribasim/ribasim" mypy-ribasim-testmodels = "mypy python/ribasim_testmodels/ribasim_testmodels"