├── docs
├── .nojekyll
├── _publish.yml
├── CNAME
├── assets
│ ├── ribasim.ico
│ ├── styles.css
│ ├── c4_system.mmd
│ └── c4_component_ribasim.mmd
├── contact.qmd
├── reference
│ ├── index.qmd
│ ├── node
│ │ ├── terminal.qmd
│ │ ├── linear-resistance.qmd
│ │ ├── flow-demand.qmd
│ │ ├── junction.qmd
│ │ ├── level-boundary.qmd
│ │ ├── level-demand.qmd
│ │ ├── flow-boundary.qmd
│ │ └── continuous-control.qmd
│ └── test-models.qmd
├── .gitignore
├── _extensions
│ └── quarto-ext
│ │ └── include-code-files
│ │ ├── _extension.yml
│ │ └── include-code-files.lua
├── dev
│ ├── copilot.qmd
│ ├── python.qmd
│ ├── benchmark.qmd
│ └── qgis_test_plan.qmd
├── known_issues.qmd
├── run_ribasim.py
├── guide
│ ├── coupling.qmd
│ ├── debug.qmd
│ └── updating-ribasim.qmd
└── index.qmd
├── python
├── ribasim
│ ├── ribasim
│ │ ├── py.typed
│ │ ├── delwaq
│ │ │ ├── .gitignore
│ │ │ ├── __init__.py
│ │ │ ├── template
│ │ │ │ ├── B8_initials.inc.j2
│ │ │ │ ├── delwaq.atr.j2
│ │ │ │ └── B5_bounddata.inc.j2
│ │ │ ├── reference
│ │ │ │ └── dimr_config.xml
│ │ │ ├── parse.py
│ │ │ └── README.md
│ │ ├── nodes
│ │ │ ├── linear_resistance.py
│ │ │ ├── manning_resistance.py
│ │ │ ├── pump.py
│ │ │ ├── outlet.py
│ │ │ ├── pid_control.py
│ │ │ ├── flow_demand.py
│ │ │ ├── level_demand.py
│ │ │ ├── tabulated_rating_curve.py
│ │ │ ├── continuous_control.py
│ │ │ ├── user_demand.py
│ │ │ ├── level_boundary.py
│ │ │ ├── discrete_control.py
│ │ │ ├── flow_boundary.py
│ │ │ ├── __init__.py
│ │ │ └── basin.py
│ │ ├── geometry
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ └── area.py
│ │ ├── styles
│ │ │ └── README.md
│ │ └── __init__.py
│ ├── tests
│ │ ├── test_input_base.py
│ │ ├── test_migrations.py
│ │ ├── test_link.py
│ │ ├── conftest.py
│ │ └── test_schemas.py
│ ├── README.md
│ ├── LICENSE
│ └── pyproject.toml
├── ribasim_api
│ ├── ribasim_api
│ │ ├── py.typed
│ │ ├── __init__.py
│ │ └── ribasim_api.py
│ ├── README.md
│ ├── pyproject.toml
│ ├── LICENSE
│ └── tests
│ │ └── conftest.py
└── ribasim_testmodels
│ ├── ribasim_testmodels
│ ├── py.typed
│ ├── trivial.py
│ ├── bucket.py
│ ├── backwater.py
│ └── two_basin.py
│ ├── README.md
│ ├── pyproject.toml
│ └── LICENSE
├── ribasim_qgis
├── core
│ ├── __init__.py
│ ├── geopackage.py
│ ├── arrow.py
│ └── model.py
├── tests
│ ├── ui
│ │ ├── __init__.py
│ │ └── test_load_plugin.py
│ ├── core
│ │ ├── __init__.py
│ │ └── test_model.py
│ ├── data
│ │ └── simple_valid.toml
│ └── __init__.py
├── icon.png
├── .coveragerc
├── resources.qrc
├── __init__.py
├── tomllib
│ ├── _types.py
│ └── __init__.py
├── scripts
│ ├── run_qgis_ui_tests.py
│ ├── enable_plugin.py
│ ├── install_ribasim_qgis.py
│ └── install_qgis_plugin.py
├── LICENSE
└── metadata.txt
├── open-vscode.bat
├── .dvc
├── .gitignore
└── config
├── data
└── .gitignore
├── .env.default
├── yamlfmt.yaml
├── .typos.toml
├── core
├── test
│ ├── aqua_test.jl
│ ├── data
│ │ ├── logging_test_no_loglevel.toml
│ │ ├── logging_test_loglevel_debug.toml
│ │ ├── config_test.toml
│ │ └── allocation_problems
│ │ │ ├── level_demand
│ │ │ └── allocation_problem_3.lp
│ │ │ ├── allocation_control
│ │ │ └── allocation_problem_1.lp
│ │ │ ├── drain_surplus
│ │ │ └── allocation_problem_2.lp
│ │ │ ├── linear_resistance_demand
│ │ │ └── allocation_problem_2.lp
│ │ │ ├── primary_and_secondary_subnetworks
│ │ │ └── allocation_problem_5.lp
│ │ │ ├── secondary_networks_with_sources
│ │ │ └── allocation_problem_5.lp
│ │ │ ├── small_primary_secondary_network
│ │ │ └── allocation_problem_2.lp
│ │ │ └── medium_primary_secondary_network
│ │ │ └── allocation_problem_2.lp
│ ├── runtests.jl
│ ├── main_test.jl
│ ├── libribasim_test.jl
│ └── carrays_test.jl
├── LICENSE
└── integration_test
│ └── hws_integration_test.jl
├── .dvcignore
├── codecov.yml
├── models
├── integration.toml
├── hws.dvc
├── benchmark.dvc
└── hws_migration_test.dvc
├── .teamcity
├── README
├── Testbench
│ ├── Testbench.kt
│ ├── IntegrationTestHWS.kt
│ └── RegressionTestODESolve.kt
├── Templates
│ ├── WindowsAgent.kt
│ ├── LinuxAgent.kt
│ ├── GithubIntegration.kt
│ ├── GenerateCache.kt
│ ├── Build.kt
│ └── TestDelwaqCoupling.kt
├── Ribasim
│ ├── vcsRoots
│ │ └── Ribasim.kt
│ └── buildTypes
│ │ ├── Ribasim_MakeQgisPlugin.kt
│ │ ├── GenerateCache.kt
│ │ └── GenerateTestmodels.kt
├── settings.kts
└── Ribasim_Linux
│ └── RibasimLinuxProject.kt
├── .devcontainer
├── dockerfile
└── devcontainer.json
├── .docker
├── compose.yml
└── test.sh
├── utils
├── unset-ssl-cert.bat
├── s3_settings.py
├── utils.jl
├── migrate_model.py
├── generate-sbom.jl
├── templates
│ ├── validation.py.jinja
│ └── schemas.py.jinja
├── s3_upload.py
├── generate-testmodels.py
├── update-manifest.jl
├── github-release.py
├── plot.jl
├── s3_download.py
├── testmodelrun.jl
└── write_allocation_problems.jl
├── ruff.toml
├── dvc.yaml
├── .github
├── workflows
│ ├── pre-commit_check.yml
│ ├── qgis.yml
│ ├── python_codegen.yml
│ ├── pre-commit_auto_update.yml
│ ├── mypy.yml
│ ├── julia_auto_update.yml
│ ├── pixi_auto_update.yml
│ ├── python_tests.yml
│ ├── core_testmodels.yml
│ ├── core_tests.yml
│ └── docs.yml
├── dependabot.yml
└── ISSUE_TEMPLATE
│ └── feature_request.md
├── .gitattributes
├── .JuliaFormatter.toml
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── dvc.lock
├── mypy.ini
├── .sonarcloud.properties
├── .pre-commit-config.yaml
├── LICENSE
└── README.md
/docs/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/_publish.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | ribasim.org
2 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ribasim_qgis/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ribasim_qgis/tests/ui/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ribasim_qgis/tests/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/open-vscode.bat:
--------------------------------------------------------------------------------
1 | pixi run code . | exit
2 |
--------------------------------------------------------------------------------
/python/ribasim_api/ribasim_api/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dvc/.gitignore:
--------------------------------------------------------------------------------
1 | /config.local
2 | /tmp
3 | /cache
4 |
--------------------------------------------------------------------------------
/python/ribasim_testmodels/ribasim_testmodels/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /predict.dat
2 | /integration.csv
3 | /integration.toml
4 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/delwaq/.gitignore:
--------------------------------------------------------------------------------
1 | model
2 | *.map
3 | *.nc
4 | *.pdf
5 | *.out
6 |
--------------------------------------------------------------------------------
/docs/assets/ribasim.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/Ribasim/main/docs/assets/ribasim.ico
--------------------------------------------------------------------------------
/ribasim_qgis/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/Ribasim/main/ribasim_qgis/icon.png
--------------------------------------------------------------------------------
/.env.default:
--------------------------------------------------------------------------------
1 | # Create a copy of this file as .env and fill in the values.
2 | MINIO_ACCESS_KEY=
3 | MINIO_SECRET_KEY=
4 |
--------------------------------------------------------------------------------
/ribasim_qgis/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = .
3 | omit =
4 | */resources.py
5 | */tests/*
6 | */tomllib/*
7 |
--------------------------------------------------------------------------------
/docs/contact.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Contact us"
3 | ---
4 |
5 | You can contact us [via email](mailto:ribasim.info@deltares.nl).
6 |
--------------------------------------------------------------------------------
/yamlfmt.yaml:
--------------------------------------------------------------------------------
1 | # https://github.com/google/yamlfmt/blob/main/docs/config-file.md
2 | gitignore_excludes: true
3 | line_ending: lf
4 |
--------------------------------------------------------------------------------
/.typos.toml:
--------------------------------------------------------------------------------
1 | [default.extend-words]
2 | # Ignore false-positives
3 | Missings = "Missings"
4 | stichting = "stichting"
5 | typ = "typ"
6 |
--------------------------------------------------------------------------------
/core/test/aqua_test.jl:
--------------------------------------------------------------------------------
1 | @testitem "Aqua" begin
2 | import Aqua
3 | Aqua.test_all(Ribasim; ambiguities = false, persistent_tasks = false)
4 | end
5 |
--------------------------------------------------------------------------------
/ribasim_qgis/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | icon.png
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/reference/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Reference"
3 | ---
4 |
5 | These pages contain the test models, node descriptions and Ribasim Python API documentation.
6 |
--------------------------------------------------------------------------------
/python/ribasim_api/ribasim_api/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "2026.1.0-rc1"
2 |
3 | from ribasim_api.ribasim_api import RibasimApi
4 |
5 | __all__ = ["RibasimApi"]
6 |
--------------------------------------------------------------------------------
/.dvcignore:
--------------------------------------------------------------------------------
1 | # Add patterns of files dvc should ignore, which could improve
2 | # the performance. Learn more at
3 | # https://dvc.org/doc/user-guide/dvcignore
4 | docs
5 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | patch:
4 | default:
5 | informational: true
6 | project:
7 | default:
8 | informational: true
9 |
--------------------------------------------------------------------------------
/models/integration.toml:
--------------------------------------------------------------------------------
1 | starttime = 2023-01-20 00:00:00
2 | endtime = 2023-07-01 00:00:00
3 | crs = "EPSG:28992"
4 | input_dir = "hws"
5 | results_dir = "hws/results"
6 | ribasim_version = "2025.3.0"
7 |
--------------------------------------------------------------------------------
/core/test/data/logging_test_no_loglevel.toml:
--------------------------------------------------------------------------------
1 | starttime = 2019-01-01
2 | endtime = 2019-12-31
3 | crs = "EPSG:28992"
4 | input_dir = "input"
5 | results_dir = "results"
6 | ribasim_version = "2026.1.0-rc1"
7 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /.quarto/
2 | /_site/
3 | /site_libs/
4 | /reference/python/
5 | /getting-started/tutorial/crystal-basin/
6 | /getting-started/data/
7 | *.html
8 | objects.json
9 |
10 | **/*.quarto_ipynb
11 |
--------------------------------------------------------------------------------
/ribasim_qgis/tests/data/simple_valid.toml:
--------------------------------------------------------------------------------
1 | starttime = 2020-01-01 00:00:00
2 | endtime = 2021-01-01 00:00:00
3 | crs = "EPSG:28992"
4 | input_dir = "."
5 | results_dir = "results"
6 | ribasim_version = "2026.1.0-rc1"
7 |
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/include-code-files/_extension.yml:
--------------------------------------------------------------------------------
1 | title: Include Code Files
2 | author: Bruno Beaufils
3 | version: 1.0.0
4 | quarto-required: ">=1.2"
5 | contributes:
6 | filters:
7 | - include-code-files.lua
8 |
--------------------------------------------------------------------------------
/docs/assets/styles.css:
--------------------------------------------------------------------------------
1 | /* css styles */
2 |
3 | /* freeze first column of a table */
4 | .freeze-left th:first-child,
5 | .freeze-left td:first-child {
6 | position: sticky;
7 | left: 0;
8 | background: white;
9 | z-index: 1;
10 | }
11 |
--------------------------------------------------------------------------------
/core/test/data/logging_test_loglevel_debug.toml:
--------------------------------------------------------------------------------
1 | starttime = 2019-01-01
2 | endtime = 2019-12-31
3 | crs = "EPSG:28992"
4 | input_dir = "input"
5 | results_dir = "results"
6 | ribasim_version = "2026.1.0-rc1"
7 |
8 | [logging]
9 | verbosity = "debug"
10 |
--------------------------------------------------------------------------------
/ribasim_qgis/__init__.py:
--------------------------------------------------------------------------------
1 | """A script which initializes the plugin, making it known to QGIS."""
2 |
3 |
4 | def classFactory(iface): # pylint: disable=invalid-name
5 | from ribasim_qgis.ribasim_qgis import RibasimPlugin
6 |
7 | return RibasimPlugin(iface)
8 |
--------------------------------------------------------------------------------
/.teamcity/README:
--------------------------------------------------------------------------------
1 | The archive contains settings for a TeamCity project.
2 |
3 | To edit the settings in IntelliJ Idea, open the pom.xml and
4 | select the 'Open as a project' option.
5 |
6 | If you want to move this dsl to version control, save it in the
7 | .teamcity directory.
--------------------------------------------------------------------------------
/.devcontainer/dockerfile:
--------------------------------------------------------------------------------
1 | FROM julia:latest
2 | ARG PIXI_VERSION=v0.49.0
3 |
4 | RUN curl -fsSL https://pixi.sh/install.sh | bash -s -- ${PIXI_VERSION}
5 | ENV PATH="/root/.pixi/bin:${PATH}"
6 | RUN apt-get update && \
7 | apt-get upgrade -y && \
8 | apt-get install -y git
9 |
--------------------------------------------------------------------------------
/core/test/data/config_test.toml:
--------------------------------------------------------------------------------
1 | starttime = 2019-01-01
2 | endtime = 2019-12-31
3 | crs = "EPSG:28992"
4 | input_dir = "input"
5 | results_dir = "results"
6 | ribasim_version = "2026.1.0-rc1"
7 |
8 | [basin]
9 | time = "basin/time.arrow"
10 |
11 | [solver]
12 | saveat = 3600
13 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/linear_resistance.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | LinearResistanceStaticSchema,
4 | )
5 |
6 | __all__ = ["Static"]
7 |
8 |
9 | class Static(TableModel[LinearResistanceStaticSchema]):
10 | pass
11 |
--------------------------------------------------------------------------------
/.docker/compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | qgis:
4 | image: qgis/qgis:release-3_34
5 | container_name: qgis
6 | volumes:
7 | - ../ribasim_qgis/:/tests_directory/ribasim_qgis
8 | environment:
9 | - CI=true
10 | - DISPLAY=:99
11 | tty: true
12 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/manning_resistance.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | ManningResistanceStaticSchema,
4 | )
5 |
6 | __all__ = ["Static"]
7 |
8 |
9 | class Static(TableModel[ManningResistanceStaticSchema]):
10 | pass
11 |
--------------------------------------------------------------------------------
/utils/unset-ssl-cert.bat:
--------------------------------------------------------------------------------
1 | rem Workaround a conflict between conda openssl activation and julia,
2 | rem until these two PRs are released:
3 | rem https://github.com/JuliaLang/NetworkOptions.jl/pull/37
4 | rem https://github.com/JuliaLang/julia/pull/56924
5 | set SSL_CERT_FILE=
6 | set SSL_CERT_DIR=
7 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/geometry/__init__.py:
--------------------------------------------------------------------------------
1 | from ribasim.geometry.area import BasinAreaSchema, FlowBoundaryAreaSchema
2 | from ribasim.geometry.link import LinkTable
3 | from ribasim.geometry.node import NodeTable
4 |
5 | __all__ = ["BasinAreaSchema", "FlowBoundaryAreaSchema", "LinkTable", "NodeTable"]
6 |
--------------------------------------------------------------------------------
/python/ribasim_testmodels/README.md:
--------------------------------------------------------------------------------
1 | # Ribasim Testmodels
2 |
3 | `ribasim_testmodels` can generate model files with data that can be run by Ribasim.
4 | The test models are used for internal testing of Ribasim.
5 |
6 | # Contributing
7 |
8 | For the developer docs please have a look at https://ribasim.org/dev/
9 |
--------------------------------------------------------------------------------
/utils/s3_settings.py:
--------------------------------------------------------------------------------
1 | from pydantic_settings import BaseSettings, SettingsConfigDict
2 |
3 |
4 | class Settings(BaseSettings):
5 | minio_access_key: str = ""
6 | minio_secret_key: str = ""
7 |
8 | model_config = SettingsConfigDict(env_file=(".env"))
9 |
10 |
11 | settings = Settings()
12 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/pump.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import PumpStaticSchema, PumpTimeSchema
3 |
4 | __all__ = ["Static", "Time"]
5 |
6 |
7 | class Static(TableModel[PumpStaticSchema]):
8 | pass
9 |
10 |
11 | class Time(TableModel[PumpTimeSchema]):
12 | pass
13 |
--------------------------------------------------------------------------------
/.dvc/config:
--------------------------------------------------------------------------------
1 | [core]
2 | remote = minio
3 | ['remote "minio"'] # for tracking artifacts with unreadable names
4 | url = s3://ribasim/dvc
5 | endpointurl = https://s3.deltares.nl
6 | ['remote "minio_readonly"'] # for readable named models in the top-dir
7 | url = s3://ribasim
8 | endpointurl = https://s3.deltares.nl
9 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/outlet.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import OutletStaticSchema, OutletTimeSchema
3 |
4 | __all__ = ["Static", "Time"]
5 |
6 |
7 | class Static(TableModel[OutletStaticSchema]):
8 | pass
9 |
10 |
11 | class Time(TableModel[OutletTimeSchema]):
12 | pass
13 |
--------------------------------------------------------------------------------
/utils/utils.jl:
--------------------------------------------------------------------------------
1 | # Shared utility functions that are not part of the Ribasim core
2 |
3 | using Glob: glob
4 |
5 | "Retrieve the names of all test models"
6 | function get_testmodels()::Vector{String}
7 | models_dir = normpath(@__DIR__, "..", "generated_testmodels")
8 | toml_paths = sort(glob("**/ribasim.toml", models_dir))
9 | end
10 |
--------------------------------------------------------------------------------
/ruff.toml:
--------------------------------------------------------------------------------
1 | exclude = ["ribasim_qgis/tomllib/*"]
2 | target-version = "py312"
3 |
4 | [lint]
5 | # See https://docs.astral.sh/ruff/rules/
6 | select = ["C4", "D2", "D3", "D4", "E", "F", "I", "NPY", "PD", "UP", "RUF"]
7 | ignore = [
8 | "E501",
9 | "PD002",
10 | ]
11 | fixable = ["ALL"]
12 |
13 | [lint.pydocstyle]
14 | convention = "numpy"
15 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/pid_control.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import PidControlStaticSchema, PidControlTimeSchema
3 |
4 | __all__ = ["Static", "Time"]
5 |
6 |
7 | class Static(TableModel[PidControlStaticSchema]):
8 | pass
9 |
10 |
11 | class Time(TableModel[PidControlTimeSchema]):
12 | pass
13 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/delwaq/__init__.py:
--------------------------------------------------------------------------------
1 | from .generate import add_tracer, generate
2 | from .parse import parse
3 | from .plot import plot_fraction, plot_spatial
4 | from .util import run_delwaq
5 |
6 | __all__ = [
7 | "add_tracer",
8 | "generate",
9 | "parse",
10 | "plot_fraction",
11 | "plot_spatial",
12 | "run_delwaq",
13 | ]
14 |
--------------------------------------------------------------------------------
/models/hws.dvc:
--------------------------------------------------------------------------------
1 | md5: ac8f106c844227171975b9d0417f33a6
2 | frozen: true
3 | deps:
4 | - md5: a5de3ddf9da718b7d431dbce2b09deea.dir
5 | size: 42621406
6 | nfiles: 3
7 | hash: md5
8 | path: remote://minio_readonly/hws_2025_4_0
9 | outs:
10 | - md5: ba7c476e3d8b0759c73b838693b9e54e.dir
11 | size: 42621406
12 | nfiles: 3
13 | hash: md5
14 | path: hws
15 |
--------------------------------------------------------------------------------
/models/benchmark.dvc:
--------------------------------------------------------------------------------
1 | md5: 2fac758278d29071bb3e4632d69cbb0c
2 | frozen: true
3 | deps:
4 | - md5: 8620613cb3fc380a20bcbbb2bd36b28c.dir
5 | size: 187932
6 | nfiles: 6
7 | hash: md5
8 | path: remote://minio_readonly/benchmark
9 | outs:
10 | - md5: ceb5e5e81c402510497da43d29dce099.dir
11 | size: 187932
12 | nfiles: 6
13 | hash: md5
14 | path: benchmark
15 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/delwaq/template/B8_initials.inc.j2:
--------------------------------------------------------------------------------
1 | ; Generated by Ribasim
2 | ; Hard coded initials for mass balance check.
3 | MASS/M2 ; The bed substances are specified in mass/m2
4 | 1 ; Input in this file
5 | 1 ; Input without defaults
6 | {{ substances | length }}*1.0 ; Scale value
7 |
8 | ; "{{ substances | join('" "') }}"
9 | {{ initial_concentrations }}
10 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/flow_demand.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | FlowDemandStaticSchema,
4 | FlowDemandTimeSchema,
5 | )
6 |
7 | __all__ = ["Static", "Time"]
8 |
9 |
10 | class Static(TableModel[FlowDemandStaticSchema]):
11 | pass
12 |
13 |
14 | class Time(TableModel[FlowDemandTimeSchema]):
15 | pass
16 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/level_demand.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | LevelDemandStaticSchema,
4 | LevelDemandTimeSchema,
5 | )
6 |
7 | __all__ = ["Static", "Time"]
8 |
9 |
10 | class Static(TableModel[LevelDemandStaticSchema]):
11 | pass
12 |
13 |
14 | class Time(TableModel[LevelDemandTimeSchema]):
15 | pass
16 |
--------------------------------------------------------------------------------
/models/hws_migration_test.dvc:
--------------------------------------------------------------------------------
1 | md5: bda2339cc165379b0f6da21aa67039d9
2 | frozen: true
3 | deps:
4 | - md5: ab64e9bee55f9a7524f36ca317ade8fb.dir
5 | size: 43307158
6 | nfiles: 2
7 | hash: md5
8 | path: remote://minio_readonly/hws_migration_test
9 | outs:
10 | - md5: 6ca6218661d0f021bc53bf468000a62b.dir
11 | size: 43307158
12 | nfiles: 2
13 | hash: md5
14 | path: hws_migration_test
15 |
--------------------------------------------------------------------------------
/dvc.yaml:
--------------------------------------------------------------------------------
1 | stages:
2 | integration:
3 | cmd: pixi run model-integration-test
4 | deps:
5 | - core/
6 | - models/hws
7 | params:
8 | - models/integration.toml:
9 | - solver.algorithm
10 | - solver.abstol
11 | - solver.reltol
12 | - solver.autodiff
13 | outs:
14 | - data/integration.toml
15 | metrics:
16 | - data/integration.toml
17 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit_check.yml:
--------------------------------------------------------------------------------
1 | name: Pre-commit
2 | on:
3 | pull_request:
4 | merge_group:
5 | push:
6 | branches: [main]
7 | workflow_dispatch:
8 | jobs:
9 | check:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v6
13 | - uses: actions/setup-python@v6
14 | with:
15 | python-version: "3.12"
16 | - uses: pre-commit/action@v3.0.1
17 |
--------------------------------------------------------------------------------
/.teamcity/Testbench/Testbench.kt:
--------------------------------------------------------------------------------
1 | package Testbench
2 |
3 | import Templates.*
4 | import Testbench.IntegrationTestHWS.IntegrationTestHWS
5 | import Testbench.RegressionTestODESolve.RegressionTestODESolve
6 | import jetbrains.buildServer.configs.kotlin.Project
7 |
8 | object Testbench : Project({
9 | name = "Testbench"
10 | subProject(IntegrationTestHWS)
11 | subProject(RegressionTestODESolve)
12 | })
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
2 | version: 2
3 | enable-beta-ecosystems: true
4 | updates:
5 | - package-ecosystem: "github-actions"
6 | directory: "/"
7 | schedule:
8 | interval: "weekly"
9 | - package-ecosystem: "julia"
10 | directories:
11 | - "/core"
12 | schedule:
13 | interval: "weekly"
14 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/tabulated_rating_curve.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | TabulatedRatingCurveStaticSchema,
4 | TabulatedRatingCurveTimeSchema,
5 | )
6 |
7 | __all__ = ["Static", "Time"]
8 |
9 |
10 | class Static(TableModel[TabulatedRatingCurveStaticSchema]):
11 | pass
12 |
13 |
14 | class Time(TableModel[TabulatedRatingCurveTimeSchema]):
15 | pass
16 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/continuous_control.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | ContinuousControlFunctionSchema,
4 | ContinuousControlVariableSchema,
5 | )
6 |
7 | __all__ = ["Function", "Variable"]
8 |
9 |
10 | class Variable(TableModel[ContinuousControlVariableSchema]):
11 | pass
12 |
13 |
14 | class Function(TableModel[ContinuousControlFunctionSchema]):
15 | pass
16 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
3 | # SCM syntax highlighting & preventing 3-way merges
4 | pixi.lock merge=binary linguist-language=YAML linguist-generated=true
5 |
6 | *.lp linguist-generated=true
7 | Manifest.toml linguist-generated=true
8 | Ribasim.spdx.json merge=binary linguist-generated=true
9 | Ribasim-python.spdx.json merge=binary linguist-generated=true
10 | python/ribasim/ribasim/styles/*.qml merge=binary linguist-generated=true
11 |
--------------------------------------------------------------------------------
/ribasim_qgis/tomllib/_types.py:
--------------------------------------------------------------------------------
1 | # Copied from https://github.com/python/cpython/blob/v3.12.0/Lib/tomllib/_types.py
2 | # QGIS does not guarantee a toml reader
3 |
4 | # SPDX-License-Identifier: MIT
5 | # SPDX-FileCopyrightText: 2021 Taneli Hukkinen
6 | # Licensed to PSF under a Contributor Agreement.
7 |
8 | from typing import Any, Callable, Tuple
9 |
10 | # Type annotations
11 | ParseFloat = Callable[[str], Any]
12 | Key = Tuple[str, ...]
13 | Pos = int
14 |
--------------------------------------------------------------------------------
/.teamcity/Templates/WindowsAgent.kt:
--------------------------------------------------------------------------------
1 | package Templates
2 |
3 | import jetbrains.buildServer.configs.kotlin.*
4 |
5 | object WindowsAgent : Template({
6 | id("Ribasim_Windows")
7 | name = "Ribasim_Windows"
8 | description = "Template for agent that uses Windows OS"
9 |
10 | params {
11 | param("env.JULIA_NUM_THREADS", "4")
12 | }
13 |
14 | requirements {
15 | contains("teamcity.agent.jvm.os.name", "Windows", "RQ_422")
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/docs/reference/node/terminal.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Terminal"
3 | ---
4 |
5 | A Terminal is a water sink without state or properties.
6 | Any water that flows into a Terminal node is removed from the model.
7 | No water can come into the model from a Terminal node.
8 | For example, Terminal nodes can be used as a downstream boundary.
9 |
10 | # Tables
11 |
12 | No tables are required for Terminal nodes.
13 |
14 | # Equations
15 |
16 | The incoming node determines the flow into the Terminal node.
17 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/user_demand.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | UserDemandConcentrationSchema,
4 | UserDemandStaticSchema,
5 | UserDemandTimeSchema,
6 | )
7 |
8 | __all__ = ["Static", "Time"]
9 |
10 |
11 | class Static(TableModel[UserDemandStaticSchema]):
12 | pass
13 |
14 |
15 | class Time(TableModel[UserDemandTimeSchema]):
16 | pass
17 |
18 |
19 | class Concentration(TableModel[UserDemandConcentrationSchema]):
20 | pass
21 |
--------------------------------------------------------------------------------
/ribasim_qgis/tomllib/__init__.py:
--------------------------------------------------------------------------------
1 | # Copied from https://github.com/python/cpython/blob/v3.12.0/Lib/tomllib/__init__.py
2 | # QGIS does not guarantee a toml reader
3 |
4 | # SPDX-License-Identifier: MIT
5 | # SPDX-FileCopyrightText: 2021 Taneli Hukkinen
6 | # Licensed to PSF under a Contributor Agreement.
7 |
8 | __all__ = ("loads", "load", "TOMLDecodeError")
9 |
10 | from ._parser import TOMLDecodeError, load, loads
11 |
12 | # Pretend this exception was created here.
13 | TOMLDecodeError.__module__ = __name__
14 |
--------------------------------------------------------------------------------
/.JuliaFormatter.toml:
--------------------------------------------------------------------------------
1 | # Options for the JuliaFormatter auto syntax formatting tool.
2 | # https://domluna.github.io/JuliaFormatter.jl/stable/
3 | # https://docs.sciml.ai/SciMLStyle/stable/
4 |
5 | # Based on the default style we do pick these non-default options from SciML style:
6 | whitespace_ops_in_indices = true
7 | remove_extra_newlines = true
8 | always_for_in = true
9 | whitespace_typedefs = true
10 |
11 | # And add other options we like:
12 | separate_kwargs_with_semicolon = true
13 |
14 | ignore = [".pixi"]
15 |
--------------------------------------------------------------------------------
/.teamcity/Templates/LinuxAgent.kt:
--------------------------------------------------------------------------------
1 | package Templates
2 |
3 | import jetbrains.buildServer.configs.kotlin.*
4 |
5 | object LinuxAgent : Template({
6 | id("Ribasim_Linux")
7 | name = "Ribasim_Linux"
8 | description = "Template for agent that uses Linux OS"
9 |
10 | publishArtifacts = PublishMode.SUCCESSFUL
11 |
12 | params {
13 | param("env.JULIA_NUM_THREADS", "8")
14 | }
15 |
16 | requirements {
17 | equals("teamcity.agent.jvm.os.name", "Linux", "RQ_418")
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "charliermarsh.ruff",
4 | "julialang.language-julia",
5 | "ms-python.mypy-type-checker",
6 | "ms-python.python",
7 | "ms-toolsai.jupyter",
8 | "njpwerner.autodocstring",
9 | "quarto.quarto",
10 | "renan-r-santos.pixi-code",
11 | "samuelcolvin.jinjahtml",
12 | "streetsidesoftware.code-spell-checker",
13 | "tamasfe.even-better-toml",
14 | "yy0931.vscode-sqlite3-editor",
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/docs/dev/copilot.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Copilot instructions"
3 | ---
4 |
5 | The developers sometimes use GitHub Copilot to assist in their work.
6 | To make this more effective, we supply custom instructions for this repository, as documented [here](https://docs.github.com/en/copilot/how-tos/custom-instructions/adding-repository-custom-instructions-for-github-copilot).
7 | These instructions are automatically used by Copilot.
8 |
9 | # Current repository Copilot instructions
10 |
11 | {{< include ../../.github/copilot-instructions.md >}}
12 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/level_boundary.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | LevelBoundaryConcentrationSchema,
4 | LevelBoundaryStaticSchema,
5 | LevelBoundaryTimeSchema,
6 | )
7 |
8 | __all__ = ["Concentration", "Static", "Time"]
9 |
10 |
11 | class Static(TableModel[LevelBoundaryStaticSchema]):
12 | pass
13 |
14 |
15 | class Time(TableModel[LevelBoundaryTimeSchema]):
16 | pass
17 |
18 |
19 | class Concentration(TableModel[LevelBoundaryConcentrationSchema]):
20 | pass
21 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/discrete_control.py:
--------------------------------------------------------------------------------
1 | from ribasim.input_base import TableModel
2 | from ribasim.schemas import (
3 | DiscreteControlConditionSchema,
4 | DiscreteControlLogicSchema,
5 | DiscreteControlVariableSchema,
6 | )
7 |
8 | __all__ = ["Condition", "Logic", "Variable"]
9 |
10 |
11 | class Variable(TableModel[DiscreteControlVariableSchema]):
12 | pass
13 |
14 |
15 | class Condition(TableModel[DiscreteControlConditionSchema]):
16 | pass
17 |
18 |
19 | class Logic(TableModel[DiscreteControlLogicSchema]):
20 | pass
21 |
--------------------------------------------------------------------------------
/core/test/runtests.jl:
--------------------------------------------------------------------------------
1 | using TestItemRunner
2 |
3 | include("utils.jl")
4 |
5 | function test_type(item)::Bool
6 | dir = basename(dirname(item.filename))
7 | is_integration = dir == "integration_test"
8 | is_regression = dir == "regression_test"
9 | if in("integration", ARGS)
10 | is_integration
11 | elseif in("regression", ARGS)
12 | is_regression
13 | elseif in("skip", ARGS)
14 | false
15 | else
16 | !is_integration && !is_regression
17 | end
18 | end
19 |
20 | @run_package_tests filter = test_type
21 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/styles/README.md:
--------------------------------------------------------------------------------
1 | # Ribasim QGIS Styles
2 |
3 | Ribasim Python always adds layer styles to the GeoPackage, such that the node types can be recognized also without the Ribasim QGIS plugin installed.
4 |
5 | One can update the Ribasim styles in QGIS itself, saving the style to the GeoPackage or disk, and retrieve it from there.
6 | See https://github.com/Deltares/Ribasim/issues/610#issuecomment-1729511312.
7 |
8 | Here we store the Node, Basin / area and Link styles, saved in the QML format, from the `styleQML` attribute in the `layer_styles` in the GeoPackage.
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: New issue template
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **What**
11 | A clear and concise description of what the problem is. Ex. A user wants to [...]
12 |
13 | **Why**
14 | A clear and concise description of why this feature is needed and its context.
15 |
16 | **How**
17 | A clear and concise description of how a technical implementation in code should look like.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.docker/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eux
3 |
4 | docker compose -f compose.yml up -d --force-recreate --remove-orphans
5 | echo "Installation of the plugin Ribasim"
6 | docker exec -t qgis sh -c "qgis_setup.sh ribasim_qgis"
7 | echo "Containers are running"
8 |
9 | docker exec -t qgis sh -c "python3 -m pip install pandas"
10 | docker exec -t qgis sh -c "cd /tests_directory && xvfb-run -a qgis_testrunner.sh ribasim_qgis.tests"
11 | exit_code=$?
12 |
13 | echo 'Stopping/killing containers'
14 | docker compose -f compose.yml kill
15 | docker compose -f compose.yml rm -f
16 |
17 | exit $exit_code
18 |
--------------------------------------------------------------------------------
/docs/known_issues.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Known issues"
3 | ---
4 |
5 | Known issues can be found on the [GitHub issues page](https://github.com/Deltares/Ribasim/issues).
6 | Besides the issues that need to be fixed, there are also considerations that had to be made while developing the application.
7 |
8 | # QGIS plugin
9 |
10 | - Input tables that are in Arrow files are currently not automatically loaded in QGIS ([#318](https://github.com/Deltares/Ribasim/issues/318)).
11 | - Results in NetCDF format are currently not automatically loaded in QGIS ([#2519](https://github.com/Deltares/Ribasim/issues/2519)).
12 |
--------------------------------------------------------------------------------
/ribasim_qgis/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pathlib import Path
3 |
4 | import coverage
5 | from qgis.testing import unittest
6 |
7 | testfolder = Path(__file__).parent
8 |
9 |
10 | def run_all():
11 | test_loader = unittest.defaultTestLoader
12 | test_suite = test_loader.discover(".", pattern="test_*.py")
13 |
14 | cov = coverage.Coverage(config_file=testfolder.parent / ".coveragerc")
15 | cov.start()
16 | unittest.TextTestRunner(verbosity=3, stream=sys.stdout).run(test_suite)
17 |
18 | cov.stop()
19 | cov.save()
20 | cov.xml_report(outfile=testfolder / "coverage.xml")
21 |
--------------------------------------------------------------------------------
/.teamcity/Ribasim/vcsRoots/Ribasim.kt:
--------------------------------------------------------------------------------
1 | package Ribasim.vcsRoots
2 |
3 | import jetbrains.buildServer.configs.kotlin.vcs.GitVcsRoot
4 |
5 | object Ribasim : GitVcsRoot({
6 | name = "Ribasim"
7 | url = "https://github.com/Deltares/Ribasim"
8 | branch = "main"
9 | branchSpec = """
10 | +:refs/heads/main
11 | +:refs/tags/*
12 | +:refs/heads/gh-readonly-queue/*
13 | """.trimIndent()
14 | useTagsAsBranches = true
15 | authMethod = password {
16 | userName = "teamcity-deltares"
17 | password = "credentialsJSON:abf605ce-e382-4b10-b5de-8a7640dc58d9"
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/python/ribasim_api/README.md:
--------------------------------------------------------------------------------
1 | # Ribasim API
2 |
3 | `ribasim_api` is an extension to [xmipy](https://pypi.org/project/xmipy/) for the Ribasim API.
4 |
5 | The `ribasim_api` can be used to access functionality in the eXtended Model Interface (XMI) wrapper (XmiWrapper)
6 | and additional functionality specific to the Ribasim API.
7 |
8 |
9 | `ribasim_api` can be installed by running
10 |
11 | ```
12 | pip install ribasim-api
13 | ```
14 |
15 | or
16 |
17 | ```
18 | conda install -c conda-forge ribasim-api
19 | ```
20 |
21 | # Contributing
22 |
23 | For the developer docs please have a look at https://ribasim.org/dev/.
24 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "2026.1.0-rc1"
2 | __schema_version__ = 10
3 |
4 | from pyogrio import set_gdal_config_options
5 |
6 | from ribasim.cli import run_ribasim
7 | from ribasim.config import Allocation, Logging, Node, Solver
8 | from ribasim.db_utils import fake_date
9 | from ribasim.geometry.link import LinkTable
10 | from ribasim.model import Model
11 |
12 | set_gdal_config_options(
13 | {
14 | "OGR_CURRENT_DATE": fake_date, # %Y-%m-%dT%H:%M:%fZ
15 | }
16 | )
17 |
18 | __all__ = [
19 | "Allocation",
20 | "LinkTable",
21 | "Logging",
22 | "Model",
23 | "Node",
24 | "Solver",
25 | "run_ribasim",
26 | ]
27 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/flow_boundary.py:
--------------------------------------------------------------------------------
1 | from ribasim.geometry import FlowBoundaryAreaSchema
2 | from ribasim.input_base import SpatialTableModel, TableModel
3 | from ribasim.schemas import (
4 | FlowBoundaryConcentrationSchema,
5 | FlowBoundaryStaticSchema,
6 | FlowBoundaryTimeSchema,
7 | )
8 |
9 | __all__ = ["Concentration", "Static", "Time"]
10 |
11 |
12 | class Static(TableModel[FlowBoundaryStaticSchema]):
13 | pass
14 |
15 |
16 | class Time(TableModel[FlowBoundaryTimeSchema]):
17 | pass
18 |
19 |
20 | class Concentration(TableModel[FlowBoundaryConcentrationSchema]):
21 | pass
22 |
23 |
24 | class Area(SpatialTableModel[FlowBoundaryAreaSchema]):
25 | pass
26 |
--------------------------------------------------------------------------------
/utils/migrate_model.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from pathlib import Path
3 |
4 | from ribasim import Model
5 |
6 | parser = argparse.ArgumentParser(description="Migrate Ribasim model.")
7 | parser.add_argument("input_toml", help="Path to input TOML file")
8 | parser.add_argument(
9 | "output_toml",
10 | nargs="?",
11 | help="Path to output TOML file (defaults to input_toml if not provided)",
12 | )
13 | args = parser.parse_args()
14 |
15 | input_path = Path(args.input_toml)
16 | # If no output path is provided, use the input path (in-place modification)
17 | output_path = Path(args.output_toml) if args.output_toml else input_path
18 |
19 | model = Model.read(input_path)
20 | model.write(output_path)
21 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/delwaq/template/delwaq.atr.j2:
--------------------------------------------------------------------------------
1 | ; Generated by Ribasim
2 | ; DELWAQ_COMPLETE_ATTRIBUTES
3 | 2 ; two blocks with input
4 | 1 ; number of attributes, they are :
5 | 1 ; '1' is active '0' is not
6 | 1 ; data follows in this file
7 | 1 ; all data is given without defaults
8 | ; layer: 1
9 | {{ nsegments }}*1
10 | 1 ; number of attributes, they are :
11 | 2 ; '1' has surface '3' has bottom
12 | ; '0' has both '2' has none
13 | 1 ; data follows in this file
14 | 1 ; all data is given without defaults
15 | ; layer: 1
16 | {{ nsegments }}*0
17 | 0 ; no time dependent attributes
18 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/__init__.py:
--------------------------------------------------------------------------------
1 | from ribasim.nodes import (
2 | basin,
3 | continuous_control,
4 | discrete_control,
5 | flow_boundary,
6 | level_boundary,
7 | level_demand,
8 | linear_resistance,
9 | manning_resistance,
10 | outlet,
11 | pid_control,
12 | pump,
13 | tabulated_rating_curve,
14 | user_demand,
15 | )
16 |
17 | __all__ = [
18 | "basin",
19 | "continuous_control",
20 | "discrete_control",
21 | "flow_boundary",
22 | "level_boundary",
23 | "level_demand",
24 | "linear_resistance",
25 | "manning_resistance",
26 | "outlet",
27 | "pid_control",
28 | "pump",
29 | "tabulated_rating_curve",
30 | "user_demand",
31 | ]
32 |
--------------------------------------------------------------------------------
/dvc.lock:
--------------------------------------------------------------------------------
1 | schema: '2.0'
2 | stages:
3 | integration:
4 | cmd: pixi run model-integration-test
5 | deps:
6 | - path: core/
7 | hash: md5
8 | md5: 6228f56acef895eeb5d4fdcd479cd1ee.dir
9 | size: 474129
10 | nfiles: 45
11 | - path: models/hws_2025_4_0
12 | hash: md5
13 | md5: e88123c69a0f7e3590b94c55dbe0062e.dir
14 | size: 45421022
15 | nfiles: 10
16 | params:
17 | models/integration.toml:
18 | solver.abstol: 1e-07
19 | solver.algorithm: QNDF
20 | solver.autodiff: true
21 | solver.reltol: 1e-07
22 | outs:
23 | - path: data/integration.toml
24 | hash: md5
25 | md5: aa6ff2820a6eda91df1073d3bc41755e
26 | size: 109
27 |
--------------------------------------------------------------------------------
/ribasim_qgis/scripts/run_qgis_ui_tests.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 | qgis_process = subprocess.run(
4 | [
5 | "qgis",
6 | "--profiles-path",
7 | ".pixi/qgis_env",
8 | "--version-migration",
9 | "--nologo",
10 | "--code",
11 | "ribasim_qgis/scripts/qgis_testrunner.py",
12 | "ribasim_qgis.tests",
13 | ],
14 | stdout=subprocess.PIPE,
15 | stderr=subprocess.STDOUT,
16 | text=True,
17 | )
18 |
19 | print(qgis_process.stdout)
20 | qgis_process.check_returncode()
21 |
22 | # QGIS always finishes with exit code 0, even when tests fail, so we have to check the output
23 | if any(s in qgis_process.stdout for s in ["QGIS died on signal", "FAILED"]):
24 | exit(1)
25 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | plugins = pydantic.mypy, numpy.typing.mypy_plugin, pandera.mypy
3 | ignore_missing_imports = True
4 |
5 | warn_unused_configs = True
6 | warn_redundant_casts = True
7 | warn_unused_ignores = True
8 | strict_equality = True
9 | extra_checks = True
10 | disallow_subclassing_any = True
11 | disallow_untyped_decorators = True
12 | disallow_any_generics = True
13 | mypy_path = .pixi/envs/$PIXI_ENVIRONMENT_NAME/Library/python,.pixi/envs/$PIXI_ENVIRONMENT_NAME/share/qgis/python
14 |
15 | # Ignore errors for imported packages.
16 | [mypy-console.*]
17 | ignore_errors = True
18 |
19 | [mypy-qgis.*]
20 | ignore_errors = True
21 |
22 | [mypy-plugins.*]
23 | ignore_errors = True
24 |
25 | [mypy-ribasim_qgis.tomllib.*]
26 | ignore_errors = True
27 |
--------------------------------------------------------------------------------
/python/ribasim/tests/test_input_base.py:
--------------------------------------------------------------------------------
1 | from ribasim import geometry, nodes
2 | from ribasim.input_base import TableModel
3 | from ribasim.schemas import BasinSubgridSchema
4 |
5 |
6 | def test_tablemodel_schema():
7 | schema = TableModel[BasinSubgridSchema].tableschema()
8 | assert schema == BasinSubgridSchema
9 |
10 |
11 | def test_tablename():
12 | cls = nodes.tabulated_rating_curve.Static
13 | assert cls.tablename() == "TabulatedRatingCurve / static"
14 |
15 | cls = nodes.basin.ConcentrationExternal
16 | assert cls.tablename() == "Basin / concentration_external"
17 |
18 | cls = geometry.NodeTable
19 | assert cls.tablename() == "Node"
20 |
21 | cls = geometry.LinkTable
22 | assert cls.tablename() == "Link"
23 |
--------------------------------------------------------------------------------
/.github/workflows/qgis.yml:
--------------------------------------------------------------------------------
1 | name: QGIS Tests
2 | on:
3 | push:
4 | branches: [main]
5 | paths: ["ribasim_qgis/**", "pixi.toml", "pixi.lock"]
6 | tags: ["*"]
7 | pull_request:
8 | paths: ["ribasim_qgis/**", "pixi.toml", "pixi.lock"]
9 | merge_group:
10 | workflow_dispatch:
11 | jobs:
12 | test-qgis:
13 | name: "Test"
14 | runs-on: ubuntu-latest
15 | defaults:
16 | run:
17 | working-directory: .docker
18 | steps:
19 | - uses: actions/checkout@v6
20 | - uses: prefix-dev/setup-pixi@v0.9.3
21 | with:
22 | pixi-version: "latest"
23 | - name: Run tests
24 | run: pixi run test-ribasim-qgis-docker
25 | - name: Upload coverage to Codecov
26 | uses: codecov/codecov-action@v5
27 | with:
28 | token: ${{ secrets.CODECOV_TOKEN }}
29 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/delwaq/reference/dimr_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.1
5 | Deltares, Coupling team
6 | 2016-01-20T07:56:32+01:00
7 |
8 |
9 |
10 |
11 |
12 |
13 | delwaq
14 | .
15 |
16 | delwaq.inp
17 |
18 |
19 |
--------------------------------------------------------------------------------
/docs/reference/test-models.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Test models"
3 | ---
4 |
5 | Ribasim developers use the following models in their testbench and in order to test new features.
6 |
7 | ```{python}
8 | # | output: asis
9 | # | code-fold: true
10 | import ribasim_testmodels
11 | import matplotlib.pyplot as plt
12 | from IPython.display import Markdown, display
13 |
14 | for model_name, model_constructor in ribasim_testmodels.constructors.items():
15 | if model_name.startswith("invalid"):
16 | continue
17 |
18 | display(Markdown(f"\n# {model_name}\n"))
19 | if model_constructor.__doc__ is not None:
20 | display(Markdown(model_constructor.__doc__))
21 |
22 | model = model_constructor()
23 | fig, ax = plt.subplots(figsize=(6, 4))
24 | model.plot(ax)
25 | ax.axis("off")
26 | plt.show()
27 | plt.close(fig)
28 | ```
29 |
--------------------------------------------------------------------------------
/.sonarcloud.properties:
--------------------------------------------------------------------------------
1 | # Path to sources
2 | sonar.sources=python/ribasim/ribasim,python/ribasim_api/ribasim_api,python/ribasim_testmodels/ribasim_testmodels,ribasim_qgis/core,ribasim_qgis/scripts,ribasim_qgis/ribasim_qgis.py,docs
3 | # sonar.exclusions=
4 | # sonar.inclusions=
5 |
6 | # Path to tests
7 | sonar.tests=python/ribasim/tests,python/ribasim_api/tests,ribasim_qgis/tests
8 | # sonar.test.exclusions=
9 | # sonar.test.inclusions=
10 |
11 | # Source encoding
12 | # sonar.sourceEncoding=
13 |
14 | # Exclusions for copy-paste detection
15 | # sonar.cpd.exclusions=
16 |
17 | # Python version (for python projects only)
18 | sonar.python.version=3.12,3.13
19 |
20 | # C++ standard version (for C++ projects only)
21 | # If not specified, it defaults to the latest supported standard
22 | # sonar.cfamily.reportingCppStandardOverride=c++98|c++11|c++14|c++17|c++20
23 |
--------------------------------------------------------------------------------
/ribasim_qgis/scripts/enable_plugin.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import sys
3 | from pathlib import Path
4 |
5 |
6 | def enable_plugin(plugin_name: str, profile_path: str) -> None:
7 | config_file = Path(profile_path) / "QGIS/QGIS3.ini"
8 | config_file.parent.mkdir(parents=True, exist_ok=True)
9 | config_file.touch()
10 |
11 | config = configparser.ConfigParser()
12 | config.read(config_file)
13 |
14 | plugins_section = "PythonPlugins"
15 | if not config.has_section(plugins_section):
16 | config.add_section(plugins_section)
17 |
18 | config[plugins_section][plugin_name] = "true"
19 |
20 | with config_file.open("w") as f:
21 | config.write(f)
22 |
23 |
24 | if __name__ == "__main__":
25 | print(f"Enabling QGIS plugin {sys.argv[1]} in profile {sys.argv[2]}")
26 | enable_plugin(sys.argv[1], sys.argv[2])
27 |
--------------------------------------------------------------------------------
/python/ribasim_api/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "ribasim_api"
7 | description = "Python bindings for libribasim"
8 | readme = "README.md"
9 | authors = [{ name = "Deltares and contributors", email = "ribasim.info@deltares.nl" }]
10 | license = { text = "MIT" }
11 | classifiers = [
12 | "Intended Audience :: Science/Research",
13 | "Topic :: Scientific/Engineering :: Hydrology",
14 | ]
15 | requires-python = ">=3.12"
16 | dependencies = [
17 | "xmipy >=1.3"
18 | ]
19 | dynamic = ["version"]
20 |
21 | [project.optional-dependencies]
22 | tests = ["pytest", "ribasim", "ribasim_testmodels"]
23 |
24 | [project.urls]
25 | Documentation = "https://ribasim.org/"
26 | Source = "https://github.com/Deltares/Ribasim"
27 |
28 | [tool.hatch.version]
29 | path = "ribasim_api/__init__.py"
30 |
--------------------------------------------------------------------------------
/python/ribasim/README.md:
--------------------------------------------------------------------------------
1 | # Ribasim Python
2 |
3 | The Ribasim Python package (named `ribasim`) aims to make it easy to build, update and analyze Ribasim models
4 | programmatically.
5 |
6 | The Ribasim QGIS plugin allows users to construct a model from scratch without programming.
7 | For specific tasks, like adding observed rainfall timeseries, it can be faster to use
8 | Python instead.
9 |
10 | One can also use Ribasim Python to build entire models from base data, such that your model
11 | setup is fully reproducible.
12 |
13 | The package is [registered in PyPI](https://pypi.org/project/ribasim/) and can therefore
14 | be installed with `pip install ribasim`.
15 |
16 | For documentation please see the https://ribasim.org/ and [API reference](https://ribasim.org/reference/python/)
17 |
18 | # Contributing
19 |
20 | For the developer docs please have a look at https://ribasim.org/dev/.
21 |
--------------------------------------------------------------------------------
/python/ribasim_testmodels/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "ribasim_testmodels"
7 | description = "Ribasim Testmodels"
8 | readme = "README.md"
9 | authors = [
10 | { name = "Deltares and contributors", email = "ribasim.info@deltares.nl" },
11 | ]
12 | license = { text = "MIT" }
13 | classifiers = [
14 | "Intended Audience :: Science/Research",
15 | "Topic :: Scientific/Engineering :: Hydrology",
16 | ]
17 | requires-python = ">=3.12"
18 | dependencies = ["geopandas >=1.0", "numpy >=2.0", "pandas >=2.2", "ribasim"]
19 | dynamic = ["version"]
20 |
21 | [project.optional-dependencies]
22 | tests = ["pytest"]
23 |
24 | [project.urls]
25 | Documentation = "https://ribasim.org/"
26 | Source = "https://github.com/Deltares/Ribasim"
27 |
28 | [tool.hatch.version]
29 | path = "ribasim_testmodels/__init__.py"
30 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/geometry/base.py:
--------------------------------------------------------------------------------
1 | from typing import get_type_hints
2 |
3 | import pandera as pa
4 | from geopandas import GeoSeries as _GeoSeries
5 | from pandera.typing import Series
6 | from pandera.typing.geopandas import GeoSeries
7 |
8 | from ribasim.schemas import _BaseSchema
9 |
10 |
11 | class _GeoBaseSchema(_BaseSchema):
12 | @pa.check("geometry")
13 | def is_correct_geometry_type(cls, geoseries: GeoSeries[object]) -> Series[bool]:
14 | T = get_type_hints(cls)["geometry"].__args__[0]
15 | return geoseries.map(lambda geom: isinstance(geom, T))
16 |
17 | @pa.parser("geometry")
18 | def force_2d(cls, geometry: GeoSeries[object]) -> GeoSeries[object]:
19 | if isinstance(geometry, _GeoSeries):
20 | # Pandera requires GeoSeries to have a name
21 | return GeoSeries(geometry.force_2d(), name=geometry.name)
22 | else:
23 | return geometry
24 |
--------------------------------------------------------------------------------
/.github/workflows/python_codegen.yml:
--------------------------------------------------------------------------------
1 | name: Python Codegen
2 | on:
3 | push:
4 | branches: [main]
5 | paths: ["core/**", "python/**", "pixi.toml", "pixi.lock"]
6 | tags: ["*"]
7 | pull_request:
8 | paths: ["core/**", "python/**", "pixi.toml", "pixi.lock"]
9 | merge_group:
10 | workflow_dispatch:
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: true
14 | jobs:
15 | codegen:
16 | name: Codegen
17 | runs-on: ubuntu-latest
18 | continue-on-error: true
19 | steps:
20 | - uses: actions/checkout@v6
21 | - uses: prefix-dev/setup-pixi@v0.9.3
22 | with:
23 | pixi-version: "latest"
24 | - name: Prepare pixi
25 | run: pixi run install-ci
26 | - name: Test if codegen runs without errors
27 | run: pixi run codegen
28 | - name: Ensure that no code has been generated
29 | run: git diff --exit-code
30 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit_auto_update.yml:
--------------------------------------------------------------------------------
1 | name: Pre-commit auto update
2 | on:
3 | schedule:
4 | # At 03:00 on day 3 of the month
5 | - cron: "0 3 3 * *"
6 | # on demand
7 | workflow_dispatch:
8 | jobs:
9 | auto-update:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v6
13 | - uses: prefix-dev/setup-pixi@v0.9.3
14 | with:
15 | pixi-version: "latest"
16 | - name: Update pre-commit hooks
17 | run: |
18 | pixi run pre-commit-autoupdate
19 | - name: Run pre-commit on all files
20 | run: |
21 | pixi run pre-commit
22 | continue-on-error: true
23 | - uses: peter-evans/create-pull-request@v8
24 | with:
25 | token: ${{ secrets.CI_PR_PAT }}
26 | branch: update/pre-commit
27 | title: Update pre-commit hooks
28 | commit-message: "Update pre-commit hooks"
29 | author: "GitHub "
30 |
--------------------------------------------------------------------------------
/ribasim_qgis/scripts/install_ribasim_qgis.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pathlib import Path
3 |
4 | from enable_plugin import enable_plugin
5 |
6 | if __name__ == "__main__":
7 | target_path = Path("ribasim_qgis").absolute()
8 | plugins_path = Path(sys.argv[1]) / "python/plugins"
9 | source_path = plugins_path / "ribasim_qgis"
10 |
11 | styles_source_path = target_path / "core" / "styles"
12 | styles_target_path = Path("python/ribasim/ribasim/styles").absolute()
13 |
14 | # Symlink ribasim_qgis styles to ribasim styles
15 | styles_source_path.unlink(missing_ok=True)
16 | styles_source_path.symlink_to(styles_target_path, target_is_directory=True)
17 |
18 | # Symlink qgis_env to ribasim_qgis, and hence qgis_env styles to ribasim styles
19 | plugins_path.mkdir(parents=True, exist_ok=True)
20 | source_path.unlink(missing_ok=True)
21 | source_path.symlink_to(target_path, target_is_directory=True)
22 | enable_plugin("ribasim_qgis", sys.argv[1])
23 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "julia",
6 | "request": "launch",
7 | "name": "debug Ribasim",
8 | "program": "debug/debug.jl",
9 | "stopOnEntry": false,
10 | "cwd": "${workspaceFolder}",
11 | "juliaEnv": "${command:activeJuliaEnvironment}"
12 | },
13 | {
14 | "type": "julia",
15 | "request": "launch",
16 | "name": "Julia: current file",
17 | "program": "${file}",
18 | "stopOnEntry": false,
19 | "cwd": "${workspaceFolder}",
20 | "juliaEnv": "${command:activeJuliaEnvironment}"
21 | },
22 | {
23 | "name": "Ribasim QGIS: Attach to QGIS",
24 | "type": "python",
25 | "request": "attach",
26 | "host": "localhost",
27 | "port": 5678,
28 | "justMyCode": false
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/docs/assets/c4_system.mmd:
--------------------------------------------------------------------------------
1 | flowchart TB
2 | modeler([Modeler]):::user
3 |
4 | api["Ribasim Python\n[python]"]:::system
5 | modeler-->|prepare model|api
6 |
7 | ribasim["Ribasim\n[julia]"]:::system
8 | modeler-->|start|ribasim
9 |
10 | subgraph qgisBoundary[QGIS]
11 | QGIS[QGIS Application]:::system_ext
12 | qgisPlugin["Ribasim QGIS plugin\n[python]"]:::system
13 | QGIS-->qgisPlugin
14 | end
15 | modeler-->|prepare model|qgisBoundary
16 |
17 | model[("input model data\n[toml + geopackage + arrow]")]:::system
18 | qgisPlugin-->|read/write|model
19 | api-->|read/write|model
20 | ribasim-->|simulate|model
21 |
22 | output[("simulation results\n[arrow]")]:::system
23 | ribasim-->|write|output
24 |
25 | class qgisBoundary boundary
26 |
27 | %% class definitions for C4 model
28 | classDef default stroke-width:1px,stroke:white,color:white
29 | classDef system fill:#1168bd
30 | classDef user fill:#08427b
31 | classDef system_ext fill:#999999
32 | classDef boundary fill:transparent,stroke-dasharray:5 5,stroke:black,color:black
33 |
--------------------------------------------------------------------------------
/ribasim_qgis/tests/ui/test_load_plugin.py:
--------------------------------------------------------------------------------
1 | from qgis.testing import unittest
2 | from qgis.utils import iface, plugins
3 |
4 |
5 | class TestPlugin(unittest.TestCase):
6 | def test_plugin_is_loaded(self):
7 | """Test plugin is properly loaded and appears in QGIS plugins."""
8 | plugin = plugins.get("ribasim_qgis")
9 | self.assertTrue(plugin, "Ribasim plugin not loaded")
10 |
11 | def test_plugin(self):
12 | """Triggers Ribasim button and checks that Dock is added."""
13 | # This checks the *actual* QGIS interface, not just a stub
14 | self.assertTrue(iface is not None, "QGIS interface not available")
15 |
16 | toolbars = [
17 | c for c in iface.mainWindow().children() if c.objectName() == "Ribasim"
18 | ]
19 | self.assertTrue(len(toolbars) == 2, "No Ribasim toolbar and menu")
20 | actions = toolbars[0].actions()
21 | self.assertTrue(
22 | len(actions) == 1, "No (single) Ribasim action button in toolbar"
23 | )
24 |
--------------------------------------------------------------------------------
/utils/generate-sbom.jl:
--------------------------------------------------------------------------------
1 | using PkgToSoftwareBOM
2 | using UUIDs
3 | using Pkg
4 |
5 | Pkg.activate("core")
6 |
7 | ribasimLicense = SpdxLicenseExpressionV2("MIT")
8 | organization = SpdxCreatorV2("Organization", "Deltares", "software@deltares.nl")
9 | packageInstructions = spdxPackageInstructions(;
10 | spdxfile_toexclude = ["Ribasim.spdx.json"],
11 | originator = organization,
12 | declaredLicense = ribasimLicense,
13 | copyright = "Copyright (c) 2025 Deltares ",
14 | name = "Ribasim",
15 | )
16 |
17 | dependencies = Pkg.project().dependencies;
18 | spdxData = spdxCreationData(;
19 | Name = "Ribasim.jl",
20 | Creators = [organization],
21 | NamespaceURL = "https://github.com/Deltares/Ribasim/Ribasim.spdx.json",
22 | rootpackages = dependencies,
23 | find_artifactsource = true,
24 | packageInstructions = Dict{UUID, spdxPackageInstructions}(
25 | Pkg.project().uuid => packageInstructions,
26 | ),
27 | )
28 |
29 | sbom = generateSPDX(spdxData)
30 | writespdx(sbom, "Ribasim.spdx.json")
31 |
--------------------------------------------------------------------------------
/docs/run_ribasim.py:
--------------------------------------------------------------------------------
1 | """
2 | Julia interface setup for Ribasim simulations.
3 |
4 | In QMD files using the jupyter engine, this script can be included using
5 | `%run path/to/run_ribasim.py`. This will start a Julia session,
6 | import Ribasim, and define the `run_ribasim` function.
7 |
8 | In a file where multiple simulations are run this has the benefit
9 | of only needing to start Julia once, compared to `subprocess.run`.
10 |
11 | Since Ribasim Python also has `run_ribasim`, we can include
12 | this script in a hidden evaluated cell, and put `from ribasim import run_ribasim`
13 | in a visible unevaluated cell. That way we can run this version on CI but
14 | it looks like we run the Ribasim Python version.
15 | """
16 |
17 | from pathlib import Path
18 |
19 | from juliacall import Main as jl
20 |
21 | jl.seval("import Ribasim")
22 |
23 |
24 | def run_ribasim(toml_path: str | Path) -> None:
25 | """Run a Ribasim simulation via juliacall."""
26 | retcode = jl.Ribasim.main(str(toml_path))
27 | assert retcode == 0, f"Simulation failed: {toml_path}"
28 |
--------------------------------------------------------------------------------
/ribasim_qgis/core/geopackage.py:
--------------------------------------------------------------------------------
1 | """
2 | Geopackage management utilities.
3 |
4 | This module lightly wraps a QGIS built in functions to:
5 |
6 | * List the layers of a geopackage
7 |
8 | """
9 |
10 | import sqlite3
11 | from contextlib import contextmanager
12 | from pathlib import Path
13 |
14 |
15 | @contextmanager
16 | def sqlite3_cursor(path: Path):
17 | connection = sqlite3.connect(path)
18 | cursor = connection.cursor()
19 | try:
20 | yield cursor
21 | finally:
22 | cursor.close()
23 | connection.commit()
24 | connection.close()
25 |
26 |
27 | def layers(path: Path) -> list[str]:
28 | """
29 | Return all layers that are present in the geopackage.
30 |
31 | Parameters
32 | ----------
33 | path: Path
34 | Path to the geopackage
35 |
36 | Returns
37 | -------
38 | layernames: list[str]
39 | """
40 | with sqlite3_cursor(path) as cursor:
41 | cursor.execute("Select table_name from gpkg_contents")
42 | layers = [item[0] for item in cursor.fetchall()]
43 | return layers
44 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | exclude: 'core/test/data/allocation_problems'
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v6.0.0
5 | hooks:
6 | - id: check-added-large-files
7 | - id: check-case-conflict
8 | - id: check-yaml
9 | - id: check-toml
10 | - id: check-merge-conflict
11 | - id: check-vcs-permalinks
12 | - id: end-of-file-fixer
13 | exclude: '.teamcity'
14 | - id: trailing-whitespace
15 | - repo: https://github.com/astral-sh/ruff-pre-commit
16 | rev: v0.14.7
17 | hooks:
18 | - id: ruff-check
19 | types_or: [python, pyi, jupyter]
20 | args: [--fix, --exit-non-zero-on-fix]
21 | - id: ruff-format
22 | types_or: [python, pyi, jupyter]
23 | - repo: https://github.com/google/yamlfmt
24 | rev: v0.20.0
25 | hooks:
26 | - id: yamlfmt
27 | - repo: https://github.com/kynan/nbstripout
28 | rev: 0.8.2
29 | hooks:
30 | - id: nbstripout
31 | - repo: https://github.com/adhtruong/mirrors-typos
32 | rev: v1.40.0
33 | hooks:
34 | - id: typos
35 |
--------------------------------------------------------------------------------
/.github/workflows/mypy.yml:
--------------------------------------------------------------------------------
1 | name: Mypy Type Check
2 | on:
3 | push:
4 | branches: [main]
5 | paths: ["ribasim_qgis/**", "python/**", "pixi.toml", "pixi.lock"]
6 | tags: ["*"]
7 | pull_request:
8 | paths: ["ribasim_qgis/**", "python/**", "pixi.toml", "pixi.lock"]
9 | merge_group:
10 | workflow_dispatch:
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: true
14 | jobs:
15 | mypy:
16 | name: Mypy
17 | runs-on: ubuntu-latest
18 | continue-on-error: true
19 | steps:
20 | - uses: actions/checkout@v6
21 | - uses: prefix-dev/setup-pixi@v0.9.3
22 | with:
23 | pixi-version: "latest"
24 | - name: Run mypy on python/ribasim
25 | run: |
26 | pixi run mypy-ribasim-python
27 | - name: Run mypy on python/ribasim_testmodels
28 | run: |
29 | pixi run mypy-ribasim-testmodels
30 | - name: Run mypy on python/ribasim_api
31 | run: |
32 | pixi run mypy-ribasim-api
33 | - name: Run mypy on ribasim_qgis
34 | run: |
35 | pixi run mypy-ribasim-qgis
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Ribasim developers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/julia_auto_update.yml:
--------------------------------------------------------------------------------
1 | name: Julia auto update
2 | on:
3 | schedule:
4 | # At 03:00 on day 2 of the month
5 | - cron: "0 3 2 * *"
6 | # on demand
7 | workflow_dispatch:
8 | jobs:
9 | auto-update:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v6
13 | with:
14 | ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
15 | - uses: prefix-dev/setup-pixi@v0.9.3
16 | with:
17 | pixi-version: "latest"
18 | - name: Update Julia manifest file
19 | run: |
20 | pixi run install-julia
21 | pixi run update-manifest-julia
22 | pixi update pyjuliacall
23 | pixi run generate-sbom-ribasim-core
24 | env:
25 | JULIA_PKG_PRECOMPILE_AUTO: "0"
26 | - uses: peter-evans/create-pull-request@v8
27 | with:
28 | token: ${{ secrets.CI_PR_PAT }}
29 | branch: update/julia-manifest
30 | title: Update Julia manifest
31 | commit-message: "Update Julia manifest"
32 | body-path: .pixi/update-manifest-julia.md
33 | author: "GitHub "
34 |
--------------------------------------------------------------------------------
/core/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Ribasim developers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/python/ribasim/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Ribasim developers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ribasim_qgis/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Ribasim developers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/python/ribasim_api/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Ribasim developers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/python/ribasim_testmodels/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Ribasim developers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/pixi_auto_update.yml:
--------------------------------------------------------------------------------
1 | name: Update Pixi lockfile
2 | permissions:
3 | contents: write
4 | pull-requests: write
5 | on:
6 | schedule:
7 | # At 03:00 on day 3 of the month
8 | - cron: "0 3 3 * *"
9 | # on demand
10 | workflow_dispatch:
11 | jobs:
12 | pixi-update:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v6
16 | - name: Set up pixi
17 | uses: prefix-dev/setup-pixi@v0.9.3
18 | with:
19 | pixi-version: "latest"
20 | run-install: false
21 | - name: Update lockfiles
22 | run: |
23 | set -o pipefail
24 | pixi update --json | pixi exec pixi-diff-to-markdown >> diff.md
25 | pixi run generate-sbom-ribasim-python
26 | - name: Create pull request
27 | uses: peter-evans/create-pull-request@v8
28 | with:
29 | token: ${{ secrets.CI_PR_PAT }}
30 | commit-message: Update pixi lockfile
31 | title: Update pixi lockfile
32 | body-path: diff.md
33 | branch: update/pixi-lock
34 | base: main
35 | delete-branch: true
36 | add-paths: pixi.lock, Ribasim-python.spdx.json
37 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/delwaq/template/B5_bounddata.inc.j2:
--------------------------------------------------------------------------------
1 | ; Generated by Ribasim
2 | ITEM 'LevelBoundary'
3 | CONCENTRATIONS 'Continuity' 'LevelBoundary'
4 | DATA 1 1
5 |
6 | ITEM 'FlowBoundary'
7 | CONCENTRATIONS 'Continuity' 'FlowBoundary'
8 | DATA 1 1
9 |
10 | ITEM 'UserDemand'
11 | CONCENTRATIONS 'Continuity' 'UserDemand'
12 | DATA 1 1
13 |
14 | ITEM 'Drainage'
15 | CONCENTRATIONS 'Continuity' 'Drainage'
16 | DATA 1 1
17 |
18 | ITEM 'Precipitation'
19 | CONCENTRATIONS 'Continuity' 'Precipitation'
20 | DATA 1 1
21 |
22 | ITEM 'SurfaceRunoff'
23 | CONCENTRATIONS 'Continuity' 'SurfaceRunoff'
24 | DATA 1 1
25 |
26 | ITEM 'Infiltration'
27 | CONCENTRATIONS 'Continuity'
28 | DATA 1
29 |
30 | ITEM 'Terminal'
31 | CONCENTRATIONS 'Continuity'
32 | DATA 1
33 |
34 |
35 | {% for boundary in boundaries -%}
36 | ITEM '{{ boundary.name }}'
37 | CONCENTRATIONS {{ boundary.substances | join(" ") | safe }}
38 | ABSOLUTE TIME
39 | LINEAR DATA {{ boundary.substances | join(" ") | safe }}
40 | {{ boundary.df | safe }}
41 |
42 | {% endfor -%}
43 | {% for state in states -%}
44 | ITEM '{{ state.name }}'
45 | CONCENTRATIONS {{ state.substances | join(" ")}}
46 | DATA {{ state.concentrations | join(" ")}}
47 |
48 | {% endfor -%}
49 |
--------------------------------------------------------------------------------
/.github/workflows/python_tests.yml:
--------------------------------------------------------------------------------
1 | name: Ribasim Python Tests
2 | on:
3 | push:
4 | branches: [main]
5 | paths: ["python/**", "pixi.toml", "pixi.lock"]
6 | tags: ["*"]
7 | pull_request:
8 | paths: ["python/**", "pixi.toml", "pixi.lock"]
9 | merge_group:
10 | workflow_dispatch:
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: true
14 | jobs:
15 | test:
16 | name: ${{ matrix.pixi-environment }} - ${{ matrix.os }}
17 | runs-on: ${{ matrix.os }}
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | os:
22 | - ubuntu-latest
23 | - macOS-latest
24 | - windows-latest
25 | pixi-environment:
26 | - default
27 | - py312
28 | steps:
29 | - uses: actions/checkout@v6
30 | - uses: prefix-dev/setup-pixi@v0.9.3
31 | with:
32 | pixi-version: "latest"
33 | - name: Test Ribasim Python
34 | run: pixi run --environment ${{ matrix.pixi-environment }} test-ribasim-python-cov
35 | - name: Upload coverage to Codecov
36 | uses: codecov/codecov-action@v5
37 | with:
38 | token: ${{ secrets.CODECOV_TOKEN }}
39 |
--------------------------------------------------------------------------------
/docs/guide/coupling.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Coupling"
3 | ---
4 |
5 | # iMOD
6 |
7 | Ribasim can also be (online) coupled to other kernels with the help of iMOD Coupler. The corresponding documentation can be found within the [iMOD Suite Documentation](https://deltares.github.io/iMOD-Documentation/coupler.html).
8 |
9 | # Water quality {#sec-waterquality}
10 |
11 | Ribasim can be offline coupled to Delwaq, the Deltares Water Quality model.
12 |
13 | ```{mermaid}
14 | flowchart LR
15 | Ribasim --> Delwaq
16 | ```
17 |
18 | ## Setup
19 |
20 | Delwaq can calculate the concentration of substances in [Basin](/reference/node/basin.qmd) nodes over time, based on initial concentrations, and of [FlowBoundary](/reference/node/flow-boundary.qmd) nodes. Ribasim exposes the `Basin / concentration`, `Basin / concentration_state`, `FlowBoundary / concentration`, and `LevelBoundary / concentration` tables to setup these substances and concentrations.
21 |
22 | When a Ribasim model ran with the above tables, one can use the utilities in the `delwaq` namespace of the Ribasim Python API to generate the input required for Delwaq to run, as well as to parse the output from Delwaq into a Ribasim compatible format. For more information see the [guide](/guide/delwaq.qmd).
23 |
--------------------------------------------------------------------------------
/python/ribasim_api/ribasim_api/ribasim_api.py:
--------------------------------------------------------------------------------
1 | # %%
2 | from ctypes import byref, c_int, create_string_buffer
3 |
4 | from xmipy import XmiWrapper
5 |
6 |
7 | class RibasimApi(XmiWrapper):
8 | def get_constant_int(self, name: str) -> int:
9 | match name:
10 | case "BMI_LENVARTYPE":
11 | return 51
12 | case "BMI_LENGRIDTYPE":
13 | return 17
14 | case "BMI_LENVARADDRESS":
15 | return 68
16 | case "BMI_LENCOMPONENTNAME":
17 | return 256
18 | case "BMI_LENVERSION":
19 | return 256
20 | case "BMI_LENERRMESSAGE":
21 | return 1025
22 | raise ValueError(f"{name} does not map to an integer exposed by Ribasim")
23 |
24 | def init_julia(self) -> None:
25 | argument = create_string_buffer(0)
26 | self.lib.init_julia(c_int(0), byref(argument))
27 |
28 | def shutdown_julia(self) -> None:
29 | self.lib.shutdown_julia(c_int(0))
30 |
31 | def update_subgrid_level(self) -> None:
32 | self.lib.update_subgrid_level()
33 |
34 | def execute(self, config_file: str) -> None:
35 | self._execute_function(self.lib.execute, config_file.encode())
36 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/geometry/area.py:
--------------------------------------------------------------------------------
1 | import pandera as pa
2 | from pandera.dtypes import Int32
3 | from pandera.typing import Index, Series
4 | from pandera.typing.geopandas import GeoSeries
5 | from shapely.geometry import MultiPolygon, Polygon
6 |
7 | from .base import _GeoBaseSchema
8 |
9 |
10 | class BasinAreaSchema(_GeoBaseSchema):
11 | fid: Index[Int32] = pa.Field(default=0, check_name=True)
12 | node_id: Series[Int32] = pa.Field(nullable=False, default=0)
13 | geometry: GeoSeries[MultiPolygon] = pa.Field(default=None, nullable=True)
14 |
15 | @pa.parser("geometry")
16 | def convert_to_multi(cls, series):
17 | return series.apply(
18 | lambda geom: MultiPolygon([geom]) if isinstance(geom, Polygon) else geom
19 | )
20 |
21 |
22 | class FlowBoundaryAreaSchema(_GeoBaseSchema):
23 | fid: Index[Int32] = pa.Field(default=0, check_name=True)
24 | node_id: Series[Int32] = pa.Field(nullable=False, default=0)
25 | geometry: GeoSeries[MultiPolygon] = pa.Field(default=None, nullable=True)
26 |
27 | @pa.parser("geometry")
28 | def convert_to_multi(cls, series):
29 | return series.apply(
30 | lambda geom: MultiPolygon([geom]) if isinstance(geom, Polygon) else geom
31 | )
32 |
--------------------------------------------------------------------------------
/docs/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: Ribasim
3 | ---
4 |
5 | Ribasim is a water resources model to simulate the physical behavior of a managed open water system based on a set of control rules and a prioritized water allocation strategy.
6 |
7 | Ribasim is written in the [Julia programming language](https://julialang.org/) and is built
8 | on top of the [SciML: Open Source Software for Scientific Machine Learning](https://sciml.ai/)
9 | libraries.
10 |
11 | The initial version of Ribasim is developed by [Deltares](https://www.deltares.nl) as part of a consortium for the Dutch watersystem.
12 | This activity is co-funded by [TKI Deltatechnology](https://tkideltatechnologie.nl/), a Dutch public–private partnership innovation program from the Ministry of Economic Affairs.
13 | Ribasim will be used as the surface water module of the [Netherlands Hydrologic Instrument (NHI)](https://nhi.nu/).
14 |
15 | {fig-alt="TKI partner logos"}
16 |
17 | Ribasim model of the main water distribution network in the Netherlands.
18 |
19 |
20 |
--------------------------------------------------------------------------------
/utils/templates/validation.py.jinja:
--------------------------------------------------------------------------------
1 | # Automatically generated file. Do not modify.
2 |
3 | # Table for connectivity
4 | # "Basin": ["LinearResistance"] means that the downstream of basin can be LinearResistance only
5 | node_type_connectivity: dict[str, list[str]] = {
6 | {% for n in nodes %}
7 | '{{n[:name]}}': [{% for value in n[:connectivity] %}
8 | '{{ value }}',
9 | {% end %}],
10 | {% end %}
11 | }
12 |
13 | # Function to validate connection
14 | def can_connect(node_type_up: str, node_type_down: str) -> bool:
15 | if node_type_up in node_type_connectivity:
16 | return node_type_down in node_type_connectivity[node_type_up]
17 | return False
18 |
19 | flow_link_neighbor_amount: dict[str, list[int]] = {
20 | {% for n in nodes %}
21 | '{{n[:name]}}':
22 | [{{ n[:flow_neighbor_bound].in_min }}, {{ n[:flow_neighbor_bound].in_max }}, {{ n[:flow_neighbor_bound].out_min }}, {{ n[:flow_neighbor_bound].out_max }}],
23 | {% end %}
24 | }
25 |
26 | control_link_neighbor_amount: dict[str, list[int]] = {
27 | {% for n in nodes %}
28 | '{{n[:name]}}':
29 | [{{ n[:control_neighbor_bound].in_min }}, {{ n[:control_neighbor_bound].in_max }}, {{ n[:control_neighbor_bound].out_min }}, {{ n[:control_neighbor_bound].out_max }}],
30 | {% end %}
31 | }
32 |
--------------------------------------------------------------------------------
/python/ribasim/tests/test_migrations.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 | from ribasim import Model
5 | from ribasim.db_utils import _get_db_schema_version
6 |
7 | root_folder = Path(__file__).parent.parent.parent.parent
8 | print(root_folder)
9 |
10 |
11 | @pytest.mark.regression
12 | def test_hws_migration(tmp_path):
13 | toml_path = root_folder / "models/hws_migration_test/hws.toml"
14 | db_path = root_folder / "models/hws_migration_test/database.gpkg"
15 |
16 | assert toml_path.exists(), (
17 | "Can't find the model, did you retrieve it with s3_download.py?"
18 | )
19 |
20 | assert _get_db_schema_version(db_path) == 0
21 | model = Model.read(toml_path)
22 |
23 | assert model.link.df.index.name == "link_id"
24 | assert len(model.link.df) == 454
25 | model.write(tmp_path / "hws_migrated.toml")
26 |
27 |
28 | def test_active_migration():
29 | from pandas import DataFrame
30 | from ribasim.migrations import check_inactive
31 |
32 | df = DataFrame({"node_id": [1, 2, 3], "active": [True, False, None]})
33 |
34 | with pytest.raises(
35 | ValueError,
36 | match=r"Inactive node\(s\) with node_id \[2\] in test_nodes cannot be migrated automatically",
37 | ):
38 | check_inactive(df, "test_nodes")
39 |
--------------------------------------------------------------------------------
/docs/assets/c4_component_ribasim.mmd:
--------------------------------------------------------------------------------
1 | flowchart TB
2 | modeler([Modeler]):::user
3 |
4 | api["Ribasim Python\n[python]"]:::system
5 | modeler-->|prepare model|api
6 |
7 | subgraph ribasimBoundary[Ribasim]
8 | ribasim["Ribasim.jl\n[julia]"]:::system
9 | libribasim["libribasim\n[julia + python + BMI]"]:::system
10 | cli["Ribasim CLI\n[julia]"]:::system
11 | cli-->ribasim
12 | libribasim-->ribasim
13 | end
14 | modeler-->|start|cli
15 | modeler-->|coupled simulation|libribasim
16 |
17 | subgraph qgisBoundary[QGIS]
18 | QGIS[QGIS Application]:::system_ext
19 | qgisPlugin["Ribasim QGIS plugin\n[python]"]:::system
20 | QGIS-->qgisPlugin
21 | end
22 | modeler-->|prepare model|qgisBoundary
23 |
24 | model[("input model data\n[toml + geopackage + arrow]")]:::system
25 | qgisPlugin-->|read/write|model
26 | api-->|read/write|model
27 | ribasim-->|simulate|model
28 |
29 | output[("simulation output\n[arrow]")]:::system
30 | ribasim-->|write|output
31 |
32 | class qgisBoundary,ribasimBoundary boundary
33 |
34 | %% class definitions for C4 model
35 | classDef default stroke-width:1px,stroke:white,color:white
36 | classDef system fill:#1168bd
37 | classDef user fill:#08427b
38 | classDef system_ext fill:#999999
39 | classDef boundary fill:transparent,stroke-dasharray:5 5,stroke:black,color:black
40 |
--------------------------------------------------------------------------------
/.teamcity/Ribasim/buildTypes/Ribasim_MakeQgisPlugin.kt:
--------------------------------------------------------------------------------
1 | package Ribasim.buildTypes
2 |
3 | import Templates.LinuxAgent
4 | import jetbrains.buildServer.configs.kotlin.*
5 | import jetbrains.buildServer.configs.kotlin.buildSteps.script
6 |
7 | object Ribasim_MakeQgisPlugin : BuildType({
8 | templates(LinuxAgent)
9 | name = "Make QGIS plugin"
10 |
11 | artifactRules = "ribasim_qgis.zip"
12 |
13 | vcs {
14 | root(Ribasim.vcsRoots.Ribasim)
15 | cleanCheckout = true
16 | }
17 |
18 | steps {
19 | script {
20 | id = "RUNNER_2193"
21 | scriptContent = """
22 | #!/bin/bash
23 | source /usr/share/Modules/init/bash
24 |
25 | module load pixi
26 | pixi run install-ribasim-qgis
27 |
28 | rm --recursive ribasim_qgis/scripts
29 | rm --recursive ribasim_qgis/tests
30 | rm ribasim_qgis/.coveragerc
31 | rsync --verbose --recursive --delete ribasim_qgis/ ribasim_qgis
32 | rm --force ribasim_qgis.zip
33 | zip -r ribasim_qgis.zip ribasim_qgis
34 | """.trimIndent()
35 | }
36 | }
37 |
38 | requirements {
39 | doesNotEqual("env.OS", "Windows_NT", "RQ_338")
40 | }
41 |
42 | disableSettings("RQ_338")
43 | })
44 |
--------------------------------------------------------------------------------
/.teamcity/settings.kts:
--------------------------------------------------------------------------------
1 | import jetbrains.buildServer.configs.kotlin.*
2 |
3 | /*
4 | The settings script is an entry point for defining a single
5 | TeamCity project. TeamCity looks for the 'settings.kts' file in a
6 | project directory and runs it if it's found, so the script name
7 | shouldn't be changed and its package should be the same as the
8 | project's id.
9 |
10 | The script should contain a single call to the project() function
11 | with a Project instance or an init function as an argument.
12 |
13 | VcsRoots, BuildTypes, and Templates of this project must be
14 | registered inside project using the vcsRoot(), buildType(), and
15 | template() methods respectively.
16 |
17 | Subprojects can be defined either in their own settings.kts or by
18 | calling the subProjects() method in this project.
19 |
20 | To debug settings scripts in command-line, run the
21 |
22 | mvnDebug org.jetbrains.teamcity:teamcity-configs-maven-plugin:generate
23 |
24 | command and attach your debugger to the port 8000.
25 |
26 | To debug in IntelliJ Idea, open the 'Maven Projects' tool window (View ->
27 | Tool Windows -> Maven Projects), find the generate task
28 | node (Plugins -> teamcity-configs -> teamcity-configs:generate),
29 | the 'Debug' option is available in the context menu for the task.
30 | */
31 |
32 | version = "2025.07"
33 | project(Ribasim.Project)
34 |
--------------------------------------------------------------------------------
/utils/s3_upload.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from pathlib import Path
3 |
4 | from minio import Minio
5 | from minio.error import S3Error
6 | from s3_settings import settings
7 |
8 | minioServer = "s3.deltares.nl"
9 | bucketName = "ribasim"
10 |
11 | parser = argparse.ArgumentParser(description="Upload a file to the MinIO server")
12 | parser.add_argument("source", type=Path, help="The source file to upload")
13 | parser.add_argument("destination", help="The destination file in the MinIO server")
14 | parser.add_argument(
15 | "--accesskey",
16 | help="The access key to access the MinIO server",
17 | default=settings.minio_access_key,
18 | )
19 | parser.add_argument(
20 | "--secretkey",
21 | help="The secret key to access the MinIO server",
22 | default=settings.minio_secret_key,
23 | )
24 | args = parser.parse_args()
25 |
26 | if not args.source.is_file():
27 | raise ValueError("The source file does not exist")
28 |
29 | if (not args.accesskey) or (not args.secretkey):
30 | raise ValueError("No MinIO access key or secret key provided")
31 |
32 | # Minio client connection
33 | client = Minio(minioServer, access_key=args.accesskey, secret_key=args.secretkey)
34 |
35 | try:
36 | client.fput_object(
37 | bucketName,
38 | args.destination,
39 | args.source,
40 | )
41 | except S3Error as e:
42 | print(f"Error occurred: {e}")
43 |
--------------------------------------------------------------------------------
/.teamcity/Templates/GithubIntegration.kt:
--------------------------------------------------------------------------------
1 | package Templates
2 |
3 | import Ribasim.vcsRoots.Ribasim
4 | import jetbrains.buildServer.configs.kotlin.Template
5 | import jetbrains.buildServer.configs.kotlin.buildFeatures.PullRequests
6 | import jetbrains.buildServer.configs.kotlin.buildFeatures.commitStatusPublisher
7 | import jetbrains.buildServer.configs.kotlin.buildFeatures.pullRequests
8 |
9 | object GithubCommitStatusIntegration : Template({
10 | name = "GithubCommitStatusIntegrationTemplate"
11 |
12 | features {
13 | commitStatusPublisher {
14 | vcsRootExtId = "${Ribasim.id}"
15 | publisher = github {
16 | githubUrl = "https://api.github.com"
17 | authType = personalToken {
18 | token = "credentialsJSON:6b37af71-1f2f-4611-8856-db07965445c0"
19 | }
20 | }
21 | }
22 | }
23 | })
24 |
25 | object GithubPullRequestsIntegration : Template({
26 | name = "GithubPullRequestsIntegrationTemplate"
27 |
28 | features {
29 | pullRequests {
30 | vcsRootExtId = "${Ribasim.id}"
31 | provider = github {
32 | authType = token {
33 | token = "credentialsJSON:6b37af71-1f2f-4611-8856-db07965445c0"
34 | }
35 | filterAuthorRole = PullRequests.GitHubRoleFilter.MEMBER
36 | }
37 | }
38 | }
39 | })
--------------------------------------------------------------------------------
/docs/reference/node/linear-resistance.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "LinearResistance"
3 | ---
4 |
5 | Bidirectional flow proportional to the level difference between the connected basins.
6 |
7 | # Tables
8 |
9 | ## Static
10 |
11 | column | type | unit | restriction
12 | ------------- | ------- | --------------------- | -----------
13 | node_id | Int32 | - |
14 | resistance | Float64 | $\text{s}/\text{m}^2$ | -
15 | max_flow_rate | Float64 | $\text{m}^3/s$ | non-negative
16 | control_state | String | - | (optional)
17 |
18 | # Equations
19 |
20 | A LinearResistance connects two Basins together.
21 | The flow between the two Basins is determined by a linear relationship, up to an optional maximum flow rate:
22 |
23 | $$
24 | Q_\text{linear\_resistance} = \phi\mathrm{clamp}\left(\frac{h_a - h_b}{R}, -Q_{\max}, Q_{\max}\right)
25 | $$
26 |
27 | Here $h_a$ is the water level in the incoming Basin and $h_b$ is the water level in the outgoing Basin.
28 | $R$ is the resistance of the link, and $Q_{\max}$ is the maximum flow rate.
29 | Water flows from high to low; either direction is possible.
30 | $\phi$ is the reduction factor which makes the flow go smoothly to $0$ as the upstream storage (as determined by the flow direction) becomes smaller than the equivalent of a water depth of $10 \;\text{cm}$ by default (change with solver setting `depth_threshold`).
31 |
--------------------------------------------------------------------------------
/ribasim_qgis/scripts/install_qgis_plugin.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import subprocess
4 | import sys
5 | from pathlib import Path
6 |
7 |
8 | def install_qgis_plugin(plugin_name: str, profile_path: str) -> None:
9 | plugin_path = Path(profile_path) / "python/plugins"
10 | plugin_path.mkdir(parents=True, exist_ok=True)
11 |
12 | try:
13 | env = os.environ.copy()
14 | env["QGIS_PLUGIN_MANAGER_QGIS_VERSION"] = "3.40"
15 |
16 | subprocess.check_call(["qgis-plugin-manager", "init"], cwd=plugin_path, env=env)
17 | subprocess.check_call(
18 | ["qgis-plugin-manager", "update"], cwd=plugin_path, env=env
19 | )
20 | subprocess.check_call(
21 | [
22 | "qgis-plugin-manager",
23 | "install",
24 | plugin_name,
25 | "--deprecated",
26 | "--force",
27 | "--upgrade",
28 | ],
29 | cwd=plugin_path,
30 | env=env,
31 | )
32 | finally:
33 | # remove the qgis-manager-plugin cache, because QGIS tries to load it as a plugin
34 | os.remove(plugin_path / "sources.list")
35 | shutil.rmtree(plugin_path / ".cache_qgis_plugin_manager")
36 |
37 |
38 | if __name__ == "__main__":
39 | print(f"Installing QGIS plugin {sys.argv[1]} to profile {sys.argv[2]}")
40 | install_qgis_plugin(sys.argv[1], sys.argv[2])
41 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
3 | {
4 | "name": "ci env ubuntu",
5 | "build": {
6 | "dockerfile": "dockerfile",
7 | "context": ".."
8 | },
9 | "features": {
10 | "ghcr.io/julialang/devcontainer-features/julia:1": {
11 | "channel": "release"
12 | }
13 | },
14 | "customizations": {
15 | "vscode": {
16 | "extensions": [
17 | "GitHub.copilot",
18 | "julialang.language-julia",
19 | "ms-python.python",
20 | "ms-python.mypy-type-checker",
21 | "charliermarsh.ruff",
22 | "njpwerner.autodocstring",
23 | "quarto.quarto",
24 | "tamasfe.even-better-toml",
25 | "samuelcolvin.jinjahtml",
26 | "yy0931.vscode-sqlite3-editor",
27 | "streetsidesoftware.code-spell-checker",
28 | "ms-toolsai.jupyter"
29 | ]
30 | }
31 | },
32 | "postCreateCommand": "pixi run install && pixi run generate-testmodels",
33 | "postStartCommand": "pixi run initialize-julia"
34 | }
35 |
--------------------------------------------------------------------------------
/ribasim_qgis/metadata.txt:
--------------------------------------------------------------------------------
1 |
2 | # This file contains metadata for your plugin.
3 |
4 | # This file should be included when you package your plugin.# Mandatory items:
5 |
6 | [general]
7 | name=Ribasim
8 | qgisMinimumVersion=3.34
9 | description=QGIS plugin to inspect Ribasim models
10 | version=2026.1.0-rc1
11 | author=Deltares
12 | email=ribasim.info@deltares.nl
13 |
14 | about=QGIS plugin to inspect Ribasim models. Requires `pandas` to be installed in the QGIS Python environment.
15 |
16 | tracker=https://github.com/Deltares/Ribasim/issues
17 | repository=https://github.com/Deltares/Ribasim
18 | # End of mandatory metadata
19 |
20 | # Recommended items:
21 |
22 | hasProcessingProvider=no
23 | changelog=https://ribasim.org/changelog
24 |
25 | # Tags are comma separated with spaces allowed
26 | tags=hydrology, water, water resources, model, julia
27 |
28 | homepage=https://ribasim.org/guide/qgis
29 | category=Plugins
30 | icon=icon.png
31 | # experimental flag
32 | experimental=False
33 |
34 | # deprecated flag (applies to the whole plugin, not just a single version)
35 | deprecated=False
36 |
37 | # Since QGIS 3.8, a comma separated list of plugins to be installed
38 | # (or upgraded) can be specified.
39 | # Check the documentation for more information.
40 | plugin_dependencies=iMOD==0.5.3
41 |
42 | Category of the plugin: Raster, Vector, Database or Web
43 | # category=
44 |
45 | # If the plugin can run on QGIS Server.
46 | server=False
47 |
--------------------------------------------------------------------------------
/utils/templates/schemas.py.jinja:
--------------------------------------------------------------------------------
1 | # Automatically generated file. Do not modify.
2 |
3 | from collections.abc import Callable
4 | from typing import Any
5 |
6 | import numpy as np
7 | import pandas as pd
8 | import pandera.pandas as pa
9 | from pandera.dtypes import Int32
10 | from pandera.typing import Index
11 |
12 | from ribasim import migrations
13 |
14 | class _BaseSchema(pa.DataFrameModel):
15 | class Config:
16 | add_missing_columns = True
17 | coerce = True
18 |
19 | @classmethod
20 | def _index_name(cls) -> str:
21 | return "fid"
22 |
23 | @pa.dataframe_parser
24 | @classmethod
25 | def _name_index(cls, df):
26 | df.index.name = cls._index_name()
27 | return df
28 |
29 | @classmethod
30 | def migrate(cls, df: Any, schema_version: int) -> Any:
31 | f: Callable[[Any, Any], Any] = getattr(
32 | migrations, str(cls.__name__).lower() + "_migration", lambda x, _: x
33 | )
34 | return f(df, schema_version)
35 |
36 | {% for m in models %}
37 | class {{m[:name]}}Schema(_BaseSchema):
38 | fid: Index[Int32] = pa.Field(default=1, check_name=True, coerce=True)
39 | {% for f in m[:fields] %}
40 | {% if (f[1] == :node_id) %}
41 | {{ f[1] }}: {{ f[2] }} = pa.Field(nullable={{ f[3] }}, default=0)
42 | {% else %}
43 | {{ f[1] }}: {{ f[2] }} = pa.Field(nullable={{ f[3] }})
44 | {% end %}
45 | {% end %}
46 | {% end %}
47 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/nodes/basin.py:
--------------------------------------------------------------------------------
1 | from ribasim.geometry import BasinAreaSchema
2 | from ribasim.input_base import SpatialTableModel, TableModel
3 | from ribasim.schemas import (
4 | BasinConcentrationExternalSchema,
5 | BasinConcentrationSchema,
6 | BasinConcentrationStateSchema,
7 | BasinProfileSchema,
8 | BasinStateSchema,
9 | BasinStaticSchema,
10 | BasinSubgridSchema,
11 | BasinSubgridTimeSchema,
12 | BasinTimeSchema,
13 | )
14 |
15 | __all__ = [
16 | "Area",
17 | "Concentration",
18 | "Profile",
19 | "State",
20 | "Static",
21 | "Subgrid",
22 | "SubgridTime",
23 | "Time",
24 | ]
25 |
26 |
27 | class Static(TableModel[BasinStaticSchema]):
28 | pass
29 |
30 |
31 | class Time(TableModel[BasinTimeSchema]):
32 | pass
33 |
34 |
35 | class State(TableModel[BasinStateSchema]):
36 | pass
37 |
38 |
39 | class Profile(TableModel[BasinProfileSchema]):
40 | pass
41 |
42 |
43 | class Subgrid(TableModel[BasinSubgridSchema]):
44 | pass
45 |
46 |
47 | class SubgridTime(TableModel[BasinSubgridTimeSchema]):
48 | pass
49 |
50 |
51 | class Area(SpatialTableModel[BasinAreaSchema]):
52 | pass
53 |
54 |
55 | class Concentration(TableModel[BasinConcentrationSchema]):
56 | pass
57 |
58 |
59 | class ConcentrationExternal(TableModel[BasinConcentrationExternalSchema]):
60 | pass
61 |
62 |
63 | class ConcentrationState(TableModel[BasinConcentrationStateSchema]):
64 | pass
65 |
--------------------------------------------------------------------------------
/utils/generate-testmodels.py:
--------------------------------------------------------------------------------
1 | import multiprocessing
2 | import shutil
3 | import sys
4 | from functools import partial
5 | from pathlib import Path
6 |
7 | import ribasim_testmodels
8 |
9 | selection = (
10 | sys.argv[1:] if len(sys.argv) > 1 else ribasim_testmodels.constructors.keys()
11 | )
12 |
13 |
14 | def generate_model(args, datadir):
15 | model_name, model_constructor = args
16 | model = model_constructor()
17 | model.write(datadir / model_name / "ribasim.toml")
18 | return model_name
19 |
20 |
21 | if __name__ == "__main__":
22 | datadir = Path("generated_testmodels")
23 | # Don't remove all models if we only (re)generate a subset
24 | if datadir.is_dir() and len(sys.argv) == 0:
25 | shutil.rmtree(datadir, ignore_errors=True)
26 |
27 | datadir.mkdir(exist_ok=True)
28 | readme = datadir / "README.md"
29 | readme.write_text(
30 | """\
31 | # Ribasim testmodels
32 |
33 | The content of this directory are generated testmodels for Ribasim
34 | Don't put important stuff in here, it will be emptied for every run."""
35 | )
36 |
37 | generate_model_partial = partial(generate_model, datadir=datadir)
38 |
39 | models = [
40 | (k, v) for k, v in ribasim_testmodels.constructors.items() if k in selection
41 | ]
42 |
43 | with multiprocessing.Pool(processes=4) as p:
44 | for model_name in p.imap_unordered(generate_model_partial, models):
45 | print(f"Generated {model_name}")
46 |
--------------------------------------------------------------------------------
/.github/workflows/core_testmodels.yml:
--------------------------------------------------------------------------------
1 | name: Julia Run Testmodels
2 | on:
3 | push:
4 | branches: [main]
5 | paths: ["core/**", "python/**", "pixi.toml", "pixi.lock", "Project.toml", "Manifest.toml"]
6 | tags: ["*"]
7 | pull_request:
8 | paths: ["core/**", "python/**", "pixi.toml", "pixi.lock", "Project.toml", "Manifest.toml"]
9 | merge_group:
10 | workflow_dispatch:
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: true
14 | # needed to allow julia-actions/cache to delete old caches that it has created
15 | permissions:
16 | actions: write
17 | contents: read
18 | jobs:
19 | test:
20 | name: Julia ${{ matrix.os }} - ${{ matrix.arch }}
21 | runs-on: ${{ matrix.os }}
22 | timeout-minutes: 60
23 | strategy:
24 | fail-fast: false
25 | matrix:
26 | os:
27 | - ubuntu-latest
28 | # https://github.com/Deltares/Ribasim/issues/825
29 | # - macOS-latest
30 | - windows-latest
31 | arch:
32 | - x64
33 | steps:
34 | - uses: actions/checkout@v6
35 | - uses: julia-actions/cache@v2
36 | - uses: prefix-dev/setup-pixi@v0.9.3
37 | with:
38 | pixi-version: "latest"
39 | - name: Prepare pixi
40 | run: pixi run install-ci
41 | - name: Run testmodels with Ribasim Core
42 | run: |
43 | pixi run ribasim-core-testmodels
44 | env:
45 | JULIA_NUM_THREADS: 4
46 |
--------------------------------------------------------------------------------
/core/test/data/allocation_problems/level_demand/allocation_problem_3.lp:
--------------------------------------------------------------------------------
1 | minimize
2 | obj:
3 | subject to
4 | storage_constraint_lower_Basin_#7,1_: 1 basin_storage_change_Basin_#7_ + 1 level_demand_error_Basin_#7,1,lower,first_ >= -250
5 | storage_constraint_upper_Basin_#7,1_: -1 basin_storage_change_Basin_#7_ + 1 level_demand_error_Basin_#7,1,upper,first_ >= -750
6 | level_demand_fairness_error_constraint_Basin_#7,1,lower_: -0.001 level_demand_error_Basin_#7,1,lower,first_ + 1 level_demand_error_Basin_#7,1,lower,second_ + 1 average_storage_unit_error_1,lower_ >= 0
7 | level_demand_fairness_error_constraint_Basin_#7,1,upper_: -0.001 level_demand_error_Basin_#7,1,upper,first_ + 1 level_demand_error_Basin_#7,1,upper,second_ + 1 average_storage_unit_error_1,upper_ >= 0
8 | volume_conservation_Basin_#7_: 1 basin_storage_change_Basin_#7_ + 1 low_storage_factor_Basin_#7_ = 1
9 | average_storage_unit_error_constraint_1,upper_: -1 level_demand_error_Basin_#7,1,upper,first_ + 1000 average_storage_unit_error_1,upper_ = 0
10 | average_storage_unit_error_constraint_1,lower_: -1 level_demand_error_Basin_#7,1,lower,first_ + 1000 average_storage_unit_error_1,lower_ = 0
11 | Bounds
12 | -0.2 <= basin_storage_change_Basin_#7_ <= 0.8
13 | 0 <= low_storage_factor_Basin_#7_ <= 1
14 | level_demand_error_Basin_#7,1,lower,first_ >= 0
15 | level_demand_error_Basin_#7,1,lower,second_ >= 0
16 | level_demand_error_Basin_#7,1,upper,first_ >= 0
17 | level_demand_error_Basin_#7,1,upper,second_ >= 0
18 | average_storage_unit_error_1,lower_ >= 0
19 | average_storage_unit_error_1,upper_ >= 0
20 | End
21 |
--------------------------------------------------------------------------------
/ribasim_qgis/core/arrow.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 |
3 |
4 | def postprocess_concentration_arrow(df: pd.DataFrame) -> pd.DataFrame:
5 | """Postprocess the concentration arrow data to a wide format."""
6 | ndf = pd.pivot_table(df, columns="substance", index=["time", "node_id"])
7 | ndf.columns = ndf.columns.droplevel(0)
8 | ndf.reset_index("node_id", inplace=True)
9 | return ndf
10 |
11 |
12 | def postprocess_allocation_arrow(df: pd.DataFrame) -> pd.DataFrame:
13 | """Postprocess the allocation arrow data to a wide format by summing over priorities."""
14 | ndf = df.groupby(["time", "node_id"]).aggregate(
15 | {"demand": "sum", "allocated": "sum", "realized": "sum"}
16 | )
17 | ndf.reset_index("node_id", inplace=True)
18 | return ndf
19 |
20 |
21 | def postprocess_allocation_flow_arrow(df: pd.DataFrame) -> pd.DataFrame:
22 | """Postprocess the allocation flow arrow data to a wide format by summing over priorities."""
23 | ndf = df.groupby(["time", "link_id"]).aggregate({"flow_rate": "sum"})
24 | # Drop Basin to Basin flows, as we can't join/visualize them
25 | ndf.drop(ndf[ndf.index.get_level_values("link_id") == 0].index, inplace=True)
26 | ndf.reset_index("link_id", inplace=True)
27 | return ndf
28 |
29 |
30 | def postprocess_flow_arrow(df: pd.DataFrame) -> pd.DataFrame:
31 | """Postprocess the allocation flow arrow data to a wide format by summing over priorities."""
32 | ndf = df.set_index(pd.DatetimeIndex(df["time"]))
33 | ndf.drop(columns=["time", "from_node_id", "to_node_id"], inplace=True)
34 | return ndf
35 |
--------------------------------------------------------------------------------
/utils/update-manifest.jl:
--------------------------------------------------------------------------------
1 | import Pkg
2 |
3 | const IS_INSTALLED = r"\s*Installed (.+)"
4 | const DETAILS_BEGIN = """
5 |
6 |
7 | All package versions
8 |
9 |
10 | ```
11 | """
12 |
13 | """
14 | Update the Julia Manifest.toml and show the changes as well as outdated packages.
15 | The output is written to a file that can be used as the body of a pull request.
16 | """
17 | function (@main)(_)
18 | path = normpath(@__DIR__, "../.pixi/update-manifest-julia.md")
19 | redirect_stdio(; stdout = path, stderr = path) do
20 | println("Update the Julia Manifest.toml to get the latest dependencies.\n")
21 | println("__Changed packages__\n```")
22 | Pkg.update()
23 | println("```\n\n__Packages still outdated after update__\n```")
24 | Pkg.status(; outdated = true)
25 | println("```")
26 | end
27 |
28 | # The Pkg.update output first prints all installed package versions.
29 | # This is a lot, strip it out, sort it, and put it in a details tag at the end.
30 | installed_lines = String[]
31 | lines = readlines(path)
32 | open(path, "w") do io
33 | for line in lines
34 | m = match(IS_INSTALLED, line)
35 | if m === nothing
36 | println(io, line)
37 | else
38 | push!(installed_lines, only(m.captures))
39 | end
40 | end
41 |
42 | println(io, DETAILS_BEGIN)
43 | sort!(installed_lines)
44 | foreach(line -> println(io, line), installed_lines)
45 | println("```\n\n ")
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/.teamcity/Testbench/IntegrationTestHWS.kt:
--------------------------------------------------------------------------------
1 | package Testbench.IntegrationTestHWS
2 |
3 | import Ribasim_Linux.Linux_BuildRibasim
4 | import Ribasim_Windows.Windows_BuildRibasim
5 | import Templates.*
6 | import jetbrains.buildServer.configs.kotlin.BuildType
7 | import jetbrains.buildServer.configs.kotlin.Project
8 | import jetbrains.buildServer.configs.kotlin.triggers.schedule
9 |
10 | object IntegrationTestHWS : Project ({
11 | id("IntegrationTestHWS")
12 | name = "IntegrationTestHWS"
13 |
14 | buildType(IntegrationTest_Windows)
15 | buildType(IntegrationTest_Linux)
16 |
17 | template(IntegrationTestWindows)
18 | template(IntegrationTestLinux)
19 | })
20 |
21 | object IntegrationTest_Windows : BuildType({
22 | name = "IntegrationTestWindows"
23 | templates(WindowsAgent, GithubCommitStatusIntegration, IntegrationTestWindows)
24 |
25 | triggers{
26 | schedule {
27 | id = ""
28 | schedulingPolicy = daily {
29 | hour = 0
30 | }
31 |
32 | branchFilter = "+:"
33 | triggerBuild = always()
34 | withPendingChangesOnly = true
35 | }
36 | }
37 | })
38 |
39 | object IntegrationTest_Linux : BuildType({
40 | name = "IntegrationTestLinux"
41 | templates(LinuxAgent, GithubCommitStatusIntegration, IntegrationTestLinux)
42 |
43 | triggers{
44 | schedule {
45 | id = ""
46 | schedulingPolicy = daily {
47 | hour = 0
48 | }
49 |
50 | branchFilter = "+:"
51 | triggerBuild = always()
52 | withPendingChangesOnly = true
53 | }
54 | }
55 | })
--------------------------------------------------------------------------------
/core/test/data/allocation_problems/allocation_control/allocation_problem_1.lp:
--------------------------------------------------------------------------------
1 | minimize
2 | obj:
3 | subject to
4 | user_demand_relative_error_constraint_UserDemand_#4,1_: 1 user_demand_allocated_UserDemand_#4,1_ + 2 user_demand_error_UserDemand_#4,1,first_ >= 2
5 | user_demand_fairness_error_constraint_UserDemand_#4,1_: -1 user_demand_error_UserDemand_#4,1,first_ + 1 user_demand_error_UserDemand_#4,1,second_ + 1 average_flow_unit_error_1_ >= 0
6 | flow_conservation_outlet_Outlet_#2_: 1 flow_(LevelBoundary_#1,_Outlet_#2)_ - 1 flow_(Outlet_#2,_Basin_#3)_ = 0
7 | volume_conservation_Basin_#3_: 1 basin_storage_change_Basin_#3_ + 1 low_storage_factor_Basin_#3_ - 1 flow_(Outlet_#2,_Basin_#3)_ + 1 flow_(Basin_#3,_UserDemand_#4)_ - 1 flow_(UserDemand_#4,_Basin_#3)_ = 1
8 | user_demand_allocated_sum_constraint_UserDemand_#4_: 1 flow_(Basin_#3,_UserDemand_#4)_ - 1 user_demand_allocated_UserDemand_#4,1_ = 0
9 | user_demand_return_flow_UserDemand_#4_: -0.5 flow_(Basin_#3,_UserDemand_#4)_ + 1 flow_(UserDemand_#4,_Basin_#3)_ = 0
10 | average_flow_unit_error_constraint_1_: -1 user_demand_error_UserDemand_#4,1,first_ + 1 average_flow_unit_error_1_ = 0
11 | Bounds
12 | -0.2 <= basin_storage_change_Basin_#3_ <= 0.8
13 | 0 <= low_storage_factor_Basin_#3_ <= 1
14 | 0 <= flow_(LevelBoundary_#1,_Outlet_#2)_ <= 155.52
15 | 0 <= flow_(Outlet_#2,_Basin_#3)_ <= 155.52
16 | 0 <= flow_(Basin_#3,_UserDemand_#4)_ <= 8640000
17 | 0 <= flow_(UserDemand_#4,_Basin_#3)_ <= 8640000
18 | boundary_level_LevelBoundary_#1_ = 0
19 | 0 <= user_demand_allocated_UserDemand_#4,1_ <= 2
20 | 0 <= user_demand_error_UserDemand_#4,1,first_ <= 1
21 | 0 <= user_demand_error_UserDemand_#4,1,second_ <= 1
22 | 0 <= average_flow_unit_error_1_ <= 1
23 | End
24 |
--------------------------------------------------------------------------------
/.teamcity/Ribasim/buildTypes/GenerateCache.kt:
--------------------------------------------------------------------------------
1 | package Ribasim.buildTypes
2 |
3 | import Templates.GithubCommitStatusIntegration
4 | import Templates.WindowsAgent
5 | import Templates.LinuxAgent
6 | import Templates.GenerateCacheWindows
7 | import Templates.GenerateCacheLinux
8 | import jetbrains.buildServer.configs.kotlin.*
9 | import jetbrains.buildServer.configs.kotlin.buildSteps.script
10 | import jetbrains.buildServer.configs.kotlin.triggers.vcs
11 |
12 | object Windows_GenerateCache : BuildType({
13 | templates(WindowsAgent, GithubCommitStatusIntegration, GenerateCacheWindows)
14 | name = "Generate Windows TC cache"
15 |
16 | triggers {
17 | vcs {
18 | id = "TRIGGER_RIBA_W1"
19 | triggerRules = """
20 | +:root=Ribasim_Ribasim:/Manifest.toml
21 | +:root=Ribasim_Ribasim:/Project.toml
22 | +:root=Ribasim_Ribasim:/pixi.lock
23 | +:root=Ribasim_Ribasim:/pixi.toml
24 | """.trimIndent()
25 | branchFilter = "+:"
26 | }
27 | }
28 | })
29 |
30 | object Linux_GenerateCache : BuildType({
31 | templates(LinuxAgent, GithubCommitStatusIntegration, GenerateCacheLinux)
32 | name = "Generate Linux TC cache"
33 |
34 | triggers {
35 | vcs {
36 | id = "TRIGGER_RIBA_L1"
37 | triggerRules = """
38 | +:root=Ribasim_Ribasim:/Manifest.toml
39 | +:root=Ribasim_Ribasim:/Project.toml
40 | +:root=Ribasim_Ribasim:/pixi.lock
41 | +:root=Ribasim_Ribasim:/pixi.toml
42 | """.trimIndent()
43 | branchFilter = "+:"
44 | }
45 | }
46 | })
47 |
--------------------------------------------------------------------------------
/docs/reference/node/flow-demand.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "FlowDemand"
3 | ---
4 |
5 | A `FlowDemand` node associates a non-consuming flow demand to a connector node (e.g. `Pump`, `TabulatedRatingCurve`) for one or more demand priorities.
6 | FlowDemand nodes can set a flow demand only for a single connector node.
7 | FlowDemand nodes do nothing when allocation is not activated, except when they are connected to a Pump or an Outlet. In that case the flow demand flow rate is taken as the minimum flow through that Pump or Outlet.
8 |
9 | # Tables
10 |
11 | ## Static
12 |
13 | column | type | unit | restriction
14 | --------------- | -------- | --------------------- | -----------
15 | node_id | Int32 | - |
16 | demand_priority | Int32 | - | positive
17 | demand | Float64 | $\text{m}^3/\text{s}$ | non-negative
18 |
19 | ## Time
20 |
21 | This table is the transient form of the `FlowDemand` table, in which a time-dependent demand can be supplied.
22 | With this the demand can be updated over time. In between the given times the
23 | demand is block interpolated (forward fill), and outside the demand is constant given by the
24 | nearest time value. The allocation algorithm evaluates the interpolation at the start of the allocation time step.
25 |
26 | column | type | unit | restriction
27 | --------------- | -------- | --------------------- | -----------
28 | node_id | Int32 | - |
29 | time | DateTime | - |
30 | demand_priority | Int32 | - | positive
31 | demand | Float64 | $\text{m}^3/\text{s}$ | non-negative
32 |
--------------------------------------------------------------------------------
/.teamcity/Templates/GenerateCache.kt:
--------------------------------------------------------------------------------
1 | package Templates
2 |
3 | import Ribasim.vcsRoots.Ribasim
4 | import jetbrains.buildServer.configs.kotlin.Template
5 | import jetbrains.buildServer.configs.kotlin.buildSteps.script
6 | import jetbrains.buildServer.configs.kotlin.*
7 |
8 | fun generateJuliaDepotPath(platformOs: String): String {
9 | if (platformOs == "Linux") {
10 | return "%teamcity.build.checkoutDir%/.julia:"
11 | } else {
12 | return "%teamcity.build.checkoutDir%/.julia;"
13 | }
14 | }
15 |
16 | open class GenerateCache(platformOs: String) : Template() {
17 | init {
18 | name = "GenerateCache${platformOs}_Template"
19 |
20 | artifactRules = """%teamcity.build.checkoutDir%/.julia => cache.zip"""
21 | publishArtifacts = PublishMode.SUCCESSFUL
22 |
23 | vcs {
24 | root(Ribasim, ". => ribasim")
25 | cleanCheckout = true
26 | }
27 |
28 | val depot_path = generateJuliaDepotPath(platformOs)
29 | params {
30 | param("env.JULIA_DEPOT_PATH", depot_path)
31 | }
32 |
33 | val header = generateTestBinariesHeader(platformOs)
34 | steps {
35 | script {
36 | name = "Set up pixi"
37 | id = "Set_up_pixi"
38 | workingDir = "ribasim"
39 | scriptContent = header +
40 | """
41 | pixi --version
42 | pixi run install-ci
43 | pixi run initialize-julia-test
44 | """.trimIndent()
45 | }
46 | }
47 | }
48 | }
49 |
50 | object GenerateCacheWindows : GenerateCache("Windows")
51 | object GenerateCacheLinux : GenerateCache("Linux")
52 |
--------------------------------------------------------------------------------
/core/test/main_test.jl:
--------------------------------------------------------------------------------
1 | @testitem "main output" begin
2 | using IOCapture: capture
3 | import TOML
4 | using Ribasim: Config, results_path
5 |
6 | model_path = normpath(@__DIR__, "../../generated_testmodels/basic/")
7 | toml_path = normpath(model_path, "ribasim.toml")
8 |
9 | # change the ribasim_version in the toml file to check warning
10 | toml_dict = TOML.parsefile(toml_path)
11 | toml_dict["ribasim_version"] = "a_different_version"
12 | open(toml_path, "w") do io
13 | TOML.print(io, toml_dict)
14 | end
15 |
16 | @test ispath(toml_path)
17 | (; value, output, error, backtrace) = capture() do
18 | Ribasim.main(toml_path)
19 | end
20 | @test value == 0
21 | if value != 0
22 | @show output
23 | @show error
24 | @show backtrace
25 | end
26 | @test occursin("version in the TOML config file does not match", output)
27 | end
28 |
29 | @testitem "main error logging" begin
30 | using IOCapture: capture
31 | import TOML
32 | using Ribasim: Config, results_path
33 |
34 | model_path = normpath(@__DIR__, "../../generated_testmodels/invalid_link_types/")
35 | toml_path = normpath(model_path, "ribasim.toml")
36 |
37 | @test ispath(toml_path)
38 | (; value, output) = capture() do
39 | Ribasim.main(toml_path)
40 | end
41 | @test value == 1
42 |
43 | # Stacktraces should be written to both the terminal and log file.
44 | @test occursin("\nStacktrace:\n", output)
45 | config = Config(toml_path)
46 | log_path = results_path(config, "ribasim.log")
47 | @test ispath(log_path)
48 | log_str = read(log_path, String)
49 | @test occursin("\nStacktrace:\n", log_str)
50 | end
51 |
--------------------------------------------------------------------------------
/python/ribasim/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "ribasim"
7 | description = "Pre- and post-process Ribasim"
8 | readme = "README.md"
9 | authors = [
10 | { name = "Deltares and contributors", email = "ribasim.info@deltares.nl" },
11 | ]
12 | license = { text = "MIT" }
13 | classifiers = [
14 | "Intended Audience :: Science/Research",
15 | "Topic :: Scientific/Engineering :: Hydrology",
16 | ]
17 | requires-python = ">=3.12"
18 | dependencies = [
19 | "datacompy >=0.16",
20 | "geopandas >=1.0",
21 | "matplotlib >=3.9",
22 | "netCDF4 >=1.7.1",
23 | "numpy >=2.0",
24 | "packaging >=23.0",
25 | "pandas >=2.2",
26 | "pandera >=0.25",
27 | "pyarrow >=17.0",
28 | "pydantic >=2.0",
29 | "pyogrio >=0.8",
30 | "shapely >=2.0",
31 | "tomli >=2.0",
32 | "tomli-w >=1.0",
33 | "xarray >=2023.11",
34 | ]
35 | dynamic = ["version"]
36 |
37 | [project.optional-dependencies]
38 | tests = [
39 | "pytest",
40 | "pytest-xdist",
41 | "pytest-cov",
42 | "ribasim_testmodels",
43 | "teamcity-messages",
44 | ]
45 | netcdf = ["xugrid"]
46 | delwaq = ["jinja2", "networkx", "ribasim[netcdf]"]
47 | all = ["ribasim[tests]", "ribasim[netcdf]", "ribasim[delwaq]"]
48 |
49 | [project.urls]
50 | Documentation = "https://ribasim.org/"
51 | Source = "https://github.com/Deltares/Ribasim"
52 |
53 | [tool.hatch.version]
54 | path = "ribasim/__init__.py"
55 |
56 | [tool.hatch.build.targets.sdist]
57 | artifacts = ["delwaq/reference/*", "delwaq/template/*"]
58 |
59 | [tool.pytest.ini_options]
60 | markers = [
61 | "regression: Older models that are not on the current database schema.",
62 | ]
63 |
--------------------------------------------------------------------------------
/python/ribasim/tests/test_link.py:
--------------------------------------------------------------------------------
1 | import geopandas as gpd
2 | import pytest
3 | import shapely.geometry as sg
4 | from pydantic import ValidationError
5 | from ribasim.geometry.link import LinkTable, NodeData
6 |
7 |
8 | @pytest.fixture(scope="session")
9 | def link() -> LinkTable:
10 | a = (0.0, 0.0)
11 | b = (0.0, 1.0)
12 | c = (0.2, 0.5)
13 | d = (1.0, 1.0)
14 | geometry = [sg.LineString([a, b, c]), sg.LineString([a, d])]
15 | df = gpd.GeoDataFrame(
16 | data={"link_id": [0, 1], "from_node_id": [1, 1], "to_node_id": [2, 3]},
17 | geometry=geometry,
18 | )
19 | df.set_index("link_id", inplace=True)
20 | link = LinkTable(df=df)
21 | return link
22 |
23 |
24 | def test_validation(link):
25 | assert isinstance(link, LinkTable)
26 |
27 | with pytest.raises(ValidationError):
28 | df = gpd.GeoDataFrame(
29 | data={
30 | "link_id": [0, 1],
31 | "from_node_id": [1, 1],
32 | "to_node_id": ["foo", 3],
33 | }, # None is coerced to 0 without errors
34 | geometry=[None, None],
35 | )
36 | df.set_index("link_id", inplace=True)
37 | LinkTable(df=df)
38 |
39 |
40 | def test_link_plot(link):
41 | link.plot()
42 |
43 |
44 | def test_link_indexing(link):
45 | with pytest.raises(NotImplementedError):
46 | link[1]
47 |
48 |
49 | def test_invalid_retour_link(basic):
50 | with pytest.raises(ValueError, match="opposite link already exists"):
51 | basic.link.add(basic.manning_resistance[2], basic.basin[1])
52 |
53 |
54 | def test_node_data():
55 | node = NodeData(node_id=5, node_type="Pump", geometry=sg.Point(0, 0))
56 | assert repr(node) == "Pump #5"
57 |
--------------------------------------------------------------------------------
/docs/reference/node/junction.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Junction"
3 | ---
4 |
5 | A Junction node allows explicitly representing confluences and bifurcations in the network. It doesn't introduce new behavior but makes it easier to make the network layout recognizable.
6 |
7 | Junctions can connect to other Junctions, but are not allowed to form cycles. Note that a confluence Junction followed by bifurcation Junction is generally invalid, as it will connect a connector node with multiple basins.
8 |
9 |
10 | # Tables
11 |
12 | No tables are required for Junction nodes.
13 |
14 | # Equations
15 |
16 | Junctions connect all upstream nodes with all downstream nodes,
17 | and are not used in the equations themselves.
18 |
19 | # Examples
20 |
21 | This testmodel (juction_combined) with Junctions
22 | ```{mermaid}
23 | flowchart LR
24 | C{Boundary} --> E[/Junction\]
25 | E --> F((Basin))
26 | E --> G((Basin))
27 | F --> H{Connector}
28 | G --> I{Connector}
29 | H --> J[/Junction\]
30 | I --> J
31 | J --> K((Basin))
32 | ```
33 |
34 | translates to the following model:
35 |
36 | ```{mermaid}
37 | flowchart LR
38 | C{Boundary} --> F((Basin))
39 | C --> G((Basin))
40 | F --> H{Connector}
41 | G --> I{Connector}
42 | H --> K((Basin))
43 | I --> K
44 | ```
45 |
46 | And this testmodel (junction_chained) with Junctions
47 |
48 | ```{mermaid}
49 | flowchart LR
50 | A{Connector} --> D[/Junction\]
51 | B{Connector} --> D[/Junction\]
52 | C{Connector} --> E[/Junction\]
53 | D --> E
54 | E --> F((Basin))
55 | ```
56 |
57 | translates to the following model:
58 |
59 | ```{mermaid}
60 | flowchart LR
61 | A{Connector} --> F((Basin))
62 | B{Connector} --> F
63 | C{Connector} --> F
64 | ```
65 |
--------------------------------------------------------------------------------
/core/test/libribasim_test.jl:
--------------------------------------------------------------------------------
1 | @testitem "libribasim" begin
2 | toml_path = normpath(@__DIR__, "../../generated_testmodels/basic/ribasim.toml")
3 |
4 | # data from which we create pointers for libribasim
5 | time = [-1.0]
6 | var_name = "basin.storage"
7 | type = ones(UInt8, 8)
8 |
9 | GC.@preserve time var_name type toml_path begin
10 | var_name_ptr = Base.unsafe_convert(Cstring, var_name)
11 | time_ptr = pointer(time)
12 | type_ptr = Cstring(pointer(type))
13 | toml_path_ptr = Base.unsafe_convert(Cstring, toml_path)
14 |
15 | # safe to finalize uninitialized model
16 | @test libribasim.model === nothing
17 | @test libribasim.finalize() == 0
18 | @test libribasim.model === nothing
19 |
20 | # cannot get time of uninitialized model
21 | @test libribasim.last_error_message == ""
22 | retcode = libribasim.get_current_time(time_ptr)
23 | @test retcode == 1
24 | @test time[1] == -1
25 | @test libribasim.last_error_message == "Model not initialized"
26 |
27 | @test libribasim.initialize(toml_path_ptr) == 0
28 | @test libribasim.model isa Ribasim.Model
29 | @test libribasim.model.integrator.t == 0.0
30 | @test libribasim.update_retcode(libribasim.model) == 1
31 |
32 | @test libribasim.get_current_time(time_ptr) == 0
33 | @test time[1] == 0.0
34 |
35 | @test libribasim.get_var_type(var_name_ptr, type_ptr) == 0
36 | @test unsafe_string(type_ptr) == "double"
37 |
38 | @test libribasim.update() == 0
39 | @test libribasim.update_retcode(libribasim.model) == 0
40 |
41 | @test libribasim.finalize() == 0
42 | @test libribasim.model === nothing
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/.teamcity/Testbench/RegressionTestODESolve.kt:
--------------------------------------------------------------------------------
1 | package Testbench.RegressionTestODESolve
2 |
3 | import Ribasim_Windows.Windows_BuildRibasim
4 | import Ribasim_Linux.Linux_BuildRibasim
5 | import Templates.*
6 | import jetbrains.buildServer.configs.kotlin.BuildType
7 | import jetbrains.buildServer.configs.kotlin.Project
8 | import jetbrains.buildServer.configs.kotlin.matrix
9 | import jetbrains.buildServer.configs.kotlin.triggers.schedule
10 | import jetbrains.buildServer.configs.kotlin.*
11 |
12 | object RegressionTestODESolve : Project({
13 | id("RegressionTestODE")
14 | name = "RegressionTestODE"
15 |
16 | buildType(RegressionTest_Windows)
17 | buildType(RegressionTest_Linux)
18 |
19 | template(RegressionTestWindows)
20 | template(RegressionTestLinux)
21 | })
22 |
23 | object RegressionTest_Windows : BuildType({
24 | name = "RegressionTestWindows"
25 | templates(WindowsAgent, GithubCommitStatusIntegration, RegressionTestWindows)
26 |
27 | triggers{
28 | schedule {
29 | id = ""
30 | schedulingPolicy = daily {
31 | hour = 0
32 | }
33 |
34 | branchFilter = "+:"
35 | triggerBuild = always()
36 | withPendingChangesOnly = true
37 | }
38 | }
39 | })
40 |
41 | object RegressionTest_Linux : BuildType({
42 | name = "RegressionTestLinux"
43 | templates(LinuxAgent, GithubCommitStatusIntegration, RegressionTestLinux)
44 |
45 | triggers{
46 | schedule {
47 | id = ""
48 | schedulingPolicy = daily {
49 | hour = 0
50 | }
51 |
52 | branchFilter = "+:"
53 | triggerBuild = always()
54 | withPendingChangesOnly = true
55 | }
56 | }
57 | })
--------------------------------------------------------------------------------
/utils/github-release.py:
--------------------------------------------------------------------------------
1 | import re
2 | import subprocess
3 |
4 |
5 | def git_describe() -> subprocess.CompletedProcess[str]:
6 | return subprocess.run(
7 | ["git", "describe", "--tags", "--exact-match"],
8 | capture_output=True,
9 | text=True,
10 | )
11 |
12 |
13 | def main(proc: subprocess.CompletedProcess[str]):
14 | # Get the name of the currently checked out tag
15 | tag_name = proc.stdout.strip()
16 |
17 | print(f"Currently checked out tag: {tag_name}")
18 |
19 | # Define regex patterns for valid tag names
20 | normal_pattern = r"^v20\d{2}\.[1-9]\.\d{1,2}$"
21 | prerelease_pattern = r"^v20\d{2}\.[1-9]\.\d{1,2}-rc[1-9]\d?$"
22 |
23 | is_normal = re.match(normal_pattern, tag_name)
24 | is_prerelease = re.match(prerelease_pattern, tag_name)
25 |
26 | if not (is_normal or is_prerelease):
27 | raise ValueError(f"Tag name '{tag_name}' does not match expected pattern.")
28 |
29 | # Build the command
30 | cmd = [
31 | "gh",
32 | "release",
33 | "create",
34 | tag_name,
35 | "--generate-notes",
36 | ]
37 |
38 | if is_prerelease:
39 | cmd.append("--prerelease")
40 |
41 | cmd.extend(
42 | [
43 | "ribasim_linux.zip",
44 | "ribasim_windows.zip",
45 | "ribasim_qgis.zip",
46 | "generated_testmodels.zip",
47 | ]
48 | )
49 |
50 | # Create a release using gh
51 | subprocess.check_call(cmd)
52 |
53 |
54 | if __name__ == "__main__":
55 | proc = git_describe()
56 | if proc.returncode == 0 and proc.stdout.startswith("v20"):
57 | main(proc)
58 | else:
59 | print("Current checkout is not a tag starting with 'v20', no release made.")
60 | print(proc.stderr)
61 |
--------------------------------------------------------------------------------
/utils/plot.jl:
--------------------------------------------------------------------------------
1 | # Utility functions to plot Ribasim results.
2 |
3 | using DataFrames: DataFrame
4 | using Makie: Figure, Axis, scatterlines!, axislegend
5 | using Ribasim: Ribasim, Model
6 |
7 | function plot_basin_data!(model::Model, ax::Axis, column::Symbol)
8 | basin_data = DataFrame(Ribasim.basin_data(model))
9 | for node_id in unique(basin_data.node_id)
10 | group = filter(:node_id => ==(node_id), basin_data)
11 | scatterlines!(ax, group.time, getproperty(group, column); label = "Basin #$node_id")
12 | end
13 |
14 | axislegend(ax)
15 | return nothing
16 | end
17 |
18 | function plot_basin_data(model::Model)
19 | f = Figure()
20 | ax1 = Axis(f[1, 1]; ylabel = "level [m]")
21 | ax2 = Axis(f[2, 1]; xlabel = "time", ylabel = "storage [m³]")
22 | plot_basin_data!(model, ax1, :level)
23 | plot_basin_data!(model, ax2, :storage)
24 | f
25 | end
26 |
27 | function plot_flow!(model::Model, ax::Axis, link_metadata::Ribasim.LinkMetadata)
28 | flow_table = DataFrame(Ribasim.flow_data(model))
29 | flow_table = filter(:link_id => ==(link_metadata.id), flow_table)
30 | label = "$(link_metadata.link[1]) → $(link_metadata.link[2])"
31 | scatterlines!(ax, flow_table.time, flow_table.flow_rate; label)
32 | return nothing
33 | end
34 |
35 | function plot_flow(model::Model; skip_conservative_out = true)
36 | f = Figure()
37 | ax = Axis(f[1, 1]; xlabel = "time", ylabel = "flow rate [m³s⁻¹]")
38 | for link_metadata in values(model.integrator.p.p_independent.graph.edge_data)
39 | if skip_conservative_out &&
40 | link_metadata.link[1].type in Ribasim.conservative_nodetypes
41 | continue
42 | end
43 | plot_flow!(model, ax, link_metadata)
44 | end
45 | axislegend(ax)
46 | f
47 | end
48 |
--------------------------------------------------------------------------------
/utils/s3_download.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from os import makedirs
3 |
4 | from minio import Minio
5 | from minio.error import S3Error
6 | from s3_settings import settings
7 |
8 | minioServer = "s3.deltares.nl"
9 | bucketName = "ribasim"
10 |
11 | parser = argparse.ArgumentParser(
12 | description="Download a folder (recursively) from the MinIO server"
13 | )
14 | parser.add_argument("remote", help="The path to download in the MinIO server")
15 | parser.add_argument("local", help="The path to the local file system")
16 | parser.add_argument(
17 | "--accesskey",
18 | help="The access key to access the MinIO server",
19 | default=settings.minio_access_key,
20 | )
21 | parser.add_argument(
22 | "--secretkey",
23 | help="The secret key to access the MinIO server",
24 | default=settings.minio_secret_key,
25 | )
26 | args = parser.parse_args()
27 | if (not args.accesskey) or (not args.secretkey):
28 | raise ValueError("No MinIO access key or secret key provided")
29 |
30 | client = Minio(minioServer, access_key=args.accesskey, secret_key=args.secretkey)
31 | objects = list(client.list_objects(bucketName, prefix=args.remote, recursive=True))
32 |
33 | if not objects:
34 | raise ValueError(f"Remote path '{args.remote}' does not exist or is empty.")
35 |
36 | for obj in objects:
37 | try:
38 | if obj.is_dir:
39 | local_dir = f"models/{args.local}" + obj.object_name.removeprefix(
40 | args.remote
41 | )
42 | makedirs(local_dir, exist_ok=True)
43 | else:
44 | client.fget_object(
45 | bucketName,
46 | obj.object_name,
47 | f"models/{args.local}" + obj.object_name.removeprefix(args.remote),
48 | )
49 | except S3Error as e:
50 | print(f"Error occurred: {e}")
51 |
--------------------------------------------------------------------------------
/core/test/data/allocation_problems/drain_surplus/allocation_problem_2.lp:
--------------------------------------------------------------------------------
1 | minimize
2 | obj:
3 | subject to
4 | storage_constraint_lower_Basin_#1,1_: 1 basin_storage_change_Basin_#1_ + 1 level_demand_error_Basin_#1,1,lower,first_ >= -250
5 | storage_constraint_upper_Basin_#1,1_: -1 basin_storage_change_Basin_#1_ + 1 level_demand_error_Basin_#1,1,upper,first_ >= -750
6 | level_demand_fairness_error_constraint_Basin_#1,1,lower_: -0.001 level_demand_error_Basin_#1,1,lower,first_ + 1 level_demand_error_Basin_#1,1,lower,second_ + 1 average_storage_unit_error_1,lower_ >= 0
7 | level_demand_fairness_error_constraint_Basin_#1,1,upper_: -0.001 level_demand_error_Basin_#1,1,upper,first_ + 1 level_demand_error_Basin_#1,1,upper,second_ + 1 average_storage_unit_error_1,upper_ >= 0
8 | flow_conservation_outlet_Outlet_#2_: 1 flow_(Basin_#1,_Outlet_#2)_ - 1 flow_(Outlet_#2,_Terminal_#3)_ = 0
9 | volume_conservation_Basin_#1_: 1 basin_storage_change_Basin_#1_ + 1 low_storage_factor_Basin_#1_ + 1 flow_(Basin_#1,_Outlet_#2)_ = 1
10 | average_storage_unit_error_constraint_1,upper_: -1 level_demand_error_Basin_#1,1,upper,first_ + 1000 average_storage_unit_error_1,upper_ = 0
11 | average_storage_unit_error_constraint_1,lower_: -1 level_demand_error_Basin_#1,1,lower,first_ + 1000 average_storage_unit_error_1,lower_ = 0
12 | Bounds
13 | -1.6666666666666667 <= basin_storage_change_Basin_#1_ <= 6.666666666666667
14 | 0 <= low_storage_factor_Basin_#1_ <= 1
15 | 0 <= flow_(Basin_#1,_Outlet_#2)_ <= 0.14400000000000002
16 | 0 <= flow_(Outlet_#2,_Terminal_#3)_ <= 0.14400000000000002
17 | level_demand_error_Basin_#1,1,lower,first_ >= 0
18 | level_demand_error_Basin_#1,1,lower,second_ >= 0
19 | level_demand_error_Basin_#1,1,upper,first_ >= 0
20 | level_demand_error_Basin_#1,1,upper,second_ >= 0
21 | average_storage_unit_error_1,lower_ >= 0
22 | average_storage_unit_error_1,upper_ >= 0
23 | End
24 |
--------------------------------------------------------------------------------
/python/ribasim/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import ribasim
3 | import ribasim_testmodels
4 |
5 |
6 | # we can't call fixtures directly, so we keep separate versions
7 | @pytest.fixture()
8 | def basic() -> ribasim.Model:
9 | return ribasim_testmodels.basic_model()
10 |
11 |
12 | @pytest.fixture()
13 | def basic_arrow() -> ribasim.Model:
14 | return ribasim_testmodels.basic_arrow_model()
15 |
16 |
17 | @pytest.fixture()
18 | def basic_transient() -> ribasim.Model:
19 | return ribasim_testmodels.basic_transient_model()
20 |
21 |
22 | @pytest.fixture()
23 | def bucket() -> ribasim.Model:
24 | return ribasim_testmodels.bucket_model()
25 |
26 |
27 | @pytest.fixture()
28 | def pid_control_equation() -> ribasim.Model:
29 | return ribasim_testmodels.pid_control_equation_model()
30 |
31 |
32 | @pytest.fixture()
33 | def tabulated_rating_curve() -> ribasim.Model:
34 | return ribasim_testmodels.tabulated_rating_curve_model()
35 |
36 |
37 | @pytest.fixture()
38 | def outlet() -> ribasim.Model:
39 | return ribasim_testmodels.outlet_model()
40 |
41 |
42 | @pytest.fixture()
43 | def backwater() -> ribasim.Model:
44 | return ribasim_testmodels.backwater_model()
45 |
46 |
47 | @pytest.fixture()
48 | def discrete_control_of_pid_control() -> ribasim.Model:
49 | return ribasim_testmodels.discrete_control_of_pid_control_model()
50 |
51 |
52 | @pytest.fixture()
53 | def level_range() -> ribasim.Model:
54 | return ribasim_testmodels.level_range_model()
55 |
56 |
57 | @pytest.fixture()
58 | def trivial() -> ribasim.Model:
59 | return ribasim_testmodels.trivial_model()
60 |
61 |
62 | @pytest.fixture()
63 | def tabulated_rating_curve_control() -> ribasim.Model:
64 | return ribasim_testmodels.tabulated_rating_curve_control_model()
65 |
66 |
67 | @pytest.fixture()
68 | def drought() -> ribasim.Model:
69 | return ribasim_testmodels.drought_model()
70 |
--------------------------------------------------------------------------------
/utils/testmodelrun.jl:
--------------------------------------------------------------------------------
1 | import Ribasim
2 |
3 | include("utils.jl")
4 |
5 | """
6 | Run all testmodels in parallel and check if they pass.
7 |
8 | A selection can be made by passing the name(s) of the individual testmodel(s) as (an) argument(s).
9 | """
10 | function main(ARGS)
11 | toml_paths = get_testmodels()
12 | if length(ARGS) > 0
13 | toml_paths = filter(x -> basename(dirname(x)) in ARGS, toml_paths)
14 | end
15 | n_model = length(toml_paths)
16 | n_pass = 0
17 | n_fail = 0
18 | lk = ReentrantLock()
19 | failed = fill("", length(toml_paths))
20 | skipped_allocation = fill("", length(toml_paths))
21 |
22 | Threads.@threads for i in eachindex(toml_paths)
23 | toml_path = toml_paths[i]
24 | modelname = basename(dirname(toml_path))
25 |
26 | if Ribasim.Config(toml_path).experimental.allocation
27 | skipped_allocation[i] = modelname
28 | continue
29 | end
30 |
31 | ret_code = Ribasim.main(toml_path)
32 |
33 | # Treat models starting with "invalid_" as expected to fail (non-zero ret_code means pass) :)
34 | if startswith(modelname, "invalid_")
35 | ret_code = ret_code == 0 ? 1 : 0
36 | end
37 |
38 | lock(lk) do
39 | if ret_code != 0
40 | failed[i] = modelname
41 | n_fail += 1
42 | else
43 | n_pass += 1
44 | end
45 | end
46 | end
47 |
48 | println("Ran $n_model models, $n_pass passed, $n_fail failed.\n")
49 | if length(skipped_allocation) > 0
50 | println("Skipped the following models that use allocation:")
51 | foreach(println, skipped_allocation)
52 | end
53 | if n_fail > 0
54 | println("Failed models:")
55 | foreach(println, failed)
56 | error("Model run failed")
57 | end
58 | end
59 |
60 | main(ARGS)
61 |
--------------------------------------------------------------------------------
/docs/dev/python.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Python tooling development"
3 | ---
4 |
5 | # Set up the developer environment
6 |
7 | ## Running the tests {#sec-test}
8 |
9 | In order to run tests on Ribasim Python execute
10 |
11 | ```sh
12 | pixi run test-ribasim-python
13 | ```
14 |
15 | ## Updating example notebooks
16 |
17 | Make sure to run `Clear All Outputs` on the notebook before committing.
18 |
19 | ## Prepare model input
20 |
21 | Before running the Julia tests or building binaries, example model input needs to created.
22 | This is done by running the following:
23 |
24 | ```sh
25 | pixi run generate-testmodels
26 | ```
27 |
28 | This places example model input files under `./generated_testmodels/`.
29 | If the example models change, re-run this script.
30 |
31 | ## Setup Visual Studio Code (optional) {#sec-vscode}
32 |
33 | Install the [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python), [ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) and [autoDocstring](https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring) extensions.
34 |
35 | ## Linting
36 |
37 | To run our linting suite locally, execute:
38 |
39 | ```sh
40 | pixi run lint
41 | ```
42 |
43 | # Code maintenance {#sec-codecov}
44 |
45 | For new features new tests have to be added. To monitor how much of the code is covered by the tests we use [Codecov](https://about.codecov.io/).
46 | For a simple overview of the local code coverage run
47 | ```sh
48 | pixi shell
49 | pytest --cov=ribasim tests/
50 | ```
51 | from `python/ribasim`. For an extensive overview in `html` format use
52 | ```sh
53 | pixi shell
54 | pytest --cov=ribasim --cov-report=html tests/
55 | ```
56 | which creates a folder `htmlcov` in the working directory. To see te contents open `htmlcov/index.html` in a browser.
57 |
58 | The code coverage of pushed branches can be seen [here](https://app.codecov.io/gh/Deltares/Ribasim).
59 |
--------------------------------------------------------------------------------
/docs/guide/debug.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Debugging models"
3 | ---
4 |
5 | ## Slow models
6 |
7 | When your model is slow, it's often only a handful of nodes that are hard to solve. If the model finishes or is interrupted, convergence bottlenecks are shown like so:
8 |
9 | ```julia
10 | ┌ Info: Convergence bottlenecks in descending order of severity:
11 | │ ManningResistance #251242 = 0.09023997405863035
12 | │ ManningResistance #70523 = 0.006218636603583534
13 | │ ManningResistance #251181 = 0.004716432403226626
14 | │ ManningResistance #251182 = 0.0035319514660666165
15 | └ ManningResistance #591558 = 0.003284110004804508
16 | ```
17 |
18 | It's best to inspect these nodes, and try to adjust the parametrization, or merge smaller nodes. You can find the convergence measure per node over time in the `flow.arrow` and `basin.arrow` [output files](/reference/usage.qmd#sec-results).
19 |
20 | To gain further insight into model performance, one can inspect the `solver_stats.arrow` output file, which gives the number of computations, number of rejected and accepted solutions, and the size of each calculation timestep.
21 |
22 | ## Unstable models
23 |
24 | When your model exits with a message like so:
25 | ```julia
26 | ┌ Error: The model exited at model time 2024-01-27T14:46:17.791 with return code Unstable. See https://docs.sciml.ai/DiffEqDocs/stable/basics/solution/#retcodes
27 | ```
28 |
29 | it's best to rerun the model with `saveat = 0` in the [solver](/reference/usage.qmd#sec-solver-settings) settings. The model might then instead exit with
30 |
31 | ```julia
32 | ┌ Error: Too large water balance error
33 | │ id = Basin #2
34 | │ balance_error = 0.0017985748886167501
35 | │ relative_error = 1.3503344464431657
36 | ```
37 |
38 | which helps you pin down the problematic node(s).
39 | The normal output for every calculation timestep is written until the moment of error, so one can use this information to understand the problem.
40 |
--------------------------------------------------------------------------------
/core/test/data/allocation_problems/linear_resistance_demand/allocation_problem_2.lp:
--------------------------------------------------------------------------------
1 | minimize
2 | obj:
3 | subject to
4 | flow_demand_relative_error_constraint_LinearResistance_#2,1_: 1 flow_demand_allocated_LinearResistance_#2,1_ + 2 flow_demand_error_LinearResistance_#2,1,first_ >= 2
5 | flow_demand_fairness_error_constraint_LinearResistance_#2,1_: -1 flow_demand_error_LinearResistance_#2,1,first_ + 1 flow_demand_error_LinearResistance_#2,1,second_ + 1 average_flow_unit_error_1_ >= 0
6 | flow_conservation_linear_resistance_LinearResistance_#2_: 1 flow_(Basin_#1,_LinearResistance_#2)_ - 1 flow_(LinearResistance_#2,_Basin_#3)_ = 0
7 | volume_conservation_Basin_#1_: 1 basin_storage_change_Basin_#1_ + 1 low_storage_factor_Basin_#1_ + 1 flow_(Basin_#1,_LinearResistance_#2)_ = 1
8 | volume_conservation_Basin_#3_: 1 basin_storage_change_Basin_#3_ + 1 low_storage_factor_Basin_#3_ - 1 flow_(LinearResistance_#2,_Basin_#3)_ = 1
9 | linear_resistance_constraint_LinearResistance_#2_: -0.01 basin_storage_change_Basin_#1_ + 0.01 basin_storage_change_Basin_#3_ + 1 flow_(Basin_#1,_LinearResistance_#2)_ = 1
10 | flow_demand_allocated_sum_constraint_LinearResistance_#2_: 1 flow_(Basin_#1,_LinearResistance_#2)_ - 1 flow_demand_allocated_LinearResistance_#2,1_ - 1 flow_demand_extra_LinearResistance_#2_ = 0
11 | average_flow_unit_error_constraint_1_: -1 flow_demand_error_LinearResistance_#2,1,first_ + 1 average_flow_unit_error_1_ = 0
12 | Bounds
13 | -2 <= basin_storage_change_Basin_#1_ <= 8
14 | -2 <= basin_storage_change_Basin_#3_ <= 8
15 | 0 <= low_storage_factor_Basin_#1_ <= 1
16 | 0 <= low_storage_factor_Basin_#3_ <= 1
17 | -345.6 <= flow_(Basin_#1,_LinearResistance_#2)_ <= 345.6
18 | -345.6 <= flow_(LinearResistance_#2,_Basin_#3)_ <= 345.6
19 | -86400000 <= flow_demand_allocated_LinearResistance_#2,1_ <= 2
20 | 0 <= flow_demand_extra_LinearResistance_#2_ <= 86400000
21 | 0 <= flow_demand_error_LinearResistance_#2,1,first_ <= 1
22 | 0 <= flow_demand_error_LinearResistance_#2,1,second_ <= 1
23 | 0 <= average_flow_unit_error_1_ <= 1
24 | End
25 |
--------------------------------------------------------------------------------
/ribasim_qgis/tests/core/test_model.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from qgis.testing import unittest
4 |
5 | from ribasim_qgis.core.model import (
6 | get_database_path_from_model_file,
7 | get_directory_path_from_model_file,
8 | )
9 |
10 |
11 | class TestModel(unittest.TestCase):
12 | tests_folder_path = Path(__file__).parent.parent.resolve()
13 | data_folder_path = tests_folder_path / "data"
14 |
15 | def test_get_directory_path_from_model_file(self):
16 | """Tests that get_directory_path_from_model_file() can resolve paths from a toml file."""
17 | for test_case in [("input_dir", "."), ("results_dir", "results")]:
18 | with self.subTest(property=test_case[0], value=test_case[1]):
19 | path = get_directory_path_from_model_file(
20 | self.data_folder_path / "simple_valid.toml",
21 | property=test_case[0],
22 | )
23 | self.assertTrue(path.is_absolute(), msg=f"{path} is not absolute")
24 | self.assertTrue(
25 | path.is_relative_to(self.tests_folder_path),
26 | msg=f"Path '{path}' is not relative to {self.tests_folder_path}",
27 | )
28 | self.assertEqual(path, self.data_folder_path / test_case[1])
29 |
30 | def test_get_database_path_from_model_file(self):
31 | """Tests that get_database_path_from_model_file() can find the input directory and appends the database.gpkg to it."""
32 | path = get_database_path_from_model_file(
33 | self.data_folder_path / "simple_valid.toml"
34 | )
35 | self.assertTrue(path.is_absolute(), msg=f"{path} is not absolute")
36 | self.assertTrue(
37 | path.is_relative_to(self.tests_folder_path),
38 | msg=f"Path '{path}' is not relative to {self.tests_folder_path}",
39 | )
40 | self.assertEqual(path, self.data_folder_path / "database.gpkg")
41 |
42 |
43 | if __name__ == "__main__":
44 | unittest.main()
45 |
--------------------------------------------------------------------------------
/python/ribasim_api/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import platform
2 | from pathlib import Path
3 |
4 | import pytest
5 | import ribasim
6 | from ribasim_api import RibasimApi
7 | from ribasim_testmodels import (
8 | basic_model,
9 | basic_transient_model,
10 | leaky_bucket_model,
11 | two_basin_model,
12 | user_demand_model,
13 | )
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def libribasim_paths() -> tuple[Path, Path]:
18 | repo_root = Path(__file__).parents[3].resolve()
19 | lib_or_bin = "bin" if platform.system() == "Windows" else "lib"
20 | extension = ".dll" if platform.system() == "Windows" else ".so"
21 | lib_folder = repo_root / "build" / "ribasim" / lib_or_bin
22 | lib_path = lib_folder / f"libribasim{extension}"
23 | return lib_path, lib_folder
24 |
25 |
26 | @pytest.fixture(scope="session", autouse=True)
27 | def load_julia(libribasim_paths) -> None:
28 | lib_path, lib_folder = libribasim_paths
29 | libribasim = RibasimApi(lib_path, lib_folder)
30 | libribasim.init_julia()
31 |
32 |
33 | @pytest.fixture(scope="function")
34 | def libribasim(libribasim_paths, request) -> RibasimApi:
35 | lib_path, lib_folder = libribasim_paths
36 | libribasim = RibasimApi(lib_path, lib_folder)
37 |
38 | # If initialized, call finalize() at end of use
39 | request.addfinalizer(libribasim.__del__)
40 | return libribasim
41 |
42 |
43 | # we can't call fixtures directly, so we keep separate versions
44 | @pytest.fixture(scope="session")
45 | def basic() -> ribasim.Model:
46 | return basic_model()
47 |
48 |
49 | @pytest.fixture(scope="session")
50 | def basic_transient(basic) -> ribasim.Model:
51 | return basic_transient_model(basic)
52 |
53 |
54 | @pytest.fixture(scope="session")
55 | def leaky_bucket() -> ribasim.Model:
56 | return leaky_bucket_model()
57 |
58 |
59 | @pytest.fixture(scope="session")
60 | def user_demand() -> ribasim.Model:
61 | return user_demand_model()
62 |
63 |
64 | @pytest.fixture(scope="session")
65 | def two_basin() -> ribasim.Model:
66 | return two_basin_model()
67 |
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/include-code-files/include-code-files.lua:
--------------------------------------------------------------------------------
1 | --- include-code-files.lua – filter to include code from source files
2 | ---
3 | --- Copyright: © 2020 Bruno BEAUFILS
4 | --- License: MIT – see LICENSE file for details
5 |
6 | --- Dedent a line
7 | local function dedent (line, n)
8 | return line:sub(1,n):gsub(" ","") .. line:sub(n+1)
9 | end
10 |
11 | --- Filter function for code blocks
12 | local function transclude (cb)
13 | if cb.attributes.include then
14 | local content = ""
15 | local fh = io.open(cb.attributes.include)
16 | if not fh then
17 | io.stderr:write("Cannot open file " .. cb.attributes.include .. " | Skipping includes\n")
18 | else
19 | local number = 1
20 | local start = 1
21 |
22 | -- change hyphenated attributes to PascalCase
23 | for i,pascal in pairs({"startLine", "endLine"})
24 | do
25 | local hyphen = pascal:gsub("%u", "-%0"):lower()
26 | if cb.attributes[hyphen] then
27 | cb.attributes[pascal] = cb.attributes[hyphen]
28 | cb.attributes[hyphen] = nil
29 | end
30 | end
31 |
32 | if cb.attributes.startLine then
33 | cb.attributes.startFrom = cb.attributes.startLine
34 | start = tonumber(cb.attributes.startLine)
35 | end
36 | for line in fh:lines ("L")
37 | do
38 | if cb.attributes.dedent then
39 | line = dedent(line, cb.attributes.dedent)
40 | end
41 | if number >= start then
42 | if not cb.attributes.endLine or number <= tonumber(cb.attributes.endLine) then
43 | content = content .. line
44 | end
45 | end
46 | number = number + 1
47 | end
48 | fh:close()
49 | end
50 | -- remove key-value pair for used keys
51 | cb.attributes.include = nil
52 | cb.attributes.startLine = nil
53 | cb.attributes.endLine = nil
54 | cb.attributes.dedent = nil
55 | -- return final code block
56 | return pandoc.CodeBlock(content, cb.attr)
57 | end
58 | end
59 |
60 | return {
61 | { CodeBlock = transclude }
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ribasim
2 |
3 | [](https://github.com/Deltares/Ribasim/actions/workflows/core_tests.yml)
4 | [](https://github.com/Deltares/Ribasim/actions/workflows/python_tests.yml)
5 | [](https://github.com/Deltares/Ribasim/actions/workflows/qgis.yml)
6 | [](https://codecov.io/gh/Deltares/Ribasim)
7 |
8 | **Documentation: https://ribasim.org/**
9 |
10 | Ribasim is a water resources model, designed to be the replacement of the regional surface
11 | water modules Mozart and SIMRES in the Netherlands Hydrological Instrument (NHI). Ribasim is
12 | a work in progress, it is a prototype that demonstrates all essential functionalities.
13 |
14 | Ribasim is written in the [Julia programming language](https://julialang.org/) and is built
15 | on top of the [SciML: Open Source Software for Scientific Machine Learning](https://sciml.ai/)
16 | libraries.
17 |
18 | # Download
19 |
20 | For most users the [latest release](https://github.com/Deltares/Ribasim/releases/latest) is recommended, it can be downloaded here:
21 |
22 | - Ribasim executable - Linux: [ribasim_linux.zip](https://github.com/Deltares/Ribasim/releases/latest/download/ribasim_linux.zip)
23 | - Ribasim executable - Windows: [ribasim_windows.zip](https://github.com/Deltares/Ribasim/releases/latest/download/ribasim_windows.zip)
24 | - QGIS plugin: [ribasim_qgis.zip](https://github.com/Deltares/Ribasim/releases/latest/download/ribasim_qgis.zip).
25 | - Generated testmodels: [generated_testmodels.zip](https://github.com/Deltares/Ribasim/releases/latest/download/generated_testmodels.zip)
26 |
27 | # Example model
28 |
29 | Ribasim model of the main water distribution network in the Netherlands.
30 |
31 | 
32 |
--------------------------------------------------------------------------------
/docs/reference/node/level-boundary.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "LevelBoundary"
3 | ---
4 |
5 | LevelBoundary is a node whose water level is determined by the input.
6 | It can be used as a boundary condition like the level of the sea or a lake.
7 | Since the water level is unaffected by flow, it acts like an infinitely large Basin.
8 | Connect the LevelBoundary to a node that will look at the level to calculate the flow, like a LinearResistance.
9 |
10 | # Tables
11 |
12 | ## Static
13 |
14 | column | type | unit | restriction
15 | ------------- | ------- | ------------ | -----------
16 | node_id | Int32 | - |
17 | level | Float64 | $\text{m}$ | -
18 |
19 | ## Time
20 |
21 | This table is the transient form of the `LevelBoundary` table.
22 | The only difference is that a time column is added.
23 | With this the levels can be updated over time. In between the given times the
24 | level is interpolated linearly, and outside the flow rate is constant given by the
25 | nearest time value.
26 | Note that a `node_id` can be either in this table or in the static one, but not both.
27 |
28 | column | type | unit | restriction
29 | --------- | ------- | ------------ | -----------
30 | node_id | Int32 | - |
31 | time | DateTime | - |
32 | level | Float64 | $\text{m}$ | -
33 |
34 | ## Concentration {#sec-level-boundary-conc}
35 | This table defines the concentration of substances for the flow from the LevelBoundary.
36 |
37 | column | type | unit | restriction
38 | -------------- | -------- | --------------------- | -----------
39 | node_id | Int32 | - |
40 | time | DateTime | - |
41 | substance | String | - | can correspond to known Delwaq substances
42 | concentration | Float64 | $\text{g}/\text{m}^3$ |
43 |
44 | # Equations
45 |
46 | A LevelBoundary can be connected to a Basin via a LinearResistance.
47 | This boundary node will then exchange water with the Basin based on the difference in water level between the two.
48 |
--------------------------------------------------------------------------------
/python/ribasim_testmodels/ribasim_testmodels/trivial.py:
--------------------------------------------------------------------------------
1 | from ribasim.config import Experimental, Node, Results
2 | from ribasim.model import Model
3 | from ribasim.nodes import basin, tabulated_rating_curve
4 | from shapely.geometry import Point
5 |
6 |
7 | def trivial_model() -> Model:
8 | """Trivial model with just a basin, tabulated rating curve and terminal node."""
9 | model = Model(
10 | starttime="2020-01-01",
11 | endtime="2021-01-01",
12 | crs="EPSG:28992",
13 | results=Results(subgrid=True, compression=False),
14 | use_validation=True,
15 | experimental=Experimental(concentration=True),
16 | )
17 |
18 | # Convert steady forcing to m/s
19 | # 2 mm/d precipitation, 1 mm/d evaporation
20 | precipitation = 0.002 / 86400
21 | potential_evaporation = 0.001 / 86400
22 |
23 | # Create a subgrid level interpolation from one basin to three elements. Scale one to one, but:
24 | # 22. start at -1.0
25 | # 11. start at 0.0
26 | # 33. start at 1.0
27 | basin6 = model.basin.add(
28 | Node(6, Point(400, 200)),
29 | [
30 | basin.Static(
31 | precipitation=[precipitation],
32 | potential_evaporation=[potential_evaporation],
33 | ),
34 | basin.Profile(area=[0.01, 1000.0], level=[0.0, 1.0]),
35 | basin.State(level=[0.04471158417652035]),
36 | basin.Subgrid(
37 | subgrid_id=[22, 22, 11, 11, 33, 33],
38 | basin_level=[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
39 | subgrid_level=[-1.0, 0.0, 0.0, 1.0, 1.0, 2.0],
40 | ),
41 | ],
42 | )
43 |
44 | # TODO largest signed 32 bit integer, to check encoding
45 | terminal_id = 2147483647
46 | term = model.terminal.add(Node(terminal_id, Point(500, 200)))
47 | trc0 = model.tabulated_rating_curve.add(
48 | Node(0, Point(450, 200)),
49 | [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 10 / 86400])],
50 | )
51 |
52 | model.link.add(basin6, trc0, link_id=100)
53 | model.link.add(trc0, term)
54 |
55 | return model
56 |
--------------------------------------------------------------------------------
/python/ribasim_testmodels/ribasim_testmodels/bucket.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import ribasim
4 | from ribasim.config import Experimental, Node
5 | from ribasim.nodes import (
6 | basin,
7 | )
8 | from shapely.geometry import Point
9 |
10 |
11 | def bucket_model() -> ribasim.Model:
12 | """Bucket model with just a single basin at Deltares' headquarter."""
13 | model = ribasim.Model(
14 | starttime="2020-01-01",
15 | endtime="2021-01-01",
16 | crs="EPSG:28992",
17 | experimental=Experimental(concentration=True),
18 | )
19 |
20 | model.basin.add(
21 | Node(1, Point(85825.6, 444613.9)),
22 | [
23 | basin.Profile(
24 | area=[1000.0, 1000.0],
25 | level=[0.0, 1.0],
26 | ),
27 | basin.State(level=[1.0]),
28 | basin.Static(
29 | drainage=[np.nan],
30 | potential_evaporation=[np.nan],
31 | infiltration=[np.nan],
32 | precipitation=[np.nan],
33 | ),
34 | ],
35 | )
36 | return model
37 |
38 |
39 | def leaky_bucket_model() -> ribasim.Model:
40 | """Bucket model with dynamic forcing with missings at Deltares' headquarter."""
41 | model = ribasim.Model(
42 | starttime="2020-01-01",
43 | endtime="2020-01-05",
44 | crs="EPSG:28992",
45 | experimental=Experimental(concentration=True),
46 | )
47 |
48 | model.basin.add(
49 | Node(1, Point(85825.6, 444613.9)),
50 | [
51 | basin.Profile(
52 | area=[1000.0, 1000.0],
53 | level=[0.0, 1.0],
54 | ),
55 | basin.State(level=[1.0]),
56 | basin.Time(
57 | time=pd.date_range("2020-01-01", "2020-01-05"),
58 | node_id=1,
59 | drainage=[0.003, np.nan, 0.001, 0.002, 0.0],
60 | potential_evaporation=np.nan,
61 | infiltration=[np.nan, 0.001, 0.002, 0.0, 0.0],
62 | precipitation=np.nan,
63 | ),
64 | ],
65 | )
66 |
67 | return model
68 |
--------------------------------------------------------------------------------
/.github/workflows/core_tests.yml:
--------------------------------------------------------------------------------
1 | name: Julia Tests
2 | on:
3 | push:
4 | branches: [main]
5 | paths: ["core/**", "python/**", "pixi.toml", "pixi.lock", "Project.toml", "Manifest.toml"]
6 | tags: ["*"]
7 | pull_request:
8 | paths: ["core/**", "python/**", "pixi.toml", "pixi.lock", "Project.toml", "Manifest.toml"]
9 | merge_group:
10 | workflow_dispatch:
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: true
14 | # needed to allow julia-actions/cache to delete old caches that it has created
15 | permissions:
16 | actions: write
17 | contents: read
18 | jobs:
19 | test:
20 | name: Julia ${{ matrix.os }} - ${{ matrix.arch }}
21 | runs-on: ${{ matrix.os }}
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | os:
26 | - ubuntu-latest
27 | # https://github.com/Deltares/Ribasim/issues/825
28 | # - macOS-latest
29 | - windows-latest
30 | arch:
31 | - x64
32 | steps:
33 | - uses: actions/checkout@v6
34 | - uses: julia-actions/cache@v2
35 | - uses: prefix-dev/setup-pixi@v0.9.3
36 | with:
37 | pixi-version: "latest"
38 | - name: Prepare pixi
39 | run: pixi run install-ci
40 | - name: Test Ribasim Core
41 | run: |
42 | pixi run test-ribasim-core-cov
43 | id: julia-tests
44 | env:
45 | JULIA_NUM_THREADS: 2
46 | - name: Upload allocation debug files
47 | if: always()
48 | uses: actions/upload-artifact@v6
49 | with:
50 | name: allocation-debug-${{ matrix.os }}-${{ matrix.arch }}-${{ github.run_id }}
51 | path: |
52 | **/allocation_infeasible_problem.lp
53 | **/allocation_problem_from_tests_*.lp
54 | **/allocation_analysis_*.log
55 | retention-days: 7
56 | if-no-files-found: ignore
57 | - uses: julia-actions/julia-processcoverage@v1
58 | with:
59 | directories: core/src
60 | - uses: codecov/codecov-action@v5
61 | with:
62 | files: lcov.info
63 | token: ${{ secrets.CODECOV_TOKEN }}
64 |
--------------------------------------------------------------------------------
/docs/reference/node/level-demand.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "LevelDemand"
3 | ---
4 |
5 | A `LevelDemand` node associates a minimum and a maximum level (possibly multiple over different priorities) with connected Basins to be used by the allocation algorithm.
6 |
7 | Since this connection conveys information rather than flow, an outgoing control link must be used.
8 | Below the minimum level the Basin has a demand, above the maximum level the Basin has a surplus and acts as a source.
9 | The source can be used by all nodes with demands in order of demand priority.
10 |
11 | The same `LevelDemand` node can be used for Basins in different subnetworks.
12 |
13 | Both `min_level` and `max_level` are optional, to be able to handle only the demand or surplus side.
14 | If both are missing, `LevelDemand` won't have any effects on allocation.
15 |
16 | # Tables
17 |
18 | ## Static
19 |
20 | column | type | unit | restriction
21 | --------------- | ------- | ------------ | -----------
22 | node_id | Int32 | - |
23 | min_level | Float64 | $\text{m}$ | (optional, default -Inf)
24 | max_level | Float64 | $\text{m}$ | (optional, default Inf)
25 | demand_priority | Int32 | - | positive
26 |
27 | ## Time
28 |
29 | This table is the transient form of the `LevelDemand` table, in which time-dependent minimum and maximum levels can be supplied.
30 | With this the levels can be updated over time. In between the given times the
31 | levels are interpolated with forward fill (block interpolation), and outside the demand is constant given by the
32 | nearest time value. The allocation algorithm evaluates the interpolation at the end of the allocation time step.
33 |
34 | This is in contrast with UserDemand and FlowDemand, as allocation will aim to reach the desired level at the end of the allocation time step.
35 |
36 | column | type | unit | restriction
37 | --------------- | ------- | ------------ | -----------
38 | node_id | Int32 | - |
39 | time | DateTime | - |
40 | min_level | Float64 | $\text{m}$ | -
41 | max_level | Float64 | $\text{m}$ | -
42 | demand_priority | Int32 | - | positive
43 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Docs
2 | on:
3 | push:
4 | branches: [main]
5 | paths: ["core/**", "docs/**", "python/**", "pixi.toml", "pixi.lock"]
6 | pull_request:
7 | paths: ["core/**", "docs/**", "python/**", "pixi.toml", "pixi.lock"]
8 | merge_group:
9 | workflow_dispatch:
10 | concurrency:
11 | group: ${{ github.workflow }}-${{ github.ref }}
12 | cancel-in-progress: true
13 | # needed to allow julia-actions/cache to delete old caches that it has created
14 | permissions:
15 | actions: write
16 | contents: write
17 | pages: write
18 | jobs:
19 | publish:
20 | name: Docs Julia
21 | runs-on: ubuntu-latest
22 | timeout-minutes: 120
23 | permissions:
24 | contents: write
25 | steps:
26 | - name: Configure Git for quarto-publish
27 | run: |
28 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
29 | git config --global user.name "github-actions[bot]"
30 | - uses: actions/checkout@v6
31 | - name: Propagate git credentials for v6
32 | run: |
33 | # Propagate git credentials to worktrees (required for actions/checkout@v6)
34 | # See: https://github.com/quarto-dev/quarto-actions/issues/133
35 | if git config get includeIf.gitdir:"$(git rev-parse --path-format=absolute --git-dir)".path &>/dev/null; then
36 | GIT_DIR=$(git rev-parse --path-format=absolute --git-dir)
37 | CRED_PATH=$(git config get --path includeIf.gitdir:"${GIT_DIR}".path)
38 | git config set --path includeIf.gitdir:"${GIT_DIR}/worktrees/*".path "${CRED_PATH}"
39 | fi
40 | - uses: julia-actions/cache@v2
41 | - uses: prefix-dev/setup-pixi@v0.9.3
42 | with:
43 | pixi-version: "latest"
44 | - name: Prepare pixi
45 | run: pixi run install-ci
46 | - name: Check Quarto installation and all engines
47 | run: pixi run quarto-check
48 | - name: Render Quarto Project
49 | run: pixi run quarto-render
50 | - name: Publish Quarto Project
51 | if: github.ref == 'refs/heads/main'
52 | run: pixi run quarto-publish
53 | env:
54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 |
--------------------------------------------------------------------------------
/utils/write_allocation_problems.jl:
--------------------------------------------------------------------------------
1 | import Ribasim
2 |
3 | include("utils.jl")
4 |
5 | """
6 | For all test models that use allocation, write for each subnetwork the problem to file.
7 |
8 | A selection can be made by passing the name(s) of the individual testmodel(s) as (an) argument(s).
9 | """
10 | function main(ARGS)
11 | toml_paths = get_testmodels()
12 | if length(ARGS) > 0
13 | toml_paths = filter(x -> basename(dirname(x)) in ARGS, toml_paths)
14 | end
15 |
16 | results_path = normpath(@__DIR__, "../core/test/data/allocation_problems")
17 | if ispath(results_path)
18 | rm(results_path; recursive = true)
19 | end
20 |
21 | mkdir(results_path)
22 |
23 | Threads.@threads for toml_path in toml_paths
24 | model_name = basename(dirname(toml_path))
25 |
26 | if !startswith(model_name, "invalid_")
27 | config = Ribasim.Config(toml_path)
28 |
29 | if config.experimental.allocation
30 | try
31 | model = Ribasim.Model(config)
32 | (; allocation_models) = model.integrator.p.p_independent.allocation
33 |
34 | if !isempty(allocation_models)
35 | model_dir = normpath(results_path, model_name)
36 | mkdir(model_dir)
37 | end
38 |
39 | for allocation_model in allocation_models
40 | (; problem, subnetwork_id) = allocation_model
41 |
42 | Ribasim.write_problem_to_file(
43 | problem,
44 | config;
45 | info = false,
46 | path = normpath(
47 | model_dir,
48 | "allocation_problem_$subnetwork_id.lp",
49 | ),
50 | )
51 | end
52 | println("Wrote allocation problem(s) for $model_name")
53 | catch e
54 | @error "Failed to process model $model_name" exception = e
55 | end
56 | end
57 | end
58 | end
59 | end
60 |
61 | main(ARGS)
62 |
--------------------------------------------------------------------------------
/.teamcity/Ribasim/buildTypes/GenerateTestmodels.kt:
--------------------------------------------------------------------------------
1 | package Ribasim.buildTypes
2 |
3 | import Templates.*
4 | import Templates.GithubCommitStatusIntegration
5 | import jetbrains.buildServer.configs.kotlin.*
6 | import jetbrains.buildServer.configs.kotlin.buildSteps.script
7 | import jetbrains.buildServer.configs.kotlin.triggers.vcs
8 |
9 | object GenerateTestmodels : BuildType({
10 | templates(GithubCommitStatusIntegration)
11 | name = "Generate Testmodels"
12 |
13 | templates(GithubPullRequestsIntegration)
14 |
15 | artifactRules = """ribasim\generated_testmodels => generated_testmodels.zip"""
16 | publishArtifacts = PublishMode.SUCCESSFUL
17 |
18 | vcs {
19 | cleanCheckout = true
20 | root(Ribasim.vcsRoots.Ribasim, ". => ribasim")
21 | }
22 |
23 | steps {
24 | script {
25 | name = "Set up pixi"
26 | id = "RUNNER_2415"
27 | workingDir = "ribasim"
28 | scriptContent = """
29 | #!/bin/bash
30 | # black magic
31 | source /usr/share/Modules/init/bash
32 |
33 | module load pixi
34 | pixi --version
35 | """.trimIndent()
36 | }
37 | script {
38 | name = "Generate testmodels"
39 | id = "RUNNER_2416"
40 | workingDir = "ribasim"
41 | scriptContent = """
42 | #!/bin/bash
43 | # black magic
44 | source /usr/share/Modules/init/bash
45 |
46 | module load pixi
47 | pixi run generate-testmodels
48 | """.trimIndent()
49 | }
50 | }
51 |
52 | triggers {
53 | vcs {
54 | id = "TRIGGER_646"
55 | branchFilter = """
56 | +:
57 | +:refs/pull/*
58 | +:pull/*
59 | """.trimIndent()
60 | triggerRules = "-:comment=skip ci:**"
61 | }
62 | }
63 |
64 | failureConditions {
65 | executionTimeoutMin = 120
66 | }
67 |
68 | requirements {
69 | doesNotEqual("env.OS", "Windows_NT", "RQ_275")
70 | doesNotEqual("teamcity.agent.name", "Default Agent", "RQ_339")
71 | }
72 | })
73 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/delwaq/parse.py:
--------------------------------------------------------------------------------
1 | """Read a Delwaq model generated from a Ribasim model and inject the results back to Ribasim."""
2 |
3 | from pathlib import Path
4 |
5 | import ribasim
6 | from ribasim.utils import MissingOptionalModule, _concat
7 |
8 | try:
9 | import xugrid as xu
10 | except ImportError:
11 | xu = MissingOptionalModule("xugrid", "delwaq")
12 |
13 |
14 | def parse(
15 | model: Path | ribasim.Model, graph, substances, output_folder=None
16 | ) -> ribasim.Model:
17 | if not isinstance(model, ribasim.Model):
18 | model = ribasim.Model.read(model)
19 | else:
20 | model = model.copy(deep=True)
21 |
22 | # Output of Delwaq
23 | if output_folder is None:
24 | assert model.filepath is not None
25 | output_folder = model.filepath.parent / "delwaq"
26 | with xu.open_dataset(output_folder / "delwaq_map.nc") as ug:
27 | mapping = dict(graph.nodes(data="id"))
28 | # Continuity is a (default) tracer representing the mass balance
29 | substances.add("Continuity")
30 |
31 | dfs = []
32 | for substance in substances:
33 | df = (
34 | ug[f"ribasim_{substance.replace(' ', '_')}"]
35 | .to_dataframe()
36 | .reset_index()
37 | )
38 | df.rename(
39 | columns={
40 | "ribasim_nNodes": "node_id",
41 | "nTimesDlwq": "time",
42 | f"ribasim_{substance.replace(' ', '_')}": "concentration",
43 | },
44 | inplace=True,
45 | )
46 | df["substance"] = substance
47 | df.drop(columns=["ribasim_node_x", "ribasim_node_y"], inplace=True)
48 | # Map the node_id (logical index) to the original node_id
49 | # TODO Check if this is correct
50 | df.node_id += 1
51 | df.node_id = df.node_id.map(mapping)
52 |
53 | dfs.append(df)
54 |
55 | df = _concat(dfs).reset_index(drop=True)
56 | df.sort_values(["time", "node_id"], inplace=True)
57 |
58 | model.basin.concentration_external = df
59 | df.to_feather(model.results_path / "basin_concentration_external.arrow")
60 |
61 | return model
62 |
--------------------------------------------------------------------------------
/core/test/data/allocation_problems/primary_and_secondary_subnetworks/allocation_problem_5.lp:
--------------------------------------------------------------------------------
1 | minimize
2 | obj:
3 | subject to
4 | user_demand_relative_error_constraint_UserDemand_#32,1_: 1 user_demand_allocated_UserDemand_#32,1_ + 2 user_demand_error_UserDemand_#32,1,first_ >= 2
5 | user_demand_fairness_error_constraint_UserDemand_#32,1_: -1 user_demand_error_UserDemand_#32,1,first_ + 1 user_demand_error_UserDemand_#32,1,second_ + 1 average_flow_unit_error_1_ >= 0
6 | flow_conservation_tabulated_rating_curve_TabulatedRatingCurve_#26_: 1 flow_(Basin_#25,_TabulatedRatingCurve_#26)_ - 1 flow_(TabulatedRatingCurve_#26,_Basin_#31)_ = 0
7 | volume_conservation_Basin_#25_: 1 basin_storage_change_Basin_#25_ + 1 low_storage_factor_Basin_#25_ - 1 flow_(Pump_#24,_Basin_#25)_ + 1 flow_(Basin_#25,_TabulatedRatingCurve_#26)_ = 1
8 | volume_conservation_Basin_#31_: 1 basin_storage_change_Basin_#31_ + 1 low_storage_factor_Basin_#31_ - 1 flow_(TabulatedRatingCurve_#26,_Basin_#31)_ + 1 flow_(Basin_#31,_UserDemand_#32)_ - 1 flow_(UserDemand_#32,_Basin_#31)_ = 1
9 | tabulated_rating_curve_constraint_TabulatedRatingCurve_#26_: -0.01 basin_storage_change_Basin_#25_ + 0.01 basin_storage_change_Basin_#31_ + 1 flow_(Basin_#25,_TabulatedRatingCurve_#26)_ = 1
10 | user_demand_allocated_sum_constraint_UserDemand_#32_: 1 flow_(Basin_#31,_UserDemand_#32)_ - 1 user_demand_allocated_UserDemand_#32,1_ = 0
11 | user_demand_return_flow_UserDemand_#32_: -0.5 flow_(Basin_#31,_UserDemand_#32)_ + 1 flow_(UserDemand_#32,_Basin_#31)_ = 0
12 | average_flow_unit_error_constraint_1_: -1 user_demand_error_UserDemand_#32,1,first_ + 1 average_flow_unit_error_1_ = 0
13 | Bounds
14 | -2 <= basin_storage_change_Basin_#25_ <= 8
15 | -2 <= basin_storage_change_Basin_#31_ <= 8
16 | 0 <= low_storage_factor_Basin_#25_ <= 1
17 | 0 <= low_storage_factor_Basin_#31_ <= 1
18 | 0 <= flow_(Pump_#24,_Basin_#25)_ <= 172.8
19 | 0 <= flow_(Basin_#25,_TabulatedRatingCurve_#26)_ <= 86400000
20 | 0 <= flow_(TabulatedRatingCurve_#26,_Basin_#31)_ <= 86400000
21 | 0 <= flow_(Basin_#31,_UserDemand_#32)_ <= 86400000
22 | 0 <= flow_(UserDemand_#32,_Basin_#31)_ <= 86400000
23 | 0 <= user_demand_allocated_UserDemand_#32,1_ <= 2
24 | 0 <= user_demand_error_UserDemand_#32,1,first_ <= 1
25 | 0 <= user_demand_error_UserDemand_#32,1,second_ <= 1
26 | 0 <= average_flow_unit_error_1_ <= 1
27 | End
28 |
--------------------------------------------------------------------------------
/core/test/data/allocation_problems/secondary_networks_with_sources/allocation_problem_5.lp:
--------------------------------------------------------------------------------
1 | minimize
2 | obj:
3 | subject to
4 | user_demand_relative_error_constraint_UserDemand_#32,1_: 1 user_demand_allocated_UserDemand_#32,1_ + 2 user_demand_error_UserDemand_#32,1,first_ >= 2
5 | user_demand_fairness_error_constraint_UserDemand_#32,1_: -1 user_demand_error_UserDemand_#32,1,first_ + 1 user_demand_error_UserDemand_#32,1,second_ + 1 average_flow_unit_error_1_ >= 0
6 | flow_conservation_tabulated_rating_curve_TabulatedRatingCurve_#26_: 1 flow_(Basin_#25,_TabulatedRatingCurve_#26)_ - 1 flow_(TabulatedRatingCurve_#26,_Basin_#31)_ = 0
7 | volume_conservation_Basin_#25_: 1 basin_storage_change_Basin_#25_ + 1 low_storage_factor_Basin_#25_ - 1 flow_(Pump_#24,_Basin_#25)_ + 1 flow_(Basin_#25,_TabulatedRatingCurve_#26)_ = 1
8 | volume_conservation_Basin_#31_: 1 basin_storage_change_Basin_#31_ + 1 low_storage_factor_Basin_#31_ - 1 flow_(TabulatedRatingCurve_#26,_Basin_#31)_ + 1 flow_(Basin_#31,_UserDemand_#32)_ - 1 flow_(UserDemand_#32,_Basin_#31)_ = 1
9 | tabulated_rating_curve_constraint_TabulatedRatingCurve_#26_: -0.01 basin_storage_change_Basin_#25_ + 0.01 basin_storage_change_Basin_#31_ + 1 flow_(Basin_#25,_TabulatedRatingCurve_#26)_ = 1
10 | user_demand_allocated_sum_constraint_UserDemand_#32_: 1 flow_(Basin_#31,_UserDemand_#32)_ - 1 user_demand_allocated_UserDemand_#32,1_ = 0
11 | user_demand_return_flow_UserDemand_#32_: -0.5 flow_(Basin_#31,_UserDemand_#32)_ + 1 flow_(UserDemand_#32,_Basin_#31)_ = 0
12 | average_flow_unit_error_constraint_1_: -1 user_demand_error_UserDemand_#32,1,first_ + 1 average_flow_unit_error_1_ = 0
13 | Bounds
14 | -2 <= basin_storage_change_Basin_#25_ <= 8
15 | -2 <= basin_storage_change_Basin_#31_ <= 8
16 | 0 <= low_storage_factor_Basin_#25_ <= 1
17 | 0 <= low_storage_factor_Basin_#31_ <= 1
18 | 0 <= flow_(Pump_#24,_Basin_#25)_ <= 172.8
19 | 0 <= flow_(Basin_#25,_TabulatedRatingCurve_#26)_ <= 86400000
20 | 0 <= flow_(TabulatedRatingCurve_#26,_Basin_#31)_ <= 86400000
21 | 0 <= flow_(Basin_#31,_UserDemand_#32)_ <= 86400000
22 | 0 <= flow_(UserDemand_#32,_Basin_#31)_ <= 86400000
23 | 0 <= user_demand_allocated_UserDemand_#32,1_ <= 2
24 | 0 <= user_demand_error_UserDemand_#32,1,first_ <= 1
25 | 0 <= user_demand_error_UserDemand_#32,1,second_ <= 1
26 | 0 <= average_flow_unit_error_1_ <= 1
27 | End
28 |
--------------------------------------------------------------------------------
/.teamcity/Templates/Build.kt:
--------------------------------------------------------------------------------
1 | package Templates
2 |
3 |
4 | import jetbrains.buildServer.configs.kotlin.Template
5 | import jetbrains.buildServer.configs.kotlin.*
6 | import jetbrains.buildServer.configs.kotlin.buildSteps.script
7 |
8 | fun generateBuildHeader(platformOs: String): String {
9 | if (platformOs == "Linux") {
10 | return """
11 | #!/bin/bash
12 | # black magic
13 | source /usr/share/Modules/init/bash
14 |
15 | module load pixi
16 | module load gcc/12.2.0_gcc12.2.0
17 | """.trimIndent() + System.lineSeparator()
18 | }
19 |
20 | return ""
21 | }
22 |
23 | open class Build(platformOs: String) : Template() {
24 | init {
25 | name = "Build${platformOs}_Template"
26 |
27 | vcs {
28 | root(Ribasim.vcsRoots.Ribasim, ". => ribasim")
29 | cleanCheckout = true
30 | }
31 |
32 | val depot_path = generateJuliaDepotPath(platformOs)
33 | params {
34 | param("env.JULIA_DEPOT_PATH", depot_path)
35 | }
36 |
37 | dependencies {
38 | artifacts(AbsoluteId("Ribasim_${platformOs}_GenerateCache")) {
39 | buildRule = lastSuccessful()
40 | artifactRules = "cache.zip!** => %teamcity.build.checkoutDir%/.julia"
41 | }
42 | }
43 |
44 | val header = generateBuildHeader(platformOs)
45 | steps {
46 | script {
47 | name = "Set up pixi"
48 | id = "RUNNER_2415"
49 | workingDir = "ribasim"
50 | scriptContent = header +
51 | """
52 | pixi --version
53 | pixi run install-ci
54 | """.trimIndent()
55 | }
56 | script {
57 | name = "Build binary"
58 | id = "RUNNER_2416"
59 | workingDir = "ribasim"
60 | scriptContent = header +
61 | """
62 | pixi run build
63 | """.trimIndent()
64 | }
65 | }
66 |
67 | failureConditions {
68 | executionTimeoutMin = 120
69 | }
70 | }
71 | }
72 |
73 | object BuildWindows : Build("Windows")
74 | object BuildLinux : Build("Linux")
75 |
--------------------------------------------------------------------------------
/core/integration_test/hws_integration_test.jl:
--------------------------------------------------------------------------------
1 | @testitem "HWS model integration test" begin
2 | using Dates
3 | using Statistics
4 | using Arrow
5 | using TOML
6 | include(joinpath(@__DIR__, "../test/utils.jl"))
7 |
8 | toml_path = normpath(@__DIR__, "../../models/integration.toml")
9 | @test ispath(toml_path)
10 | model = Ribasim.run(toml_path)
11 | @test model isa Ribasim.Model
12 | @test success(model)
13 |
14 | basin_bytes_bench =
15 | read(normpath(@__DIR__, "../../models/hws/benchmark/basin_state.arrow"))
16 | basin_bench = Arrow.Table(basin_bytes_bench)
17 |
18 | basin_bytes =
19 | read(normpath(dirname(toml_path), model.config.results_dir, "basin_state.arrow"))
20 | basin = Arrow.Table(basin_bytes)
21 |
22 | @testset "Results values" begin
23 | @test basin.node_id == basin_bench.node_id
24 | @test all(q -> abs(q) < 1.0, basin.level - basin_bench.level)
25 | end
26 |
27 | diff = basin.level - basin_bench.level
28 |
29 | timed = @timed Ribasim.run(toml_path)
30 | dt = Millisecond(round(Int, timed.time * 1000)) + Time(0)
31 |
32 | @tcstatistic "time" timed.time
33 | @tcstatistic "min_diff" minimum(diff)
34 | @tcstatistic "max_diff" maximum(diff)
35 | @tcstatistic "med_diff" median(diff)
36 |
37 | data = Dict(
38 | "time" => timed.time,
39 | "min_diff" => minimum(diff),
40 | "max_diff" => maximum(diff),
41 | "med_diff" => median(diff),
42 | )
43 | open(joinpath(@__DIR__, "../../data/integration.toml"), "w") do io
44 | TOML.print(io, data)
45 | end
46 |
47 | # current benchmark in seconds, TeamCity is up to 4x slower than local
48 | benchmark_runtime = 60
49 | performance_diff =
50 | round((timed.time - benchmark_runtime) / benchmark_runtime * 100; digits = 2)
51 | if performance_diff < 0.0
52 | performance_diff = abs(performance_diff)
53 | @tcstatus "Runtime is $(dt) and it is $performance_diff % faster than benchmark"
54 | elseif performance_diff > 0.0 && performance_diff < 0.2
55 | @tcstatus "Runtime is $(dt) and it is $performance_diff % slower than benchmark"
56 | else
57 | @tcstatus "Runtime is $(dt) and it is $performance_diff % slower than benchmark, close to fail the benchmark"
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/python/ribasim/tests/test_schemas.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | import pandas as pd
4 | import pytest
5 | import ribasim
6 | from pandas.testing import assert_frame_equal
7 | from pydantic import ValidationError
8 | from ribasim import Model
9 | from ribasim.db_utils import _get_db_schema_version, _set_db_schema_version
10 | from ribasim.migrations import _rename_column
11 | from ribasim.nodes import basin
12 | from ribasim.schemas import BasinProfileSchema
13 | from shapely.geometry import Point
14 |
15 |
16 | def test_config_inheritance():
17 | assert BasinProfileSchema.__config__.add_missing_columns
18 | assert BasinProfileSchema.__config__.coerce
19 |
20 |
21 | @patch("ribasim.schemas.migrations.nodeschema_migration")
22 | def test_migration(migration, basic, tmp_path):
23 | toml_path = tmp_path / "basic.toml"
24 | db_path = tmp_path / "input/database.gpkg"
25 | basic.write(toml_path)
26 |
27 | # Migration is not needed on default model
28 | Model.read(toml_path)
29 | assert not migration.called
30 |
31 | # Fake old schema that needs migration
32 | _set_db_schema_version(db_path, 0)
33 | Model.read(toml_path)
34 | assert migration.called
35 |
36 |
37 | def test_model_schema(basic, tmp_path):
38 | toml_path = tmp_path / "basic.toml"
39 | db_path = tmp_path / "input/database.gpkg"
40 | basic.write(toml_path)
41 |
42 | assert _get_db_schema_version(db_path) == ribasim.__schema_version__
43 | _set_db_schema_version(db_path, 0)
44 | assert _get_db_schema_version(db_path) == 0
45 |
46 |
47 | def test_geometry_validation():
48 | with pytest.raises(
49 | ValidationError,
50 | match="Column 'geometry' failed element-wise validator number 0: failure cases",
51 | ):
52 | basin.Area(geometry=[Point([1.0, 2.0])])
53 |
54 |
55 | def test_column_rename():
56 | df = pd.DataFrame({"edge_type": [1], "link_type": [2]})
57 | _rename_column(df, "edge_type", "link_type")
58 | assert_frame_equal(df, pd.DataFrame({"link_type": [1]}))
59 | df = pd.DataFrame({"edge_type": [1]})
60 | _rename_column(df, "edge_type", "link_type")
61 | assert_frame_equal(df, pd.DataFrame({"link_type": [1]}))
62 | df = pd.DataFrame({"link_type": [2]})
63 | assert_frame_equal(df, _rename_column(df, "edge_type", "link_type"))
64 |
--------------------------------------------------------------------------------
/.teamcity/Ribasim_Linux/RibasimLinuxProject.kt:
--------------------------------------------------------------------------------
1 | package Ribasim_Linux
2 |
3 | import Ribasim.vcsRoots.Ribasim
4 | import Templates.*
5 | import jetbrains.buildServer.configs.kotlin.BuildType
6 | import jetbrains.buildServer.configs.kotlin.FailureAction
7 | import jetbrains.buildServer.configs.kotlin.Project
8 | import jetbrains.buildServer.configs.kotlin.triggers.vcs
9 |
10 | object RibasimLinuxProject : Project({
11 | id("Ribasim_Linux")
12 | name = "Ribasim_Linux"
13 |
14 | buildType(Linux_Main)
15 | buildType(Linux_BuildRibasim)
16 | buildType(Linux_TestRibasimBinaries)
17 |
18 | template(TestBinariesLinux)
19 | template(GenerateCacheLinux)
20 | })
21 |
22 | object Linux_Main : BuildType({
23 | name = "RibasimMain"
24 |
25 | templates(GithubPullRequestsIntegration)
26 |
27 | allowExternalStatus = true
28 | type = Type.COMPOSITE
29 |
30 | vcs {
31 | root(Ribasim, ". => ribasim")
32 | cleanCheckout = true
33 | }
34 |
35 | triggers {
36 | vcs {
37 | id = "TRIGGER_RIBA_SKIPL1"
38 | branchFilter = """
39 | +:
40 | +:refs/pull/*
41 | +:pull/*
42 | """.trimIndent()
43 | triggerRules = "-:comment=skip ci:**"
44 | }
45 | }
46 |
47 | dependencies {
48 | snapshot(Linux_TestRibasimBinaries) {
49 | onDependencyFailure = FailureAction.FAIL_TO_START
50 | }
51 | }
52 | })
53 |
54 | object Linux_BuildRibasim : BuildType({
55 | templates(
56 | LinuxAgent,
57 | GithubCommitStatusIntegration,
58 | BuildLinux
59 | )
60 |
61 | name = "Build Ribasim"
62 |
63 | artifactRules = """ribasim\build\ribasim => ribasim_linux.zip!/ribasim"""
64 | })
65 |
66 | object Linux_TestRibasimBinaries : BuildType({
67 | templates(LinuxAgent, GithubCommitStatusIntegration, TestBinariesLinux)
68 | name = "Test Ribasim Binaries"
69 |
70 | dependencies {
71 | dependency(Linux_BuildRibasim) {
72 | snapshot {
73 | }
74 |
75 | artifacts {
76 | id = "ARTIFACT_DEPENDENCY_570"
77 | cleanDestination = true
78 | artifactRules = """
79 | ribasim_linux.zip!/ribasim/** => ribasim/build/ribasim
80 | """.trimIndent()
81 | }
82 | }
83 | }
84 | })
85 |
--------------------------------------------------------------------------------
/.teamcity/Templates/TestDelwaqCoupling.kt:
--------------------------------------------------------------------------------
1 | package Templates
2 |
3 | import Ribasim.vcsRoots.Ribasim
4 | import jetbrains.buildServer.configs.kotlin.*
5 | import jetbrains.buildServer.configs.kotlin.Template
6 | import jetbrains.buildServer.configs.kotlin.buildSteps.script
7 |
8 | open class TestDelwaqCoupling(platformOs: String) : Template() {
9 | init {
10 | name = "TestDelwaqCoupling${platformOs}_Template"
11 |
12 | vcs {
13 | root(Ribasim, ". => ribasim")
14 | cleanCheckout = true
15 | }
16 |
17 | val depot_path = generateJuliaDepotPath(platformOs)
18 | params {
19 | param("env.MINIO_ACCESS_KEY", "KwKRzscudy3GvRB8BN1Z")
20 | password("env.MINIO_SECRET_KEY", "credentialsJSON:86cbf3e5-724c-437d-9962-7a3f429b0aa2")
21 | param("env.JULIA_DEPOT_PATH", depot_path)
22 | }
23 |
24 | dependencies {
25 | artifacts(AbsoluteId("Ribasim_${platformOs}_GenerateCache")) {
26 | buildRule = lastSuccessful()
27 | artifactRules = "cache.zip!** => %teamcity.build.checkoutDir%/.julia"
28 | }
29 | }
30 |
31 | steps {
32 | script {
33 | name = "Set up pixi"
34 | id = "Set_up_pixi"
35 | workingDir = "ribasim"
36 | scriptContent = """
37 | pixi --version
38 | pixi run install-ci
39 | """.trimIndent()
40 | }
41 | script {
42 | name = "Run Delwaq"
43 | id = "Run_Delwaq"
44 | workingDir = "ribasim"
45 | scriptContent = """
46 | pixi run ribasim-core-testmodels basic
47 | set D3D_HOME=%teamcity.build.checkoutDir%/dimr
48 | pixi run delwaq
49 | """.trimIndent()
50 | }
51 | script {
52 | name = "Upload delwaq model"
53 | id = "Delwaq_upload"
54 | workingDir = "ribasim"
55 | scriptContent = """
56 | pixi run s3-upload "python/ribasim/ribasim/delwaq/model/test_offline_delwaq_coupling_ecurrent/delwaq/delwaq_map.nc" "doc-image/delwaq/delwaq_map.nc"
57 | """.trimIndent()
58 | }
59 | }
60 | }
61 | }
62 |
63 | object TestDelwaqCouplingWindows : TestDelwaqCoupling("Windows")
64 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[julia]": {
3 | "editor.formatOnSave": true
4 | },
5 | "[python]": {
6 | "editor.codeActionsOnSave": {
7 | "source.fixAll": "explicit",
8 | "source.organizeImports": "explicit"
9 | },
10 | "editor.defaultFormatter": "charliermarsh.ruff",
11 | "editor.formatOnSave": true
12 | },
13 | "cSpell.enabledLanguageIds": [
14 | "asciidoc",
15 | "c",
16 | "cpp",
17 | "csharp",
18 | "css",
19 | "elixir",
20 | "erlang",
21 | "git-commit",
22 | "go",
23 | "graphql",
24 | "handlebars",
25 | "haskell",
26 | "html",
27 | "jade",
28 | "java",
29 | "javascript",
30 | "javascriptreact",
31 | "json",
32 | "jsonc",
33 | "jupyter",
34 | "latex",
35 | "less",
36 | "markdown",
37 | "php",
38 | "plaintext",
39 | "python",
40 | "pug",
41 | "restructuredtext",
42 | "rust",
43 | "scala",
44 | "scss",
45 | "scminput",
46 | "swift",
47 | "text",
48 | "typescript",
49 | "typescriptreact",
50 | "vue",
51 | "yaml",
52 | "yml",
53 | "quarto",
54 | "julia"
55 | ],
56 | "cSpell.words": [
57 | "gpkg",
58 | "ipynb",
59 | "pixi",
60 | "pkgdown",
61 | "qgis",
62 | "quartodoc",
63 | "Ribasim"
64 | ],
65 | "files.insertFinalNewline": true,
66 | "julia.executablePath": "julia +1.11.7",
67 | "julia.additionalArgs": [
68 | "--check-bounds=yes"
69 | ],
70 | "julia.lint.disabledDirs": [
71 | ".pixi"
72 | ],
73 | "julia.lint.run": true,
74 | "julia.numTestProcesses": 6,
75 | "mypy-type-checker.importStrategy": "fromEnvironment",
76 | "notebook.codeActionsOnSave": {
77 | "notebook.source.fixAll": "explicit",
78 | "notebook.source.organizeImports": "explicit"
79 | },
80 | "notebook.formatOnSave.enabled": true,
81 | "python.testing.pytestArgs": [
82 | "."
83 | ],
84 | "python.testing.pytestEnabled": true,
85 | "python.testing.unittestEnabled": false,
86 | "python-envs.defaultEnvManager": "renan-r-santos.pixi-code:pixi",
87 | "python-envs.defaultPackageManager": "renan-r-santos.pixi-code:pixi",
88 | "python-envs.pythonProjects": []
89 | }
90 |
--------------------------------------------------------------------------------
/ribasim_qgis/core/model.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import Any
3 |
4 | from qgis.core import qgsfunction
5 |
6 | import ribasim_qgis.tomllib as tomllib
7 |
8 |
9 | def get_directory_path_from_model_file(model_path: Path, *, property: str) -> Path:
10 | """Generate database absolute full path from model .toml file.
11 |
12 | Parameters
13 | ----------
14 | path : Path
15 | Path to model .toml file.
16 | property : str
17 | The property to retrieve from the model file and append to the path.
18 |
19 | Returns
20 | -------
21 | Path
22 | Full path to database Geopackage.
23 | """
24 | with open(model_path, "rb") as f:
25 | found_property = Path(tomllib.load(f)[property])
26 | # The .joinpath method (/) of pathlib.Path will take care of an absolute input_dir.
27 | # No need to check it ourselves!
28 | return (Path(model_path).parent / found_property).resolve()
29 |
30 |
31 | def get_toml_dict(model_path: Path) -> dict[str, Any]:
32 | with open(model_path, "rb") as f:
33 | return tomllib.load(f)
34 |
35 |
36 | def get_database_path_from_model_file(model_path: Path) -> Path:
37 | """Get the database path database.gpkg based on the model file's input_dir.
38 |
39 | Parameters
40 | ----------
41 | model_path : Path
42 | Path to the model (.toml) file.
43 |
44 | Returns
45 | -------
46 | Path
47 | The full path to database.gpkg
48 | """
49 | return (
50 | get_directory_path_from_model_file(model_path, property="input_dir")
51 | / "database.gpkg"
52 | )
53 |
54 |
55 | @qgsfunction(args="auto", group="Custom", referenced_columns=[]) # type: ignore
56 | def label_flow_rate(value: float) -> str:
57 | """
58 | Format the label for `flow_rate`.
59 |
60 | Above 1, show 2 decimals.
61 | Show 0 as 0.
62 | Between 0 and 1, and below 1, show scientific notation and 2 decimals.
63 | Example outputs: 0, 1.23e-06, 12345.68
64 | """
65 | if abs(value) >= 1:
66 | return f"{value:.2f}"
67 | if abs(value) == 0.0:
68 | return "0"
69 | else:
70 | return f"{value:.2e}"
71 |
72 |
73 | @qgsfunction(args="auto", group="Custom", referenced_columns=[]) # type: ignore
74 | def label_scientific(value: float) -> str:
75 | """
76 | Format the label for `concentration`.
77 |
78 | Uses scientific notation with 3 decimals.
79 | """
80 | return f"{value:.3e}"
81 |
--------------------------------------------------------------------------------
/docs/dev/benchmark.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Benchmark"
3 | ---
4 |
5 | This document describes how the benchmarking and performance testing of Ribasim is handled.
6 | In Ribasim, the benchmarking includes and regression tests on the test models and regressive performance tests on the production models.
7 |
8 | The idea of regression tests on the test models is to run models with various solvers, run models with a sparse Jacobian and a dense one and compare the outputs.
9 | It will possibly involve production models in the future.
10 | And runtime performance test is lined up for the next step (in [issue #1698](https://github.com/Deltares/Ribasim/issues/1698)).
11 |
12 | The idea of regressive performance tests on the production models is to test the performance of running the production models.
13 | It will report if the new changes in the code decrease the model's performance or result in failed runs.
14 |
15 | # Benchmarking of the test models
16 | ## Benchmark the ODE solvers
17 | The benchmarking of the ODE solvers is done by running the test models with different ODE solvers and solver settings and comparing the output with the benchmark.
18 |
19 | The settings include toggling the `sparse` and `autodiff` solver settings.
20 | Currently, 4 models are chosen to undergo the regression tests.
21 | They are `trivial`, `basic`, `pid_control` and `subnetwork_with_sources`.
22 |
23 | The benchmark reference are the output files of a run of the test models with default solver settings.
24 | The output files `basin.arrow` and `flow.arrow` are used for comparison.
25 | Different margins are set for the comparison of the outputs, and the benchmark is considered passed if the output is within the margin.
26 | Since we are still in the process of evaluating the performance of different solvers, the margin is subject to change.
27 |
28 | The regression tests are run on a weekly basis.
29 |
30 | # Benchmarking of the production model
31 | Regressive performance tests on the production models are done by running the production models with the new changes of the code and comparing the runtime performance with the reference run.
32 | The references are the output files of a run of the production models with the default solver settings.
33 | The output file `basin_state.arrow` which records the end states of the basin is used for comparison.
34 | Since the development of the model is still ongoing, the benchmark is subject to change.
35 |
36 | The regressive performance tests are currently run on a weekly basis.
37 |
--------------------------------------------------------------------------------
/python/ribasim/ribasim/delwaq/README.md:
--------------------------------------------------------------------------------
1 | # Ribasim Delwaq coupling
2 | This folder contains scripts to setup a Delwaq model from a Ribasim model, and to update the Ribasim model with the Delwaq output.
3 |
4 | ## Steps
5 | Setup a Ribasim model with substances and concentrations and run it. For example, we can run the basic testmodel with some default concentrations using the Ribasim CLI:
6 |
7 | ```bash
8 | ribasim generated_testmodels/basic/ribasim.toml
9 | ```
10 |
11 | Afterwards we can use the Ribasim model and the generated output to setup a Delwaq model using Python from this folder.
12 |
13 | ```python
14 | from pathlib import Path
15 |
16 | from generate import generate
17 | from parse import parse
18 | from util import run_delwaq
19 |
20 | toml_path = Path("generated_testmodels/basic/ribasim.toml")
21 |
22 | graph, substances = generate(toml_path)
23 | run_delwaq()
24 | model = parse(toml_path, graph, substances)
25 | ```
26 |
27 | The resulting Ribasim model will have an updated `model.basin.concentration_external` table with the Delwaq output.
28 | We also store the same table in the `basin_concentration_external.arrow` file in the results folder, which can be
29 | referred to using the Ribasim config file.
30 |
31 | ## Running Delwaq
32 | If you have access to a DIMR release, you can find the Delwaq executables in the `bin` folder. You can run a model directly with the `run_dimr.bat` script, and providing the path to the generated `.inp` file to it. In `util.py` we provide a `run_delwaq` (as used above) that does this for you, if you set the `D3D_HOME` environment variable to the path of the unzipped DIMR release, using the generated `model/ribasim.inp` configuration file.
33 |
34 | ### Running Delwaq with Docker
35 | Alternative to running Delwaq with a DIMR release, you can also run the Delwaq model in a Docker container if you are a Deltares employee.
36 | First install WSL and install docker in WSL, then create a CLI secret and log into the Deltares containers. To install docker in WSL and create a CLI secret for the following steps, follow this guide https://publicwiki.deltares.nl/display/Delft3DContainers/.
37 |
38 | Log into Deltares containers in docker:
39 | ```bash
40 | docker login containers.deltares.nl # use your deltares email + token
41 | ```
42 |
43 | You can now run the Delwaq model from this directory.
44 | ```bash
45 | docker run --mount type=bind,source="$(pwd)/model",target=/mnt/myModel \
46 | --workdir /mnt/myModel containers.deltares.nl/delft3d/delft3dfm run_dimr.sh
47 | ```
48 |
--------------------------------------------------------------------------------
/docs/dev/qgis_test_plan.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "QGIS plugin manual test plan"
3 | ---
4 |
5 | This document describes how to perform a full manual test on the Ribasim QGIS plugin.
6 | Known shortcomings and issues can be documented [here](/known_issues.qmd).
7 | Bugs can be reported on [GitHub](https://github.com/Deltares/Ribasim/issues).
8 |
9 | # Clean slate tests
10 | Before starting with data, perform the following tests to see if the plugin doesn't result in any errors.
11 |
12 | ## Enable and disable
13 |
14 | - Open QGIS and navigate to "Plugins > Manage and Install Plugins...": _The plugin management window opens_.
15 | - Navigate to "Installed": _Ribasim plugin is in the list (enabled)_.
16 | - Disable the Ribasim plugin: _Ribasim plugin panel hides if it was open, Ribasim button hides from navigation toolbar_.
17 | - Enable the Ribasim plugin: _Ribasim button shows on the navigation toolbar_.
18 |
19 | # Model tab button interaction tests
20 |
21 | ## Open model twice
22 |
23 | - Open QGIS and ensure that the Ribasim plugin is installed and enabled.
24 | - Click the Ribasim button on the QGIS toolbar: _file navigation window pops up_.
25 | - Choose an existing model from the `generated_testmodels` folder.
26 | - Press OK: _The model layers appear in the layer panel and on the map_.
27 | - Click the Ribasim button on the QGIS toolbar: _file navigation window pops up_.
28 | - Open the same model again: _A new layer group is added to the layers panel_.
29 |
30 | Intended behavior: The same model is loaded twice, but there is only a connection on the last loaded model when interacting with the plugin.
31 |
32 | # Map interaction tests
33 |
34 | ## Node selection on map triggers table selection
35 |
36 | - Open QGIS and ensure that the Ribasim plugin is installed and enabled.
37 | - Choose an existing model from the `generated_testmodels` folder in the file explorer.
38 | - Drag the TOML file onto QGIS: _The model layers appear in the layer panel and on the map_.
39 | - Select the node layer, and make a subselection of nodes on the map: _Nodes are highlighted in yellow, including their links_.
40 | - Open the Link attribute table: _The highlighted rows are those with a from/to node\_id that was selected_.
41 | - Open any non-spatial attribute table: _The highlighted rows are those with a node\_id that was selected_.
42 |
43 | # Result inspection tests
44 |
45 | ## Run a model and check the time series
46 | TODO
47 |
48 | # Tutorial tests
49 |
50 | ## Perform tutorial in documentation
51 |
52 | Go through the tutorial as described in the [How-to guide](/guide/qgis.qmd).
53 |
--------------------------------------------------------------------------------
/docs/guide/updating-ribasim.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Updating Ribasim"
3 | ---
4 |
5 | This guide explains how to update Ribasim models from older versions to newer versions.
6 | To update the Ribasim installation itself, follow the [installation steps](/getting-started/install.qmd), ensuring that all installed components are the same.
7 | To see the most important changes in Ribasim, consult the [changelog](/changelog.qmd).
8 |
9 | # Version numbers and breaking changes
10 |
11 | Ribasim uses version numbers like `2023.1.0`, following `YYYY.MINOR.MICRO` from [calver](https://calver.org/).
12 | It starts with the year the release was made, followed by the minor version number for normal releases, and a micro number for non-breaking, hotfix releases.
13 | This means that whenever the year or minor version changes, there is a possibility that the user has to make changes to the model for it to keep working.
14 | If this is the case, it will be highlighted in the [changelog](/changelog.qmd).
15 | When possible, we automate this process using model migration in Ribasim Python, see the section below.
16 |
17 | # Automatic model migration
18 |
19 | Models are automatically migrated when read from file using Ribasim Python, and this is the recommended way to update your models.
20 |
21 | The Ribasim Python package contains a set of migration functions that are applied automatically when you read a model file from an older version.
22 | When Ribasim developers make changes to the model structure (for example, adding new required columns to tables), these changes would normally break compatibility with existing models.
23 | To prevent this, migration functions automatically update your model data to match the current version's requirements.
24 |
25 | The core always expects models to be written by the same Ribasim Python version as itself, which is why it gives a warning whenever the `ribasim_version` in the TOML does not match `ribasim --version`.
26 |
27 | When you read an older model, Ribasim Python will automatically apply all necessary migrations and update the model version accordingly.
28 | To migrate an existing model to the latest version, simply use the following script:
29 |
30 | ```python
31 | import ribasim
32 |
33 | # Read the old model (migration happens automatically)
34 | model = ribasim.Model.read("path/to/your/old_model.toml")
35 |
36 | # Write the migrated model
37 | model.write("path/to/your/migrated_model.toml")
38 | ```
39 |
40 | If you have a script that builds your model from scratch, simply re-running that script with the new Ribasim Python version will produce a model with the updated version, making the above migration step unnecessary.
41 |
--------------------------------------------------------------------------------
/python/ribasim_testmodels/ribasim_testmodels/backwater.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import ribasim
3 | from ribasim.config import Experimental, Node
4 | from ribasim.model import Model
5 | from ribasim.nodes import (
6 | basin,
7 | flow_boundary,
8 | manning_resistance,
9 | )
10 | from shapely.geometry import Point
11 |
12 |
13 | def backwater_model() -> Model:
14 | """Backwater curve as an integration test for ManningResistance."""
15 | node_type = np.full(102, "ManningResistance")
16 | node_type[1::2] = "Basin"
17 | node_type[0] = "FlowBoundary"
18 | node_type[-1] = "LevelBoundary"
19 |
20 | ids = np.arange(1, node_type.size + 1, dtype=np.int32)
21 |
22 | model = ribasim.Model(
23 | starttime="2020-01-01",
24 | endtime="2021-01-01",
25 | crs="EPSG:28992",
26 | solver=ribasim.Solver(autodiff=True, specialize=True),
27 | experimental=Experimental(concentration=True),
28 | )
29 |
30 | model.flow_boundary.add(
31 | Node(1, Point(0.0, 0.0)), [flow_boundary.Static(flow_rate=[5.0])]
32 | )
33 |
34 | # Rectangular profile, width of 1.0 m.
35 | basin_ids = ids[node_type == "Basin"]
36 | basin_x = np.arange(10.0, 1000.0, 20.0)
37 | for id, x in zip(basin_ids, basin_x):
38 | model.basin.add(
39 | Node(id, Point(x, 0.0)),
40 | [
41 | basin.Profile(area=[20.0, 20.0], level=[0.0, 1.0]),
42 | basin.State(level=[0.05]),
43 | ],
44 | )
45 | model.manning_resistance.add(
46 | Node(id + 1, Point(x + 10.0, 0.0)),
47 | [
48 | manning_resistance.Static(
49 | length=[20.0],
50 | manning_n=[0.04],
51 | profile_width=[1.0],
52 | profile_slope=[0.0],
53 | )
54 | ],
55 | )
56 | if id == 2:
57 | model.link.add(
58 | model.flow_boundary[1],
59 | model.basin[2],
60 | )
61 | else:
62 | model.link.add(
63 | model.manning_resistance[id - 1],
64 | model.basin[id],
65 | )
66 |
67 | model.link.add(
68 | model.basin[id],
69 | model.manning_resistance[id + 1],
70 | )
71 |
72 | model.basin.add(
73 | Node(102, Point(1010.0, 0.0)),
74 | [basin.State(level=[2.0]), basin.Profile(level=[0.0, 1.0], area=1e10)],
75 | )
76 | model.link.add(
77 | model.manning_resistance[101],
78 | model.basin[102],
79 | )
80 |
81 | return model
82 |
--------------------------------------------------------------------------------
/core/test/carrays_test.jl:
--------------------------------------------------------------------------------
1 | @testitem "UnitRange" begin
2 | using Ribasim.CArrays: CArray, CVector, getdata, getaxes
3 | data = [1.0, 2.0, 3.0]
4 | axes = (a = 1:1, b = 2:3)
5 | x = CArray(data, axes)
6 | @test x isa CVector{Float64}
7 | @test x isa DenseVector{Float64}
8 | @test length(x) == 3
9 | @test size(x) == (3,)
10 | @test x[1] == 1.0
11 | @test x[2] == 2.0
12 | @test x[3] == 3.0
13 | @test x[1:2] == [1.0, 2.0]
14 | @test x.a isa SubArray
15 | @test x.b isa SubArray
16 | @test getdata(x) === data
17 | @test getaxes(x) === axes
18 | @test keys(x) === (:a, :b)
19 | @inferred getproperty(x, :a)
20 | @test similar(x) isa CVector{Float64}
21 | @test similar(x, Int) isa CVector{Int}
22 | @test similar(x, 2, 3) isa Matrix{Float64}
23 | @test similar(x, Int, 2, 3) isa Matrix{Int}
24 | @test iterate(x) === (1.0, 2)
25 |
26 | @test map(identity, x) isa CVector{Float64}
27 | @test map!(identity, similar(data), x) isa Vector{Float64}
28 | @test map!(identity, similar(x), x) isa CVector{Float64}
29 | end
30 |
31 | @testitem "Int" begin
32 | using Ribasim.CArrays: CArray, CVector
33 | data = [1.0, 2.0, 3.0]
34 | axes = (a = 1, b = 2:3)
35 | x = CArray(data, axes)
36 | @test x.a === 1.0
37 | @test x.b isa SubArray
38 | @test x.b == [2.0, 3.0]
39 | FloatView = SubArray{Float64, 1, Vector{Float64}, Tuple{UnitRange{Int64}}, true}
40 | @inferred Union{Float64, FloatView} getproperty(x, :a)
41 | @inferred Union{Float64, FloatView} getproperty(x, :b)
42 | end
43 |
44 | @testitem "Nested" begin
45 | using Ribasim.CArrays: CArray, CVector, getdata, getaxes
46 | data = [1.0, 2.0, 3.0]
47 | axes = (; a = (; b = 1, c = 2:3))
48 | x = CArray(data, axes)
49 | xa = x.a
50 | @test xa isa CVector
51 | @test getdata(xa) === data
52 | @test getaxes(xa) === axes.a
53 | @test_throws ErrorException x.b
54 | @test x.a.b === 1.0
55 | @test x.a.c isa SubArray
56 | @test x.a.c == [2.0, 3.0]
57 | @inferred getproperty(x, :a)
58 | FloatView = SubArray{Float64, 1, Vector{Float64}, Tuple{UnitRange{Int64}}, true}
59 | @inferred Union{Float64, FloatView} getproperty(xa, :b)
60 | @inferred Union{Float64, FloatView} getproperty(xa, :c)
61 | end
62 |
63 | @testitem "CMatrix" begin
64 | using Ribasim.CArrays: CArray, CMatrix
65 | data = [1.0; 2; 3;; 4; 5; 6]
66 | axes = (a = 1, b = 2, c = CartesianIndex(3, 2))
67 | x = CArray(data, axes)
68 | x isa CMatrix
69 | x.a === 1.0
70 | x.b === 2.0
71 | x.c === 6.0
72 | @inferred getproperty(x, :a)
73 | @inferred getproperty(x, :b)
74 | @inferred getproperty(x, :c)
75 | end
76 |
--------------------------------------------------------------------------------
/docs/reference/node/flow-boundary.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "FlowBoundary"
3 | ---
4 |
5 | A FlowBoundary adds water to the model at a specified flow rate.
6 | It can be used as a boundary condition like a measured upstream flow rate, or lateral inflow.
7 | We require that an link connected to a FlowBoundary is always outgoing, and points towards a Basin.
8 |
9 | # Tables
10 |
11 | ## Static
12 |
13 | column | type | unit | restriction
14 | ------------- | ------- | --------------------- | -----------
15 | node_id | Int32 | - |
16 | flow_rate | Float64 | $\text{m}^3/\text{s}$ | non-negative
17 |
18 | ## Time
19 |
20 | This table is the transient form of the `FlowBoundary` table.
21 | The only difference is that a time column is added.
22 | With this the flow rates can be updated over time. In between the given times the
23 | flow rate is interpolated in a way specified in the [interpolation settings](/reference/usage.qmd#interpolation-settings) (block interpolation by default), and outside the flow rate is constant given by the
24 | nearest time value unless the node is cyclic in time.
25 | Note that a `node_id` can be either in this table or in the static one, but not both.
26 |
27 | column | type | unit | restriction
28 | --------- | ------- | --------------------- | -----------
29 | node_id | Int32 | - |
30 | time | DateTime | - |
31 | flow_rate | Float64 | $\text{m}^3/\text{s}$ | non-negative
32 |
33 | ## Concentration {#sec-flow-boundary-conc}
34 | This table defines the concentration of substances for the flow from the FlowBoundary.
35 |
36 | column | type | unit | restriction
37 | -------------- | -------- | --------------------- | -----------
38 | node_id | Int32 | - |
39 | time | DateTime | - |
40 | substance | String | - | can correspond to known Delwaq substances
41 | concentration | Float64 | $\text{g}/\text{m}^3$ |
42 |
43 | ## Area
44 |
45 | The optional area table is not used during computation, but provides a place to associate areas in the form of polygons to a FlowBoundary.
46 | Using this makes it easier to recognize which water or land surfaces are represented by a FlowBoundary.
47 |
48 | column | type | restriction
49 | --------- | ----------------------- | -----------
50 | node_id | Int32 |
51 | geom | Polygon or MultiPolygon | (optional)
52 |
53 | # Equations
54 |
55 | A FlowBoundary can be connected directly to a Basin and prescribes the flow to that Basin.
56 | Since the `flow_rate` cannot be negative a FlowBoundary can only add water to the model.
57 |
--------------------------------------------------------------------------------
/core/test/data/allocation_problems/small_primary_secondary_network/allocation_problem_2.lp:
--------------------------------------------------------------------------------
1 | minimize
2 | obj:
3 | subject to
4 | user_demand_relative_error_constraint_UserDemand_#6,2_: 1 user_demand_allocated_UserDemand_#6,2_ + 2 user_demand_error_UserDemand_#6,2,first_ >= 2
5 | storage_constraint_lower_Basin_#5,1_: 1 basin_storage_change_Basin_#5_ + 1 level_demand_error_Basin_#5,1,lower,first_ >= -250
6 | storage_constraint_upper_Basin_#5,1_: -1 basin_storage_change_Basin_#5_ + 1 level_demand_error_Basin_#5,1,upper,first_ >= -750
7 | user_demand_fairness_error_constraint_UserDemand_#6,2_: -1 user_demand_error_UserDemand_#6,2,first_ + 1 user_demand_error_UserDemand_#6,2,second_ + 1 average_flow_unit_error_2_ >= 0
8 | level_demand_fairness_error_constraint_Basin_#5,1,lower_: -0.001 level_demand_error_Basin_#5,1,lower,first_ + 1 level_demand_error_Basin_#5,1,lower,second_ + 1 average_storage_unit_error_1,lower_ >= 0
9 | level_demand_fairness_error_constraint_Basin_#5,1,upper_: -0.001 level_demand_error_Basin_#5,1,upper,first_ + 1 level_demand_error_Basin_#5,1,upper,second_ + 1 average_storage_unit_error_1,upper_ >= 0
10 | volume_conservation_Basin_#5_: 1 basin_storage_change_Basin_#5_ + 1 low_storage_factor_Basin_#5_ - 1 flow_(Outlet_#4,_Basin_#5)_ + 1 flow_(Basin_#5,_UserDemand_#6)_ - 1 flow_(UserDemand_#6,_Basin_#5)_ = 1
11 | user_demand_allocated_sum_constraint_UserDemand_#6_: 1 flow_(Basin_#5,_UserDemand_#6)_ - 1 user_demand_allocated_UserDemand_#6,2_ = 0
12 | user_demand_return_flow_UserDemand_#6_: -0.5 flow_(Basin_#5,_UserDemand_#6)_ + 1 flow_(UserDemand_#6,_Basin_#5)_ = 0
13 | average_flow_unit_error_constraint_2_: -1 user_demand_error_UserDemand_#6,2,first_ + 1 average_flow_unit_error_2_ = 0
14 | average_storage_unit_error_constraint_1,upper_: -1 level_demand_error_Basin_#5,1,upper,first_ + 1000 average_storage_unit_error_1,upper_ = 0
15 | average_storage_unit_error_constraint_1,lower_: -1 level_demand_error_Basin_#5,1,lower,first_ + 1000 average_storage_unit_error_1,lower_ = 0
16 | Bounds
17 | -0.006666666666666667 <= basin_storage_change_Basin_#5_ <= 0.02666666666666667
18 | 0 <= low_storage_factor_Basin_#5_ <= 1
19 | 0 <= flow_(Outlet_#4,_Basin_#5)_ <= 1.728
20 | 0 <= flow_(Basin_#5,_UserDemand_#6)_ <= 288000
21 | 0 <= flow_(UserDemand_#6,_Basin_#5)_ <= 288000
22 | 0 <= user_demand_allocated_UserDemand_#6,2_ <= 2
23 | 0 <= user_demand_error_UserDemand_#6,2,first_ <= 1
24 | 0 <= user_demand_error_UserDemand_#6,2,second_ <= 1
25 | level_demand_error_Basin_#5,1,lower,first_ >= 0
26 | level_demand_error_Basin_#5,1,lower,second_ >= 0
27 | level_demand_error_Basin_#5,1,upper,first_ >= 0
28 | level_demand_error_Basin_#5,1,upper,second_ >= 0
29 | 0 <= average_flow_unit_error_2_ <= 1
30 | average_storage_unit_error_1,lower_ >= 0
31 | average_storage_unit_error_1,upper_ >= 0
32 | End
33 |
--------------------------------------------------------------------------------
/python/ribasim_testmodels/ribasim_testmodels/two_basin.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from ribasim.config import Experimental, Node, Results
4 | from ribasim.input_base import TableModel
5 | from ribasim.model import Model
6 | from ribasim.nodes import basin, flow_boundary, tabulated_rating_curve
7 | from shapely.geometry import Point
8 |
9 |
10 | def two_basin_model() -> Model:
11 | """
12 | Create a model of two basins.
13 |
14 | The basins are not connected; the model is mostly designed to test in
15 | combination with a groundwater model.
16 |
17 | The left basin receives water. In case of a coupled run, the water
18 | infiltrates in the left basin, and exfiltrates in the right basin.
19 | The right basin fills up and discharges over the rating curve.
20 | """
21 | model = Model(
22 | starttime="2020-01-01",
23 | endtime="2021-01-01",
24 | crs="EPSG:28992",
25 | experimental=Experimental(concentration=True),
26 | results=Results(subgrid=True),
27 | )
28 |
29 | model.flow_boundary.add(
30 | Node(1, Point(0, 0)), [flow_boundary.Static(flow_rate=[1e-2])]
31 | )
32 | basin_shared: list[TableModel[Any]] = [
33 | basin.Profile(area=400.0, level=[0.0, 1.0]),
34 | basin.State(level=[0.01]),
35 | ]
36 | model.basin.add(
37 | Node(2, Point(250, 0)),
38 | [
39 | *basin_shared,
40 | basin.Subgrid(
41 | subgrid_id=1,
42 | basin_level=[0.0, 1.0],
43 | subgrid_level=[0.0, 1.0],
44 | meta_x=250.0,
45 | meta_y=0.0,
46 | ),
47 | ],
48 | )
49 | model.basin.add(
50 | Node(3, Point(750, 0)),
51 | [
52 | *basin_shared,
53 | # Raise the subgrid levels by a meter after a month
54 | basin.SubgridTime(
55 | subgrid_id=2,
56 | time=["2020-01-01", "2020-01-01", "2020-02-01", "2020-02-01"],
57 | basin_level=[0.0, 1.0, 0.0, 1.0],
58 | subgrid_level=[0.0, 1.0, 1.0, 2.0],
59 | meta_x=750.0,
60 | meta_y=0.0,
61 | ),
62 | ],
63 | )
64 | model.tabulated_rating_curve.add(
65 | Node(4, Point(1000, 0)),
66 | [tabulated_rating_curve.Static(level=[0.0, 1.0], flow_rate=[0.0, 0.01])],
67 | )
68 | model.terminal.add(Node(5, Point(1100, 0)))
69 |
70 | model.link.add(model.flow_boundary[1], model.basin[2])
71 | model.link.add(
72 | model.basin[3],
73 | model.tabulated_rating_curve[4],
74 | )
75 | model.link.add(
76 | model.tabulated_rating_curve[4],
77 | model.terminal[5],
78 | )
79 | return model
80 |
--------------------------------------------------------------------------------
/docs/reference/node/continuous-control.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "ContinuousControl"
3 | ---
4 |
5 | The `ContinuousControl` node allows for fine control of a controllable property of a connector node, which is updated at each time step. This control can be set up as follows:
6 |
7 | 1. Define a compound variable. This is a linear combination of variables in the model the `ContinuousControl` node can listen to, i.e. flows and levels.;
8 | 2. Define a piecewise linear function by providing datapoints which get interpolated. The controlled parameter is then set to the outcome of this function given the value of the compound variable.
9 |
10 | :::{.callout-note}
11 | Having `ContinuousControl` nodes depend on eachother or `PidControl` nodes does *not* work.
12 | For instance, if one `ContinuousControl` node sets the flow rate of a pump, this flow rate cannot be used as the input of another `ContinuousControl` node. This will not throw an error but will produce incorrect results.
13 | :::
14 |
15 | # Tables
16 |
17 | ## Variable
18 |
19 | The compound variable schema defines linear combinations of variables which can be used as an input for continuous functions described below. This means that
20 | this schema defines new variables that look like
21 | $$
22 | \text{weight}_1 * \text{variable}_1 + \text{weight}_2 * \text{variable}_2 + \ldots,
23 | $$
24 |
25 | which can be for instance an average or a difference of variables. If a variable comes from a timeseries, a look ahead $\Delta t$ can be supplied. There is only one compound variable per `ContinuousControl` node.
26 |
27 | column | type | unit | restriction
28 | -------------------- | -------- | ---------- | -----------
29 | node_id | Int32 | - |
30 | listen_node_id | Int32 | - | cannot be a Junction
31 | variable | String | - | must be "level" or "flow_rate"
32 | weight | Float64 | - | (optional, default 1.0)
33 | look_ahead | Float64 | $\text{s}$ | Only on transient boundary conditions, non-negative (optional, default 0.0).
34 |
35 | ## Function
36 |
37 | The function table defines a smooth function $f$ interpolating between `(input, output)` datapoints for each `ContinuousControl`. The interpolation type is PCHIP, for more information see [here](https://www.mathworks.com/help/matlab/ref/pchip.html). The total computation thus looks like
38 |
39 | $$
40 | f(\text{weight}_1 * \text{variable}_1 + \text{weight}_2 * \text{variable}_2 + \ldots).
41 | $$
42 |
43 | column | type | unit | restriction
44 | -------------------- | ------- | ------- | -----------
45 | node_id | Int32 | - |
46 | input | Float64 | - |
47 | output | Float64 | - | -
48 | controlled_variable | String | - | must be "level" or "flow_rate"
49 |
--------------------------------------------------------------------------------
/core/test/data/allocation_problems/medium_primary_secondary_network/allocation_problem_2.lp:
--------------------------------------------------------------------------------
1 | minimize
2 | obj:
3 | subject to
4 | user_demand_relative_error_constraint_UserDemand_#14,3_: 1 user_demand_allocated_UserDemand_#14,3_ + 2 user_demand_error_UserDemand_#14,3,first_ >= 2
5 | storage_constraint_lower_Basin_#13,2_: 1 basin_storage_change_Basin_#13_ + 1 level_demand_error_Basin_#13,2,lower,first_ >= -250
6 | storage_constraint_upper_Basin_#13,2_: -1 basin_storage_change_Basin_#13_ + 1 level_demand_error_Basin_#13,2,upper,first_ >= -750
7 | user_demand_fairness_error_constraint_UserDemand_#14,3_: -1 user_demand_error_UserDemand_#14,3,first_ + 1 user_demand_error_UserDemand_#14,3,second_ + 1 average_flow_unit_error_3_ >= 0
8 | level_demand_fairness_error_constraint_Basin_#13,2,lower_: -0.001 level_demand_error_Basin_#13,2,lower,first_ + 1 level_demand_error_Basin_#13,2,lower,second_ + 1 average_storage_unit_error_2,lower_ >= 0
9 | level_demand_fairness_error_constraint_Basin_#13,2,upper_: -0.001 level_demand_error_Basin_#13,2,upper,first_ + 1 level_demand_error_Basin_#13,2,upper,second_ + 1 average_storage_unit_error_2,upper_ >= 0
10 | volume_conservation_Basin_#13_: 1 basin_storage_change_Basin_#13_ + 1 low_storage_factor_Basin_#13_ - 1 flow_(Outlet_#3,_Basin_#13)_ - 1 flow_(Pump_#6,_Basin_#13)_ + 1 flow_(Basin_#13,_UserDemand_#14)_ - 1 flow_(UserDemand_#14,_Basin_#13)_ = 1
11 | user_demand_allocated_sum_constraint_UserDemand_#14_: 1 flow_(Basin_#13,_UserDemand_#14)_ - 1 user_demand_allocated_UserDemand_#14,3_ = 0
12 | user_demand_return_flow_UserDemand_#14_: -0.5 flow_(Basin_#13,_UserDemand_#14)_ + 1 flow_(UserDemand_#14,_Basin_#13)_ = 0
13 | average_flow_unit_error_constraint_3_: -1 user_demand_error_UserDemand_#14,3,first_ + 1 average_flow_unit_error_3_ = 0
14 | average_storage_unit_error_constraint_2,upper_: -1 level_demand_error_Basin_#13,2,upper,first_ + 1000 average_storage_unit_error_2,upper_ = 0
15 | average_storage_unit_error_constraint_2,lower_: -1 level_demand_error_Basin_#13,2,lower,first_ + 1000 average_storage_unit_error_2,lower_ = 0
16 | Bounds
17 | -0.02 <= basin_storage_change_Basin_#13_ <= 0.08
18 | 0 <= low_storage_factor_Basin_#13_ <= 1
19 | 0 <= flow_(Outlet_#3,_Basin_#13)_ <= 0.001728
20 | 0 <= flow_(Pump_#6,_Basin_#13)_ <= 1.728
21 | 0 <= flow_(Basin_#13,_UserDemand_#14)_ <= 864000
22 | 0 <= flow_(UserDemand_#14,_Basin_#13)_ <= 864000
23 | 0 <= user_demand_allocated_UserDemand_#14,3_ <= 2
24 | 0 <= user_demand_error_UserDemand_#14,3,first_ <= 1
25 | 0 <= user_demand_error_UserDemand_#14,3,second_ <= 1
26 | level_demand_error_Basin_#13,2,lower,first_ >= 0
27 | level_demand_error_Basin_#13,2,lower,second_ >= 0
28 | level_demand_error_Basin_#13,2,upper,first_ >= 0
29 | level_demand_error_Basin_#13,2,upper,second_ >= 0
30 | 0 <= average_flow_unit_error_3_ <= 1
31 | average_storage_unit_error_2,lower_ >= 0
32 | average_storage_unit_error_2,upper_ >= 0
33 | End
34 |
--------------------------------------------------------------------------------