Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document parsing Delwaq results. #1845

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 113 additions & 4 deletions docs/guide/delwaq.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"from pathlib import Path\n",
"\n",
"toml_path = Path(\"../../generated_testmodels/basic/ribasim.toml\")\n",
"\n",
"assert toml_path.is_file()"
]
},
Expand Down Expand Up @@ -54,6 +55,38 @@
"model.plot(); # for later comparison"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"model.basin.profile"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's add another tracer to the model, to setup a fraction calculation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from ribasim.delwaq import add_tracer\n",
"\n",
"add_tracer(model, 11, \"Foo\")\n",
"add_tracer(model, 15, \"Bar\")\n",
"display(model.flow_boundary.concentration) # flow boundaries\n",
"display(model.level_boundary.concentration) # flow boundaries\n",
"\n",
"model.write(toml_path)"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down Expand Up @@ -89,9 +122,8 @@
"source": [
"from ribasim.delwaq import generate\n",
"\n",
"output_path = Path(\n",
" \"../../generated_testmodels/basic/delwaq\"\n",
") # set a path where we store the Delwaq input files\n",
"output_path = Path(\"../../generated_testmodels/basic/delwaq\")\n",
"\n",
"graph, substances = generate(toml_path, output_path)"
]
},
Expand Down Expand Up @@ -182,6 +214,83 @@
"- Basin boundaries are split into separate nodes and links (drainage, precipitation, and evaporation, as indicated by the duplicated Basin IDs on the right hand side)\n",
"- All node IDs have been renumbered, with boundaries being negative, and Basins being positive."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Parsing the results\n",
"With Delwaq having run, we can now parse the results using `ribasim.delwaq.parse`. This function requires the `graph` and `substances` variables that were output by `ribasim.delwaq.generate`, as well as the path to the results folder of the Delwaq simulation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from ribasim.delwaq import parse\n",
"\n",
"nmodel = parse(toml_path, graph, substances, output_folder=output_path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The parsed model is identical to the Ribasim model, with the exception of the added concentration_external table that contains all tracer results from Delwaq."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"display(nmodel.basin.concentration_external)\n",
"print(substances)\n",
"t = nmodel.basin.concentration_external.df\n",
"t[t.time == t.time.unique()[2]]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can use this table to plot the results of the Delwaq model, both spatially as over time."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from ribasim.delwaq import plot_fraction\n",
"\n",
"plot_fraction(nmodel, 1) # default tracers, should add up to 1\n",
"plot_fraction(nmodel, 9, [\"Foo\", \"Bar\"]) # custom tracers\n",
"plot_fraction(nmodel, 9, [\"Continuity\"]) # mass balance check"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from ribasim.delwaq import plot_spatial\n",
"\n",
"plot_spatial(nmodel, \"Bar\")\n",
"plot_spatial(nmodel, \"Foo\", versus=\"Bar\") # ratio of Meuse to Rhine"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand All @@ -200,7 +309,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.4"
"version": "3.12.5"
}
},
"nbformat": 4,
Expand Down
14 changes: 11 additions & 3 deletions python/ribasim/ribasim/delwaq/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from .generate import generate
from .generate import add_tracer, generate
from .parse import parse
from .plot import plot
from .plot import plot_fraction, plot_spatial
from .util import run_delwaq

__all__ = ["generate", "parse", "run_delwaq", "plot"]
__all__ = [
"generate",
"parse",
"run_delwaq",
"plot",
"add_tracer",
"plot_fraction",
"plot_spatial",
]
39 changes: 36 additions & 3 deletions python/ribasim/ribasim/delwaq/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from datetime import timedelta
from pathlib import Path

from ribasim.utils import MissingOptionalModule
from ribasim import nodes
from ribasim.utils import MissingOptionalModule, _pascal_to_snake

try:
import networkx as nx
Expand Down Expand Up @@ -276,13 +277,14 @@
toml_path: Path,
output_folder=output_folder,
use_evaporation=USE_EVAP,
results_folder="results",
) -> tuple[nx.DiGraph, set[str]]:
"""Generate a Delwaq model from a Ribasim model and results."""

# Read in model and results
model = ribasim.Model.read(toml_path)
basins = pd.read_feather(toml_path.parent / "results" / "basin.arrow")
flows = pd.read_feather(toml_path.parent / "results" / "flow.arrow")
basins = pd.read_feather(toml_path.parent / results_folder / "basin.arrow")
flows = pd.read_feather(toml_path.parent / results_folder / "flow.arrow")

Check warning on line 287 in python/ribasim/ribasim/delwaq/generate.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/generate.py#L286-L287

Added lines #L286 - L287 were not covered by tests

output_folder.mkdir(exist_ok=True)

Expand Down Expand Up @@ -409,10 +411,14 @@
# Setup initial basin concentrations
defaults = {
"Continuity": 1.0,
"Initial": 1.0,
"Basin": 0.0,
"LevelBoundary": 0.0,
"FlowBoundary": 0.0,
"Terminal": 0.0,
"UserDemand": 0.0,
"Precipitation": 0.0,
"Drainage": 0.0,
}
substances.update(defaults.keys())

Expand Down Expand Up @@ -486,6 +492,33 @@
return G, substances


def add_tracer(model, node_id, tracer_name):
"""Add a tracer to the Delwaq model."""
n = model.node_table().df.loc[node_id]
node_type = n.node_type
if node_type not in [

Check warning on line 499 in python/ribasim/ribasim/delwaq/generate.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/generate.py#L497-L499

Added lines #L497 - L499 were not covered by tests
"Basin",
"LevelBoundary",
"FlowBoundary",
"UserDemand",
]:
raise ValueError("Can only trace Basins and boundaries")
snake_node_type = _pascal_to_snake(node_type)
nt = getattr(model, snake_node_type)

Check warning on line 507 in python/ribasim/ribasim/delwaq/generate.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/generate.py#L505-L507

Added lines #L505 - L507 were not covered by tests

ct = getattr(nodes, snake_node_type)
table = ct.Concentration(

Check warning on line 510 in python/ribasim/ribasim/delwaq/generate.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/generate.py#L509-L510

Added lines #L509 - L510 were not covered by tests
node_id=[node_id],
time=[model.starttime],
substance=[tracer_name],
concentration=[1.0],
)
if nt.concentration is None:
nt.concentration = table

Check warning on line 517 in python/ribasim/ribasim/delwaq/generate.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/generate.py#L516-L517

Added lines #L516 - L517 were not covered by tests
else:
nt.concentration = pd.concat([nt.concentration.df, table.df], ignore_index=True)

Check warning on line 519 in python/ribasim/ribasim/delwaq/generate.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/generate.py#L519

Added line #L519 was not covered by tests


if __name__ == "__main__":
# Generate a Delwaq model from the default Ribasim model
repo_dir = delwaq_dir.parents[1]
Expand Down
96 changes: 94 additions & 2 deletions python/ribasim/ribasim/delwaq/plot.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,94 @@
def plot():
pass
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1 import make_axes_locatable


def plot_fraction(
model,
node_id,
tracers=[
"Basin",
"LevelBoundary",
"FlowBoundary",
"UserDemand",
"Initial",
"Drainage",
"Precipitation",
"Terminal",
],
):
table = model.basin.concentration_external.df
table = table[table["node_id"] == node_id]
table = table[table["substance"].isin(tracers)]

Check warning on line 22 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L20-L22

Added lines #L20 - L22 were not covered by tests

groups = table.groupby("substance")
stack = {k: v["concentration"].to_numpy() for (k, v) in groups}

Check warning on line 25 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L24-L25

Added lines #L24 - L25 were not covered by tests

fig, ax = plt.subplots()
ax.stackplot(

Check warning on line 28 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L27-L28

Added lines #L27 - L28 were not covered by tests
groups.get_group(tracers[0])["time"],
stack.values(),
labels=stack.keys(),
)
ax.plot(

Check warning on line 33 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L33

Added line #L33 was not covered by tests
groups.get_group(tracers[0])["time"],
np.sum(list(stack.values()), axis=0),
c="black",
lw=2,
)
ax.legend()
ax.set_title(f"Fraction plot for node {node_id}")
ax.set_xlabel("Time")
ax.set_ylabel("Fraction")

Check warning on line 42 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L39-L42

Added lines #L39 - L42 were not covered by tests

plt.show(fig)

Check warning on line 44 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L44

Added line #L44 was not covered by tests


def plot_spatial(model, tracer="Basin", versus=None, limit=0.001):
table = model.basin.concentration_external.df
table = table[table["time"] == table["time"].max()]

Check warning on line 49 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L48-L49

Added lines #L48 - L49 were not covered by tests

if versus is not None:
vtable = table[table["substance"] == versus]
vtable.set_index("node_id", inplace=True)
table = table[table["substance"] == tracer]
table.set_index("node_id", inplace=True)

Check warning on line 55 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L51-L55

Added lines #L51 - L55 were not covered by tests

nodes = model.node_table().df
nodes = nodes[nodes.index.isin(table.index)]

Check warning on line 58 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L57-L58

Added lines #L57 - L58 were not covered by tests

if versus is None:
c = table["concentration"][nodes.index]
alpha = c > limit

Check warning on line 62 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L60-L62

Added lines #L60 - L62 were not covered by tests
else:
alpha = (

Check warning on line 64 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L64

Added line #L64 was not covered by tests
table["concentration"][nodes.index] + vtable["concentration"][nodes.index]
)
c = table["concentration"][nodes.index] / alpha

Check warning on line 67 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L67

Added line #L67 was not covered by tests

fig, ax = plt.subplots()
s = ax.scatter(

Check warning on line 70 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L69-L70

Added lines #L69 - L70 were not covered by tests
nodes.geometry.x,
nodes.geometry.y,
c=c,
clim=(0, 1),
alpha=alpha,
)
dt = table["time"].iloc[0]
if versus is None:
ax.set_title(f"Scatter plot for {tracer} tracer at {dt}")

Check warning on line 79 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L77-L79

Added lines #L77 - L79 were not covered by tests
else:
ax.set_title(f"Scatter plot for {tracer} vs {versus} tracer at {dt}")

Check warning on line 81 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L81

Added line #L81 was not covered by tests

divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.05)

Check warning on line 84 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L83-L84

Added lines #L83 - L84 were not covered by tests

fig.colorbar(s, cax=cax, orientation="vertical")
if versus is not None:
cax.set_ylabel(f"{tracer} fraction vs {versus} fraction")

Check warning on line 88 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L86-L88

Added lines #L86 - L88 were not covered by tests
else:
cax.set_ylabel(f"Overall {tracer} fraction")
ax.set_xlabel("x")
ax.set_ylabel("y")

Check warning on line 92 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L90-L92

Added lines #L90 - L92 were not covered by tests

plt.show(fig)

Check warning on line 94 in python/ribasim/ribasim/delwaq/plot.py

View check run for this annotation

Codecov / codecov/patch

python/ribasim/ribasim/delwaq/plot.py#L94

Added line #L94 was not covered by tests
12 changes: 11 additions & 1 deletion python/ribasim/tests/test_delwaq.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from pathlib import Path

import pytest
from ribasim.delwaq import generate, parse, run_delwaq
from ribasim import Model
from ribasim.delwaq import add_tracer, generate, parse, run_delwaq

delwaq_dir = Path(__file__).parent

Expand All @@ -14,6 +15,10 @@ def test_offline_delwaq_coupling():
repo_dir = delwaq_dir.parents[2]
toml_path = repo_dir / "generated_testmodels/basic/ribasim.toml"

model = Model.read(toml_path)
add_tracer(model, 17, "Foo")
model.write(toml_path)

graph, substances = generate(toml_path)
run_delwaq()
model = parse(toml_path, graph, substances)
Expand All @@ -26,8 +31,13 @@ def test_offline_delwaq_coupling():
"Basin",
"Cl",
"Continuity",
"Drainage",
"FlowBoundary",
"Foo",
"Initial",
"LevelBoundary",
"Precipitation",
"Terminal",
"Tracer",
"UserDemand",
]
Loading