Quick start guide

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

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

2 Starting RIBASIM

2.1 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

2.2 Installation

  1. Download Ribasim: Obtain the Ribasim 9 installation package from the official website: Ribasim - Installation under chapter ‘2 Download’:
  • For Windows download: ribasim_windows.zip
  • For Linux: ribasim_linux.zip
  1. 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;
  2. 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:
error: the following required arguments were not provided:
 <TOML_PATH>
Usage: ribasim <TOML_PATH>
For more information, try '--help'.
  1. We use a command line interface (CLI) to install our Ribasim packages. To install Ribasim open PowerShell or Windows command prompt and write:
conda install ribasim

or

mamba install ribasim

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

3 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 Figure 1). 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.

Figure 1: Crystal Basin based on natural flow

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

3.1 Modual 1.1 - Natural Flow

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

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

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

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",
)

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

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,
    )]
)

3.1.4 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. Figure 2 visualizes a cross section of the confluence point in our model.

Figure 2: Basin node concept for the confluence

Table 1 shows the input data for the basin node profile.

Table 1: Profile data for the basin node
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:

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]),
    ],
)

3.1.5 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 Table 2 will be used as input.

Table 2: Input data for the Tabulated Rating Curve
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. Figure 3 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.
Figure 3: Discharge at corresponding water levels

Taking this into account, add the Tabulated Rating Curve as follows:

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],
        )
    ]
)

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

model.terminal.add(Node(5, Point(-1.5, -3.0), name="Terminal"))

3.1.7 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.
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])

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

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

Figure 4: Schematization of the Crystal basin 1.1

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:

subprocess.run([rib_path, toml_path], check=True)

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

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()

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

Figure 5: Simulated basin level and storage

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

Figure 6: Simulated flows on each edge

3.2 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 Figure 7).

Figure 7: Crystal basin with 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.

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

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

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

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]),
    ],
)

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

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"]
        )
    ]
)

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

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.

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

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])

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

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

Figure 8: Schematization of the Crystal basin with irrigation

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

# 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',
}

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


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()

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

Figure 9: Simulated basin levels and storages

3.2.9 Step 9: Plot and compare the flow results

Plot the flow results in an interactive plotting tool.

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 (Figure 10).

Figure 10: Simulated flows of each edge

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 (Figure 11) and the return flow remains zero, as the assumption defined before was that there is no drain.

Figure 11: Supplied irrigation and return flow

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

Figure 12: Simulated flow to Terminal

4 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 Figure 13). 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.

Figure 13: Crystal basin with demands and a reservoir

4.1 Modual 2.1 – Reservoir

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

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]),
    ],
)

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

toml_path = model_dir/ "Crystal_2.1/ribasim.toml"
model.write(toml_path)
rib_path = base_dir / "ribasim_windows/ribasim.exe"
# 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")
# 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)
# 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')
# 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()

4.1.3 Step 3: Plotting results

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

Figure 14: Simulated basin storages and levels

4.2 Module 2.2 – Public Water Supply

4.2.1 Step 1: Rename the saving files

Rename the files to Crystal_2.2

4.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%\).

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"
        ]
    )]
)

4.2.3 Step 3: Add the edges

The connection between the reservoir and the demand node needs to be defined:

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])

4.2.4 Step 4: Adjust the name dictionaries

# 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',
}

4.2.5 Step 5: Check the simulated demands

Figure 15 shows the flow to (PWS Demand) and out (PWS Return) of the PWS node. Figure 16 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.

Figure 15: Simulated flows to and from the city
Figure 16: Simulated basin storages and levels