from pathlib import Path
import matplotlib.pyplot as plt
import pandas as pd
import plotly.express as px
from ribasim import Model, Node
from ribasim.nodes import (
basin,
flow_boundary,
tabulated_rating_curve,
user_demand,
)from shapely.geometry import Point
Irrigation demand
= Path("crystal-basin")
base_dir
= "2022-01-01"
starttime = "2023-01-01"
endtime = Model(
model =starttime,
starttime=endtime,
endtime="EPSG:4326",
crs )
These nodes are identical to the previous tutorial:
# FlowBoundary
= pd.DataFrame({
data "time": pd.date_range(start="2022-01-01", end="2023-01-01", freq="MS"),
"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],
"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]
# fmt: skip
}) "total"] = data["minor"] + data["main"]
data[= model.flow_boundary.add(
main 1, Point(0.0, 0.0), name="main"),
Node(
[
flow_boundary.Time(=data.time,
time=data.main,
flow_rate
)
],
)= model.flow_boundary.add(
minor 2, Point(-3.0, 0.0), name="minor"),
Node(
[
flow_boundary.Time(=data.time,
time=data.minor,
flow_rate
)
],
)
# Basin
= model.basin.add(
confluence 3, Point(-1.5, -1), name="confluence"),
Node(
[=[672000, 5600000], level=[0, 6]),
basin.Profile(area=[4]),
basin.State(level=[starttime, endtime]),
basin.Time(time
],
)
# TabulatedRatingCurve
= model.tabulated_rating_curve.add(
weir 4, Point(-1.5, -1.5), name="weir"),
Node(
[
tabulated_rating_curve.Static(=[0.0, 2, 5],
level=[0.0, 50, 200],
flow_rate
)
],
)
# Terminal
= model.terminal.add(Node(5, Point(-1.5, -3.0), name="sea")) sea
1 Irrigation demand
Let us modify the environment to include agricultural activities within the basin, which necessitate irrigation. Water is diverted from the main river through an irrigation canal, with a portion of it eventually returning to the main river (see Figure 1).
For this schematization update, we need to incorporate three additional nodes:
- Basin: Represents a cross-sectional point where water is diverted.
- UserDemand: Represents the irrigation demand.
- TabulatedRatingCurve: Defines the remaining water flow from the main river at the diversion point.
1.1 Add a second Basin node
This Basin will portray as the point in the river where the diversion takes place, getting the name diversion
. Its profile area at this intersection is slightly smaller than at the confluence.
= model.basin.add(
diversion_basin 6, Point(-0.75, -0.5), name="diversion_basin"),
Node(
[=[500000, 5000000], level=[0, 6]),
basin.Profile(area=[3]),
basin.State(level=[starttime, endtime]),
basin.Time(time
], )
1.2 Add the irrigation demand
An irrigation district needs to apply irrigation to its field starting from April to September. The irrigated area is \(> 17000 \text{ ha}\) and requires around \(5 \text{ mm/day}\). 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. Start of irrigation takes place on the 1st of April until the end of August. The water intake is 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(
irrigation 7, Point(-1.5, 0.5), name="irrigation"),
Node(
[
user_demand.Time(=[0.0, 0.0, 10, 12, 12, 0.0],
demand=0,
return_factor=0,
min_level=1,
priority=[
time
starttime,"2022-03-31",
"2022-04-01",
"2022-07-01",
"2022-09-30",
"2022-10-01",
],
)
], )
1.3 Add a TabulatedRatingCurve
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. The rest of the water will flow naturally towards the confluence:
= model.tabulated_rating_curve.add(
diversion_weir 8, Point(-1.125, -0.75), name="diversion_weir"),
Node(
[
tabulated_rating_curve.Static(=[0.0, 1.5, 5],
level=[0.0, 45, 200],
flow_rate
)
], )
1.4 Add edges
="main")
model.edge.add(main, diversion_basin, name="minor")
model.edge.add(minor, confluence, name="irrigation")
model.edge.add(diversion_basin, irrigation, name
model.edge.add(irrigation, confluence)="not diverted")
model.edge.add(diversion_basin, diversion_weir, name
model.edge.add(diversion_weir, confluence)
model.edge.add(confluence, weir)="sea") model.edge.add(weir, sea, name
= base_dir / "Crystal-2/ribasim.toml"
toml_path
model.write(toml_path)= "ribasim" cli_path
1.5 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-2
:
; model.plot()
1.6 Plot and compare the Basin results
Plot the simulated levels and storages at the diverted section and at the confluence.
= pd.read_feather(
df_basin / "Crystal-2/results/basin.arrow", dtype_backend="pyarrow"
base_dir
)
# Create pivot tables and plot for basin data
= df_basin.pivot_table(
df_basin_wide ="time", columns="node_id", values=["storage", "level"]
index
)
= df_basin_wide.loc[:, pd.IndexSlice[:, diversion_basin.node_id]]
df_basin_div = df_basin_wide.loc[:, pd.IndexSlice[:, confluence.node_id]]
df_basin_conf
def plot_basin_data(
="b", storage_color="r", title="Basin"
ax, ax_twin, df_basin, level_color
):# Plot level data
for column in df_basin["level"].columns:
ax.plot(
df_basin.index,"level"][column],
df_basin[="-",
linestyle=level_color,
color=f"Level - {column}",
label
)
# Plot storage data
for column in df_basin["storage"].columns:
ax_twin.plot(
df_basin.index,"storage"][column],
df_basin[="--",
linestyle=storage_color,
color=f"Storage - {column}",
label
)
"Level [m]", color=level_color)
ax.set_ylabel("Storage [m³]", color=storage_color)
ax_twin.set_ylabel(
="y", labelcolor=level_color)
ax.tick_params(axis="y", labelcolor=storage_color)
ax_twin.tick_params(axis
ax.set_title(title)
# Combine legends from both axes
= ax.get_legend_handles_labels()
lines, labels = ax_twin.get_legend_handles_labels()
lines_twin, labels_twin + lines_twin, labels + labels_twin, loc="upper left")
ax.legend(lines
# Create subplots
= plt.subplots(2, 1, figsize=(12, 12), sharex=True)
fig, (ax1, ax3)
# Plot Div basin data
= ax1.twinx() # Secondary y-axis for storage
ax2 ="Diversion Basin level and storage")
plot_basin_data(ax1, ax2, df_basin_div, title
# Plot Conf basin data
= ax3.twinx() # Secondary y-axis for storage
ax4 ="Confluence Basin level and storage")
plot_basin_data(ax3, ax4, df_basin_conf, title
# Common X label
"Time")
ax3.set_xlabel(# Adjust layout to fit labels
fig.tight_layout() plt.show()
The figure above illustrates the water levels and storage capacities for each Basin.
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. 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.
1.7 Plot and compare the flow results
Plot the flow results in an interactive plotting tool.
= pd.read_feather(
df_flow / "Crystal-2/results/flow.arrow", dtype_backend="pyarrow"
base_dir
)# Add the edge names and then remove unnamed edges
"name"] = model.edge.df["name"].loc[df_flow["edge_id"]].to_numpy()
df_flow[= df_flow[df_flow["name"].astype(bool)]
df_flow
# Plot the flow data, interactive plot with Plotly
= df_flow.pivot_table(
pivot_flow ="time", columns="name", values="flow_rate"
index
).reset_index()= px.line(pivot_flow, x="time", y=pivot_flow.columns[1:], title="Flow [m3/s]")
fig
="Edge")
fig.update_layout(legend_title_text fig.show()
Try toggling the edges on and off by clicking on them in the edges.