Model viewer

The Model viewer shows the input and results of the coupled LHM model (lhm_coupled) of main on a web map. It is a static web app: the docs site serves only the JavaScript, and the browser reads the model data directly from MinIO. The model of a pull request can be previewed too, together with its differences from main, at https://ribasim.nl/viewer?pr=<number>. Any other published version can be viewed, or compared with another, by its commit, see Links to a version.

The Water authority choice above the layer list shows the nodes and links of one authority, by the waterbeheercode their node ids start with, see ribasim_nl.aquo. It also shows the links to other authorities and the nodes at their other end, such as the regional inlets and outlets of the hws, and the Basin / areas of the shown Basins. The choice is kept in the URL, as #authority=<waterbeheercode>.

flowchart LR
  A[modellen/lhm_coupled] -->|dvc repro run_lhm_coupled| R[runs/lhm_coupled]
  R -->|dvc repro export_webmap| B[webmap/lhm_coupled]
  B -->|pixi run publish-webmap| C[MinIO doc-image/webmap]
  C --> D[Model viewer page]
  L[dvc.lock on GitHub main] --> D

All local paths are relative to data/Rijkswaterstaat.

Fit to the measurements

The Observations choice in the time bar colors the Observation nodes by how well the simulated values fit their measurements, see ribasim_nl.goodness_of_fit:

  • KGE and NSE: the Kling-Gupta and Nash-Sutcliffe efficiencies over the whole run, from purple at -1 to green at 1. At 0 the results are as good as the mean of the measurements.
  • bias: the difference of the mean simulated and measured value, relative to the mean absolute measurement. It is green within 25%, the limit of a satisfactory bias of discharges of Moriasi et al. (2007), grey at 50%, and purple from 100% either way.
  • simulated − observed: the difference at the time of the time bar, from red for too low to blue for too high. It is colored on a symmetric log scale, since the differences of large rivers and small ditches span orders of magnitude.

The efficiencies and bias need 365 days with both a measured and a simulated value, so Observation nodes with fewer days, or without a measurement at the time of the time bar, are small grey dots. Change the limits of a color scale in its legend. The Observations box above the layer list lists the shown Observation nodes by their fit, the best or the worst first, with the median, and zooms to one when clicked. The panel of an Observation node shows its fit above the chart of its observed and simulated values.

Updating the published data

  1. Run the model with the run_lhm_coupled DVC stage. It copies the model of modellen/lhm_coupled to runs/lhm_coupled, and writes the results next to it. It also writes the commit of the code to runinfo.json, which the viewer shows next to the scale bar, with the time the run finished from results/ribasim.log. For now this runs with a looser solver tolerance, see pipeline/run_lhm_coupled.py. The NetCDF results are compressed, to save space in DVC. This takes many hours, so normally pull the cached results instead:

    pixi run dvc pull run_lhm_coupled
  2. Export the model to web-friendly files with the export_webmap DVC stage, which takes about 10 minutes:

    pixi run dvc repro --pull --single-item export_webmap

    This writes data/Rijkswaterstaat/webmap/lhm_coupled, see ribasim_nl.webmap for details:

    File Content
    nodes.parquet Node table with all attributes and lon/lat
    links.parquet Link table with all attributes and simplified geometry
    basin_area.pmtiles Basin / area polygons as vector tiles
    waterboards.geojson Simplified water board boundaries
    tables/*.parquet All other input tables, including those in NetCDF, sorted by node_id
    results/*_by_id.parquet Basin and flow results sorted by id, for the time series charts
    results/*_by_time.parquet Basin level and storage, and flow rate, ordered by time step, for coloring the map
    results/observation_metrics.parquet How well the results fit the measurements of each Observation node, see Fit to the measurements
    results/observation_by_time.parquet The difference simulated − observed of each Observation node, ordered by time step, for coloring the map
    manifest.json List of the files above with their content hash and size, the color scale of each result variable, and when and from which commit the model was run

    Results are only exported if the model’s results directory exists. DVC does not cache this output, since the published model on MinIO is a copy of it already. dvc.lock does record its hash, which names the published model.

  3. Push the run and commit the updated dvc.lock, see DVC:

    pixi run dvc push run_lhm_coupled
  4. Publish the model as a preview of the pull request of the current branch. This needs credentials with write access in .env, and the GitHub CLI gh, logged in.

    pixi run publish-webmap

    This uploads the model of the working tree, and that of origin/main, which it fetches first, and their differences, see Comparing models. It then adds a comment to the pull request with the link https://ribasim.nl/viewer?pr=<number> and a summary of the differences, or updates that comment on later publishes. Use --pr <number> to publish for another pull request, and --main to only publish the model of origin/main. Use --commit <revision> to publish the model of a commit on GitHub, and add --base <revision> to also publish its differences from another commit, see Links to a version.

pixi run repro-local runs steps 1 to 4 by default, see Running locally. The docs do not need to be rebuilt to show another model.

How the published models are found

Everything is uploaded to the anonymously readable doc-image/webmap/ prefix on MinIO, see ribasim_nl.webmap_publish:

Object Content
files/<hash>.<ext> The exported files, named by their content hash, so unchanged files are uploaded once and shared by all models
lhm_coupled/snapshots/<md5>/manifest.json The manifest of an export, named by the DVC hash of the export_webmap output in dvc.lock
lhm_coupled/main.json The snapshot of the latest published main
lhm_coupled/diffs/<base md5>-<head md5>.json The differences between two snapshots
lhm_coupled/prs/<number>.json The snapshots of a pull request and the main it was compared with, and their differences

To show main, the viewer reads dvc.lock from main on GitHub, and loads the snapshot of its export_webmap output. So once a pull request whose model was published is merged, the viewer shows its model, without publishing again. If that snapshot is not published, for instance because a pull request changed the model without publishing it, the viewer shows the snapshot in main.json with a warning. The next pixi run publish-webmap then exports the run of main again, and publishes it. Likewise, to compare a pull request with main, the export of main is downloaded from its snapshot, or exported again from its run if it is not published. For a pull request, the viewer warns if main changed since the model was published; publish again to compare with the latest main. Snapshots and files that are no longer used are not deleted.

Viewing local models

The viewer can also show any local model, without publishing it:

pixi run viewer data/Rijkswaterstaat/modellen/hws_2025_10_1/hws.toml

This exports the model with ribasim_nl.webmap to .cache/viewer, serves it with the viewer on http://localhost:8000, and opens it in a web browser. The export is reused as long as the model, its results and the export code are unchanged, so only the first run takes long; about 10 minutes for the coupled LHM model with results. Results are shown if the model’s results directory exists. Use --port to serve on another port, and --no-browser to not open a browser.

Comparing models

To review a pull request that changes a model, view only the differences from another version of the model. Usually that is the same model at another git revision, like main:

pixi run viewer data/Rijkswaterstaat/runs/lhm_coupled/lhm_coupled.toml --base origin/main

--base takes a git revision or the TOML of another model. The model at a revision is read with DVC, from the DVC cache or the DVC remote, into .cache/viewer/revisions. Add --rev to also take the viewed model from a git revision instead of the working tree, for instance --rev HEAD --base origin/main. Fetch first to compare with the latest origin/main.

Nodes are matched by id, and links by their from and to node, since links get renumbered, see ribasim_nl.webmap_diff. A node or link in both models is changed if its geometry, an attribute, its rows in an input table, or its Basin / area differ. Initial Basin levels in Basin / state that differ less than 1 cm are equal, since every run of the regional models changes them slightly, see TABLE_TOLERANCES in ribasim_nl.webmap_diff. The map shows only the added, removed and changed features, in the colors of their status: green, orange and purple, on a grey base map. When zoomed out, the status rings around changed nodes become small dots. Removed and changed Basin / areas of the old model are drawn as dashed outlines. Turn on Unchanged in the layer list to show the other features faintly, for context. Observation nodes and their links are always shown, also when unchanged or filtered out in the Differences box, so their charts can be compared; the observed values are charted with the simulated values of both models. The simulated values of the base model are those of the observed node in the base model, also if its links were renumbered or lead to renumbered nodes.

If both models have results, the Basin levels and flow rates are compared too. Basins and flow links whose results differ more than a tolerance are also shown, in shades of grey by the largest difference. Differences of up to 1 mm in level, and of up to 1 L/s plus 1% in flow rate, are ignored as solver noise.

The Differences box counts the differences by status and by what changed, for instance Basin / state or geometry. Its View without differences link shows the model normally, at the same place, by adding &compare=0 to the URL. Uncheck a status or a change to hide those features from the map, for instance to look past the many changed initial states. It also lists the changed TOML settings, all differing features, and the features whose results differ, largest first; click one to zoom to it. The panel of a changed feature lists what changed, shows the old value of changed attributes struck through, and shows both versions of changed tables. The result charts show the old model as dashed lines in a lighter tint.

If both models have the fit to the measurements, the time bar has the Observations choice only, which can also color the Observation nodes by the change of their KGE, NSE or absolute bias, from purple for a worse to green for a better fit. The panel of an Observation node shows the fit of both models.

Developing the viewer

The viewer lives in viewer/ and is written in TypeScript, built with Vite. It uses MapLibre GL JS for the basemap and the Basin / area tiles, deck.gl to draw the nodes and links, and hyparquet to read Parquet files with HTTP range requests.

Start a development server that serves your local export_webmap output, and open http://localhost:5173:

pixi run viewer-dev

Changes to the TypeScript code reload the page automatically. pixi run viewer-build checks types and builds the viewer into docs/viewer-app. pixi run docs and pixi run docs-render run it before rendering the docs. Add ?data=<url> to the viewer page URL to load the data from another location, and &base_data=<url>&diff=<url> to compare it with another model, for instance ?data=http://localhost:5173/ to use the development server. Only the default MinIO host, the docs site itself and localhost are accepted.