Ribasim Delwaq coupling

In order to generate the Delwaq input files, we need a completed Ribasim simulation (typically one with a results folder) that ideally also includes some substances and initial concentrations. Let’s take the basic test model for example, which already has set some initial concentrations.

All testmodels can be downloaded from here.

1 Generating Delwaq input

from pathlib import Path

toml_path = Path("../../generated_testmodels/basic/ribasim.toml")

This Ribasim model already has substance concentrations for Cl and Tracer in the input tables, and we will use these to generate the Delwaq input files.

from ribasim import Model, run_ribasim

model = Model.read(toml_path)

display(model.basin.concentration_state)  # basin initial state
display(model.basin.concentration)  # basin boundaries
display(model.basin.mass_load)  # basin mass loads
display(model.flow_boundary.concentration)  # flow boundaries
display(model.level_boundary.concentration)  # level boundaries
model.plot()  # for later comparison
Basin / concentration_state
node_id substance concentration
fid
0 1 Cl 1.0
1 1 Tracer 2.0
2 3 Cl 1.0
3 3 Tracer 2.0
4 6 Cl 1.0
5 6 Tracer 2.0
6 9 Cl 1.0
7 9 Tracer 2.0
Basin / concentration
node_id time substance drainage precipitation surface_runoff
fid
0 1 2020-01-01 Cl 0.0 0.0 0.0
1 1 2020-01-02 Cl 1.0 1.0 1.0
2 1 2020-01-01 Tracer 1.0 1.0 1.0
3 3 2020-01-01 Cl 0.0 0.0 0.0
4 3 2020-01-02 Cl 1.0 1.0 1.0
5 3 2020-01-01 Tracer 1.0 1.0 1.0
6 6 2020-01-01 Cl 0.0 0.0 0.0
7 6 2020-01-02 Cl 1.0 1.0 1.0
8 6 2020-01-01 Tracer 1.0 1.0 1.0
9 9 2020-01-01 Cl 0.0 0.0 0.0
10 9 2020-01-02 Cl 1.0 1.0 1.0
11 9 2020-01-01 Tracer 1.0 1.0 1.0
Basin / mass_load
node_id time substance load
fid
0 1 2020-01-01 Basic 0.001
1 1 2020-01-01 Tracer 0.002
2 3 2020-01-01 Basic 0.001
3 3 2020-01-01 Tracer 0.002
4 6 2020-01-01 Basic 0.001
5 6 2020-01-01 Tracer 0.002
6 9 2020-01-01 Basic 0.001
7 9 2020-01-01 Tracer 0.002
FlowBoundary / concentration
node_id time substance concentration
fid
0 15 2020-01-01 Cl 0.0
1 15 2020-01-01 Tracer 1.0
2 16 2020-01-01 Cl 0.0
3 16 2020-01-01 Tracer 1.0
LevelBoundary / concentration
node_id time substance concentration
fid
0 11 2020-01-01 Cl 34.0
1 17 2020-01-01 Cl 34.0

model.basin.profile
Basin / profile
node_id area level storage
fid
0 1 0.01 0.0 NaN
1 1 1000.00 1.0 NaN
2 3 0.01 0.0 NaN
3 3 1000.00 1.0 NaN
4 6 0.01 0.0 NaN
5 6 1000.00 1.0 NaN
6 9 0.01 0.0 NaN
7 9 1000.00 1.0 NaN

Let’s add another tracer to the model, to setup a fraction calculation.

from ribasim.delwaq import add_tracer

add_tracer(model, 11, "Foo")
add_tracer(model, 15, "Bar")
display(model.flow_boundary.concentration)  # flow boundaries
display(model.level_boundary.concentration)  # flow boundaries

model.write(toml_path)
FlowBoundary / concentration
node_id time substance concentration
fid
0 15 2020-01-01 Cl 0.0
1 15 2020-01-01 Tracer 1.0
2 16 2020-01-01 Cl 0.0
3 16 2020-01-01 Tracer 1.0
4 15 2020-01-01 Bar 1.0
LevelBoundary / concentration
node_id time substance concentration
fid
0 11 2020-01-01 Cl 34.0
1 17 2020-01-01 Cl 34.0
2 11 2020-01-01 Foo 1.0
PosixPath('../../generated_testmodels/basic/ribasim.toml')
run_ribasim(toml_path)
⠀⠀⠀⠀⠀⠀⠀⣿⣿⢂⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⣾⣶⣶⣿⡿⢟⣴⣶⡆⣾⣿⡆⠀⠀
⠀⢀⣀⢠⣶⣷⢻⡇⠀⢀⣄⠟⠉⠙⢿⣮⣭⠀⠀⠀
⠀⢿⣿⢇⣿⠃⠈⠃⣰⣿⣿⣧⠀⣠⣈⣛⠿⠀⠀⠀  Ribasim v2026.1.2
⠀⠀⠰⣿⣧⣀⠀⠰⣿⣿⣿⣿⡇⠀⠈⢹⣿⡦⠀⠀  Water resources modeling
⠀⠀⠀⢰⣮⣍⠉⠁⠙⠻⠿⠛⠐⡄⢠⣾⡏⣾⣿⡆
⠀⠀⠀⢘⣛⢿⣆⣀⣠⡞⠀⠀⠀⣿⣸⠿⠗⠈⠉⠀
⠀⠀⠀⢿⣿⠇⠿⠟⢿⣾⣿⠿⠿⠿⠃⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⣿⣿⠆⠀⠀⠀⠀⠀⠀

┌ Info: Starting a Ribasim simulation at 2026-06-17T20:55:30.626.
│   toml_path = "../../generated_testmodels/basic/ribasim.toml"
│   ribasim_version = "2026.1.2"
│   ribasim_home = "/home/runner/.julia/juliaup/julia-1.12.6+0.x64.linux.gnu"
│   starttime = 2020-01-01T00:00:00
│   endtime = 2021-01-01T00:00:00
└   threads = 1
┌ Warning: The following experimental features are enabled: concentration
└ @ Ribasim /home/runner/work/Ribasim/Ribasim/core/src/logging.jl:56
Simulating   0%|                                        |  ETA: N/A
Simulating   6%|██▎                                     |  ETA: 0:02:44
Simulating  33%|█████████████▎                          |  ETA: 0:00:20
Simulating  60%|████████████████████████▏               |  ETA: 0:00:07
Simulating  88%|███████████████████████████████████▏    |  ETA: 0:00:01
Simulating 100%|████████████████████████████████████████| Time: 0:00:10
[ Info: Computation time: 6 seconds, 780 milliseconds
[ Info: Writing results.
[ Info: The model finished successfully at 2026-06-17T20:56:26.217.

Given the path to a completed Ribasim simulation, we can call ribasim.delwaq.generate for generating the required input files for Delwaq from scratch. ribasim.delwaq.generate either takes a Model instance, or the path to a toml file, as well as an output_path keyword, where the input for Delwaq will be written. By default it is set to the delwaq folder next to the toml.

from ribasim.delwaq import generate

# The default path is the delwaq folder next to the toml
output_path = Path("../../generated_testmodels/basic/delwaq")

generate(model, output_path)
ribasim.Model(
    input_dir=PosixPath('input'),
    endtime=datetime.datetime(2021, 1, 1, 0, 0),
    ribasim_version='2026.1.2',
    link=Link(...),
    filepath=PosixPath('../../generated_testmodels/basic/ribasim.toml'),
    starttime=datetime.datetime(2020, 1, 1, 0, 0),
    crs='EPSG:28992',
    logging=Logging(verbosity='debug'),
    node=Node(...),
    results_dir=PosixPath('results'),
    experimental=Experimental(concentration=True, allocation=False),
)

This call produces a handful of files in the user defined folder. Let’s take a look at them:

list(output_path.iterdir())
[PosixPath('../../generated_testmodels/basic/delwaq/bndlist.csv'),
 PosixPath('../../generated_testmodels/basic/delwaq/ribasim.poi'),
 PosixPath('../../generated_testmodels/basic/delwaq/ribasim_bndlist.inc'),
 PosixPath('../../generated_testmodels/basic/delwaq/ribasim.vol'),
 PosixPath('../../generated_testmodels/basic/delwaq/B5_bounddata.inc'),
 PosixPath('../../generated_testmodels/basic/delwaq/ribasim.are'),
 PosixPath('../../generated_testmodels/basic/delwaq/B6_wasteloads.inc'),
 PosixPath('../../generated_testmodels/basic/delwaq/B8_initials.inc'),
 PosixPath('../../generated_testmodels/basic/delwaq/dimr_config.xml'),
 PosixPath('../../generated_testmodels/basic/delwaq/ribasim.flo'),
 PosixPath('../../generated_testmodels/basic/delwaq/network.csv'),
 PosixPath('../../generated_testmodels/basic/delwaq/ribasim.nc'),
 PosixPath('../../generated_testmodels/basic/delwaq/ribasim.atr'),
 PosixPath('../../generated_testmodels/basic/delwaq/delwaq.inp'),
 PosixPath('../../generated_testmodels/basic/delwaq/ribasim.len')]

These files form a complete Delwaq simulation, and can be run by either pointing DIMR to the dimr_config.xml file or pointing Delwaq to the delwaq.inp file.

The call to generate also writes a ribasim.nc file in the output folder. Besides the Delwaq network mesh, it stores the mapping between Delwaq and Ribasim node IDs and the list of substances. These are everything parse needs to map the Delwaq results back to the original Ribasim model, so no extra variables have to be passed between generate and parse.

The substances stored are a combination of user input (Cl and Tracer in the input tables), a Continuity tracer, and tracers for all nodetypes in the Ribasim model. The latter tracers allow for deeper inspection of the Ribasim model, such as debugging the mass balance by plotting fraction graphs.

The Delwaq network that generate builds from the Ribasim model is very similar to the Ribasim network, with some notable changes:

  • All non-Basin or non-boundary types are removed (e.g. no more Pumps or TabulatedRatingCurves)
  • Basin boundaries are split into separate nodes and links (drainage, precipitation, and evaporation)
  • All node IDs have been renumbered, with boundaries being negative, and Basins being positive.

2 Parsing the results

With Delwaq having run, we can now parse the results using ribasim.delwaq.parse. This function requires either a path to a toml file, or a Model instance. The mapping and substances needed to interpret the results are read from the ribasim.nc file written by ribasim.delwaq.generate. You can optionally set the path to the results folder of the Delwaq simulation, if you overrode the default during ribasim.delwaq.generate.

from ribasim.delwaq import parse

nmodel_nc = parse(model, output_path, to_input=False)

concentration_nc = toml_path.parent / "results" / "concentration.nc"
concentration_nc
PosixPath('../../generated_testmodels/basic/results/concentration.nc')

By default (to_input=False), the parsed model writes the Delwaq tracer results to results/concentration.nc and does not populate the Basin / concentration_external table.

# Parse Delwaq results and also populate Basin / concentration_external for plotting
nmodel = parse(model, output_path, to_input=True)
display(nmodel.basin.concentration_external)
t = nmodel.basin.concentration_external.df
print(sorted(t.substance.unique()))
# Show the first available timestep
t[t.time == t.time.unique()[0]]
Basin / concentration_external
time node_id concentration substance
fid
0 2020-01-01 1 1.000000 Continuity
1464 2020-01-01 1 0.000000 Precipitation
2928 2020-01-01 1 2.000000 Tracer
4392 2020-01-01 1 1.000000 Cl
5856 2020-01-01 1 0.000000 Bar
... ... ... ... ...
13175 2020-12-31 9 0.645681 FlowBoundary
14639 2020-12-31 9 0.000088 Initial
16103 2020-12-31 9 0.019346 LevelBoundary
17567 2020-12-31 9 0.167443 SurfaceRunoff
19031 2020-12-31 9 14.467084 Basic

19032 rows × 4 columns

['Bar', 'Basic', 'Cl', 'Continuity', 'Drainage', 'FlowBoundary', 'Foo', 'Initial', 'LevelBoundary', 'Precipitation', 'SurfaceRunoff', 'Tracer', 'UserDemand']
time node_id concentration substance
fid
0 2020-01-01 1 1.0 Continuity
1464 2020-01-01 1 0.0 Precipitation
2928 2020-01-01 1 2.0 Tracer
4392 2020-01-01 1 1.0 Cl
5856 2020-01-01 1 0.0 Bar
7320 2020-01-01 1 0.0 Foo
8784 2020-01-01 1 0.0 UserDemand
10248 2020-01-01 1 0.0 Drainage
11712 2020-01-01 1 0.0 FlowBoundary
13176 2020-01-01 1 1.0 Initial
14640 2020-01-01 1 0.0 LevelBoundary
16104 2020-01-01 1 0.0 SurfaceRunoff
17568 2020-01-01 1 0.0 Basic
1 2020-01-01 3 1.0 Continuity
1465 2020-01-01 3 0.0 Precipitation
2929 2020-01-01 3 2.0 Tracer
4393 2020-01-01 3 1.0 Cl
5857 2020-01-01 3 0.0 Bar
7321 2020-01-01 3 0.0 Foo
8785 2020-01-01 3 0.0 UserDemand
10249 2020-01-01 3 0.0 Drainage
11713 2020-01-01 3 0.0 FlowBoundary
13177 2020-01-01 3 1.0 Initial
14641 2020-01-01 3 0.0 LevelBoundary
16105 2020-01-01 3 0.0 SurfaceRunoff
17569 2020-01-01 3 0.0 Basic
2 2020-01-01 6 1.0 Continuity
1466 2020-01-01 6 0.0 Precipitation
2930 2020-01-01 6 2.0 Tracer
4394 2020-01-01 6 1.0 Cl
5858 2020-01-01 6 0.0 Bar
7322 2020-01-01 6 0.0 Foo
8786 2020-01-01 6 0.0 UserDemand
10250 2020-01-01 6 0.0 Drainage
11714 2020-01-01 6 0.0 FlowBoundary
13178 2020-01-01 6 1.0 Initial
14642 2020-01-01 6 0.0 LevelBoundary
16106 2020-01-01 6 0.0 SurfaceRunoff
17570 2020-01-01 6 0.0 Basic
3 2020-01-01 9 1.0 Continuity
1467 2020-01-01 9 0.0 Precipitation
2931 2020-01-01 9 2.0 Tracer
4395 2020-01-01 9 1.0 Cl
5859 2020-01-01 9 0.0 Bar
7323 2020-01-01 9 0.0 Foo
8787 2020-01-01 9 0.0 UserDemand
10251 2020-01-01 9 0.0 Drainage
11715 2020-01-01 9 0.0 FlowBoundary
13179 2020-01-01 9 1.0 Initial
14643 2020-01-01 9 0.0 LevelBoundary
16107 2020-01-01 9 0.0 SurfaceRunoff
17571 2020-01-01 9 0.0 Basic

We can use this table to plot the results of the Delwaq model, both spatially as over time.

from ribasim.delwaq import plot_fraction

# Plot default tracers (sum to 1), plus custom tracers on node 9
plot_fraction(nmodel, 1)  # default tracers, should add up to 1
plot_fraction(nmodel, 9, ["Foo", "Bar"])  # custom tracers
plot_fraction(nmodel, 9, ["Continuity"])  # mass balance check

from ribasim.delwaq import plot_spatial

plot_spatial(nmodel, "Bar")
plot_spatial(nmodel, "Foo", versus="Bar")