From cffe25f1abbcd07bb017414fa68cc0b969a5b5d3 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Sat, 1 Aug 2026 21:45:30 +0200 Subject: [PATCH 1/8] chore: trim verbose comments from pyproject.toml --- pyproject.toml | 68 ++++++++++++++++---------------------------------- 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd934ece..c5da01f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,45 +105,28 @@ include = ["hapi", "hapi.*"] [tool.setuptools.package-data] hapi = ["*.yaml", "include/gdal/*.h"] -# Ruff replaces black + isort + flake8 with one tool. Because a single engine -# formats and sorts imports against one line-length, the formatter and the -# import sorter cannot disagree the way black and isort did. Ruff lints and -# formats both .py and .ipynb natively. [tool.ruff] line-length = 88 -target-version = "py311" # requires-python floors at 3.11 +target-version = "py311" [tool.ruff.format] -# black had skip-string-normalization = true → preserve existing quotes. quote-style = "preserve" [tool.ruff.lint] -# The old flake8 select was E/F/W (B and T4 were inert: flake8-bugbear / -# flake8-print were never installed). Add I (isort) and UP (pyupgrade) to fold -# in the standalone isort hook and modern-typing upgrades. select = ["E", "F", "W", "I", "UP"] -# Mirror the old [tool.flake8].ignore. W503 is dropped — ruff does not -# implement it (formatter-owned pycodestyle code black conflicted with). ignore = ["E203", "E226", "E266", "E501", "E701", "E722", "C901", "E741", "E731"] [tool.ruff.lint.isort] -# Classify the import roots explicitly so sorting never depends on the filesystem. -# The legacy examples still import the pre-rename `Hapi.*` package: on a -# case-insensitive filesystem (Windows/macOS) ruff resolves `Hapi` to `src/hapi` and -# sorts those imports as first-party, while Linux CI finds no such package and sorts -# them as third-party — so the same file failed lint on whichever platform had not -# formatted it last. Naming both spellings pins the answer on every platform. +# Both spellings are named so sorting does not depend on filesystem case +# sensitivity: the legacy examples still import the pre-rename `Hapi.*` package. known-first-party = ["hapi"] known-third-party = ["Hapi"] [tool.ruff.lint.per-file-ignores] -# flake8 excluded examples/ and tests/ from linting — drop E/F/W and UP there, -# keeping only import-sorting (I), which isort did run everywhere. This keeps -# the migration's lint churn on shipped src/. +# Lint shipped src/ only; tests and examples keep import sorting (I) alone. "tests/**" = ["E", "F", "W", "UP"] "examples/**" = ["E", "F", "W", "UP"] -# Tutorial notebooks import next to first use and run setup before imports — -# both legitimately trip E402, which stays enforced for real modules. +# Notebooks import next to first use, which legitimately trips E402. "*.ipynb" = ["E402"] [tool.bandit] @@ -156,8 +139,7 @@ source = ["src/hapi"] [tool.coverage.report] show_missing = true -# Current branch coverage is ~52%; the floor locks that level in so it can only -# move up. Raise it as coverage improves. +# Locks in the current level; raise it as coverage improves. fail_under = 50 [tool.commitizen] @@ -180,10 +162,8 @@ style = [ ] [tool.mypy] -# Checker target is 3.12 even though `requires-python` floors at 3.11: numpy -# ships PEP 695 `type` statements in its stubs, which mypy only parses when the -# target is >= 3.12. This only widens the *typing* target; actual 3.11 runtime -# compatibility is covered by the py311 leg of the test matrix, not by mypy. +# 3.12, not the 3.11 floor: numpy's stubs use PEP 695 `type` statements, which +# mypy only parses at >= 3.12. The py311 test leg covers the runtime floor. python_version = "3.12" mypy_path = "src" packages = ["hapi"] @@ -209,20 +189,17 @@ module = [ "hapi.calibration", "hapi.wrapper", ] -# These modules use a builder pattern: attributes are typed X | None in -# __init__ and populated by separate read_*() methods before use. Methods -# assume attributes are non-None but mypy cannot verify call order. -# Fix: add assert-based narrowing helpers (e.g. _require_spatial_data()) -# that return non-None types, then remove these suppressions one by one. -# Error count without overrides: ~257 across these 4 modules. +# These modules are built by successive read_*() calls: attributes are typed +# X | None and populated before use, which mypy cannot verify. Removing the +# suppressions needs assert-based narrowing helpers first (~257 errors). disable_error_code = [ - "union-attr", # .attr on X | None (159 sites) - "attr-defined", # .attr on None (11 sites) - "arg-type", # passing X | None where X expected (13 sites) - "misc", # slice index, None not callable (13 sites) - "operator", # arithmetic on X | None (4 sites) - "index", # indexing X | None (2 sites) - "call-overload", # numpy overload mismatches on X | None (2 sites) + "union-attr", + "attr-defined", + "arg-type", + "misc", + "operator", + "index", + "call-overload", ] [[tool.mypy.overrides]] @@ -233,15 +210,12 @@ disable_error_code = ["empty-body"] [tool.pytest.ini_options] env = [ "MPLBACKEND=Agg", - # D: sets the value only when the variable is not already set, so an - # explicitly exported HAPI_DATA_DIR still wins over this default. + # D: only sets the default; an exported HAPI_DATA_DIR still wins. "D:HAPI_DATA_DIR=src/hapi/parameters", ] pythonpath = ["src"] testpaths = ["tests"] -# Fail (don't hang) on a stuck test: pytest-timeout aborts any single test that -# runs longer than `timeout` and prints its stack; `faulthandler_timeout` -# (stdlib) dumps an earlier all-thread traceback for diagnosis. +# Abort a stuck test instead of hanging CI. timeout = 300 faulthandler_timeout = 120 markers = [ @@ -280,7 +254,7 @@ description = "Run all test suite" [tool.pixi.tasks.notebooks] cmd = "pytest --nbval --nbval-lax --verbose -p no:cacheprovider examples" -description = "Check notebooks — currently red: every notebook still needs migration off the pre-rename Hapi package" +description = "Check notebooks (they still need migration off the pre-rename Hapi package)" [tool.pixi.tasks.publish-pypi] cmd = "twine upload --non-interactive --repository pypi dist/*" From 0abf6cd4fd4b355d6a37dab4b42ec745e9d4c71d Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Sat, 1 Aug 2026 22:21:19 +0200 Subject: [PATCH 2/8] ci: pin every action to the latest release SHA --- .github/workflows/github-pages-mkdocs.yml | 6 +++--- .github/workflows/github-release.yml | 2 +- .github/workflows/lint.yml | 8 ++++---- .github/workflows/pip-audit.yml | 2 +- .github/workflows/pure-wheel-test.yml | 10 +++++----- .github/workflows/pypi-release.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/github-pages-mkdocs.yml b/.github/workflows/github-pages-mkdocs.yml index 127982f0..0de666f3 100644 --- a/.github/workflows/github-pages-mkdocs.yml +++ b/.github/workflows/github-pages-mkdocs.yml @@ -27,7 +27,7 @@ jobs: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - uses: serapeum-org/github-actions/actions/mkdocs-deploy@38c111084f99d5ff5f4a1e63bedce48fe33a10c0 # mkdocs/v1.3.1 @@ -41,7 +41,7 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - uses: serapeum-org/github-actions/actions/mkdocs-deploy@38c111084f99d5ff5f4a1e63bedce48fe33a10c0 # mkdocs/v1.3.1 @@ -57,7 +57,7 @@ jobs: (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - uses: serapeum-org/github-actions/actions/mkdocs-deploy@38c111084f99d5ff5f4a1e63bedce48fe33a10c0 # mkdocs/v1.3.1 diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index c8577bb5..079dcf69 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ inputs.release-branch }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3546ae53..648247a1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -39,15 +39,15 @@ jobs: timeout-minutes: 15 steps: # No fetch-depth: `pre-commit run --all-files` scans the working tree, not history. - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" # Cache the hook toolchains (each hook repo builds an isolated env on first run). # Keyed on the config so a hook or rev bump rebuilds; restore-keys reuse the rest. - - uses: actions/cache@v4 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/pre-commit key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} @@ -76,7 +76,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/pip-audit.yml b/.github/workflows/pip-audit.yml index 872c036e..77b53616 100644 --- a/.github/workflows/pip-audit.yml +++ b/.github/workflows/pip-audit.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/pure-wheel-test.yml b/.github/workflows/pure-wheel-test.yml index 8dff06b0..a7212caa 100644 --- a/.github/workflows/pure-wheel-test.yml +++ b/.github/workflows/pure-wheel-test.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ inputs.branch || github.ref }} @@ -55,7 +55,7 @@ jobs: pixi run -e dev python -m zipfile -l dist/*.whl | grep "hapi/parameters/parameters.py" - name: Upload wheel artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheel path: dist/*.whl @@ -74,18 +74,18 @@ jobs: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ inputs.branch || github.ref }} - name: Download wheel artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: wheel path: dist/ - - uses: actions/setup-python@v6 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml index e1f883f0..3d8916e7 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/pypi-release.yml @@ -17,7 +17,7 @@ jobs: github.event.workflow_run.conclusion == 'success' }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ github.event.workflow_run.head_sha || github.sha }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b758fd85..148bbbf8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: OS: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -38,4 +38,4 @@ jobs: HAPI_DATA_DIR: ${{ github.workspace }}/src/hapi/parameters - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 From 2ec26c978295d2d802e328e131ff0bb752c07130 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Sat, 1 Aug 2026 23:20:23 +0200 Subject: [PATCH 3/8] docs: refresh the readme badges and installation instructions --- README.md | 97 +++++++++++++++++++++++-------------------------------- 1 file changed, 40 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index cd464715..9d5d4ad1 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,24 @@ -![GitHub release (latest by date)](https://img.shields.io/github/v/release/mafarrag/hapi) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5758979.svg)](https://doi.org/10.5281/zenodo.5758979) -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/MAfarrag/HAPI/master) -[![Python Versions](https://img.shields.io/pypi/pyversions/HAPI-Nile.png)](https://img.shields.io/pypi/pyversions/HAPI-Nile) -[![Documentation Status](https://readthedocs.org/projects/hapi-hm/badge/?version=latest)](https://hapi-hm.readthedocs.io/en/latest/?badge=latest) -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/MAfarrag/Hapi.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/MAfarrag/Hapi/context:python) - - -[![GitHub Clones](https://img.shields.io/badge/dynamic/json?color=success&label=Clone&query=count&url=https://github.com/MAfarrag/Hapi/blob/master/clone.json?raw=True&logo=github)](https://github.com/MShawon/github-clone-count-badge) [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/MAfarrag) - -Current build status -==================== - - - - - -
All platforms: - - - -
- -[![Build status](https://ci.appveyor.com/api/projects/status/rys2u0l1nbmfjuww?svg=true)](https://ci.appveyor.com/project/MAfarrag/hapi) +[![Tests](https://github.com/serapeum-org/Hapi/actions/workflows/tests.yml/badge.svg)](https://github.com/serapeum-org/Hapi/actions/workflows/tests.yml) +[![Lint](https://github.com/serapeum-org/Hapi/actions/workflows/lint.yml/badge.svg)](https://github.com/serapeum-org/Hapi/actions/workflows/lint.yml) [![codecov](https://codecov.io/gh/serapeum-org/Hapi/branch/main/graph/badge.svg?token=EMQSR7K2YV)](https://codecov.io/gh/serapeum-org/Hapi) -![GitHub last commit](https://img.shields.io/github/last-commit/MAfarrag/Hapi) -![GitHub forks](https://img.shields.io/github/forks/MAfarrag/hapi?style=social) -![GitHub Repo stars](https://img.shields.io/github/stars/MAfarrag/Hapi?style=social) -![AppVeyor tests (branch)](https://img.shields.io/appveyor/tests/MAfarrag/Ha%5Bi/hydraulic-model) - - -[![Github all releases](https://img.shields.io/github/downloads/Naereen/StrapDown.js/total.svg)](https://GitHub.com/Naereen/StrapDown.js/releases/) - -![Profile views](https://gpvc.arturio.dev/MAfarrag) - - -Current release info -==================== +[![Documentation](https://img.shields.io/badge/docs-serapeum--org.github.io%2FHapi-blue)](https://serapeum-org.github.io/Hapi) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5758979.svg)](https://doi.org/10.5281/zenodo.5758979) -| Name | Downloads | Version | Platforms | -| --- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | --- | -| [![Conda Recipe](https://img.shields.io/badge/recipe-hapi-green.svg)](https://anaconda.org/conda-forge/hapi) | [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/hapi.svg)](https://anaconda.org/conda-forge/hapi) [![Downloads](https://pepy.tech/badge/hapi-nile)](https://pepy.tech/project/hapi-nile) [![Downloads](https://pepy.tech/badge/hapi-nile/month)](https://pepy.tech/project/hapi-nile) [![Downloads](https://pepy.tech/badge/hapi-nile/week)](https://pepy.tech/project/hapi-nile) ![PyPI - Downloads](https://img.shields.io/pypi/dd/hapi-nile?color=blue&style=flat-square) ![GitHub all releases](https://img.shields.io/github/downloads/MAfarrag/Hapi/total) | [![Conda Version](https://img.shields.io/conda/vn/conda-forge/hapi.svg)](https://anaconda.org/conda-forge/hapi) [![PyPI version](https://badge.fury.io/py/HAPI-Nile.svg)](https://badge.fury.io/py/HAPI-Nile) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/hapi/badges/version.svg)](https://anaconda.org/conda-forge/hapi) | [![Conda Platforms](https://img.shields.io/conda/pn/conda-forge/hapi.svg)](https://anaconda.org/conda-forge/hapi) [![Join the chat at https://gitter.im/Hapi-Nile/Hapi](https://badges.gitter.im/Hapi-Nile/Hapi.svg)](https://gitter.im/Hapi-Nile/Hapi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | +[![PyPI version](https://img.shields.io/pypi/v/HAPI-Nile.svg)](https://pypi.org/project/HAPI-Nile/) +[![Conda Version](https://img.shields.io/conda/vn/conda-forge/hapi.svg)](https://anaconda.org/conda-forge/hapi) +[![Python Versions](https://img.shields.io/pypi/pyversions/HAPI-Nile.svg)](https://pypi.org/project/HAPI-Nile/) +[![Conda Platforms](https://img.shields.io/conda/pn/conda-forge/hapi.svg)](https://anaconda.org/conda-forge/hapi) +[![Downloads](https://static.pepy.tech/badge/hapi-nile)](https://pepy.tech/project/hapi-nile) +[![Downloads](https://static.pepy.tech/badge/hapi-nile/month)](https://pepy.tech/project/hapi-nile) +[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/hapi.svg)](https://anaconda.org/conda-forge/hapi) +[![GitHub last commit](https://img.shields.io/github/last-commit/serapeum-org/Hapi)](https://github.com/serapeum-org/Hapi/commits/main) +[![GitHub Repo stars](https://img.shields.io/github/stars/serapeum-org/Hapi?style=social)](https://github.com/serapeum-org/Hapi/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/serapeum-org/Hapi?style=social)](https://github.com/serapeum-org/Hapi/network/members) -![Hapi](/docs/img/Hapi4.png) ![Hapi](/docs/img/name.png) +![Hapi](docs/img/Hapi4.png) ![Hapi](docs/img/name.png) Hapi - Hydrological library for Python @@ -54,7 +28,7 @@ model & Muskingum routing method at a catchment scale (Farrag & Corzo, 2021), Ha (spatial discretization - cell size, temporal resolution, parameterization approaches and calibration (Farrag et al., 2021)). -![1](/docs/img/Picture1.png) ![2](/docs/img/Picture2.png) +![1](docs/img/Picture1.png) ![2](docs/img/Picture2.png) Hapi @@ -73,8 +47,8 @@ Main Features - Visualization module for animating the results of the distributed model, and the meteorological inputs - Optimization module, for calibrating the model based on the Harmony search method -The recent version of Hapi (Hapi 1.0.1) integrates the global hydrological parameters obtained by Beck et al., (2016), -to reduce model complexity and uncertainty of parameters. +Hapi integrates the global hydrological parameters obtained by Beck et al., (2016), to reduce model complexity +and uncertainty of parameters. Future work ------------- @@ -105,6 +79,16 @@ Rusli, S. R., Yudianto, D. & Liu, J. tao. (2015) Effects of temporal variability Installing hapi =============== +## pip + +To install the last release, use pip. The distribution is named `HAPI-Nile` and the import package is `hapi`. + +``` +pip install HAPI-Nile +``` + +## conda + Installing `hapi` from the `conda-forge` channel can be achieved by: ``` @@ -117,29 +101,28 @@ It is possible to list all of the versions of `hapi` available on your platform conda search hapi --channel conda-forge ``` -## Install from Github -to install the last development to time you can install the library from github -``` -pip install git+https://github.com/MAfarrag/HAPI -``` +## Install from GitHub + +To install the latest development version, install the library from GitHub: -## pip -to install the last release you can easly use pip ``` -pip install HAPI-Nile==1.6.0 +pip install git+https://github.com/serapeum-org/Hapi ``` Quick start =========== ``` - >>> import Hapi + >>> import hapi ``` -[other code samples](https://hapi-hm.readthedocs.io/en/latest/?badge=latest) +[other code samples](https://serapeum-org.github.io/Hapi) ## Naming Convention [PEP8](https://peps.python.org/pep-0008/#naming-conventions) - module names: lower case word, preferably one word if not, separate words with underscores (module.py, my_module.py). - class names: PascalCase (Model, MyClass). -- class method/function: CamelCase(getFile, readConfig).should have a verb one them, because they perform some action +- class method/function: snake_case (get_file, read_config). They should have a verb in them, because they perform some action. + +Some CamelCase entry points survive from earlier releases (for example `Run.RunHapi` and `Wrapper.RRMModel`) +because examples and downstream code still call them. New methods are written in snake_case. From bac2f29e141ba64f47e25bf758aca05ec79df405 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Sun, 2 Aug 2026 01:11:47 +0200 Subject: [PATCH 4/8] docs: update the documentation to the current api --- docs/api/distrrm.md | 4 + docs/dev/Installation.md | 157 +++++++---------- docs/examples/distributed-model-calib.md | 201 ++++++++-------------- docs/examples/distributed-model-run.md | 28 +-- docs/examples/lumped-model-calibration.md | 16 +- docs/examples/lumped-model-run.md | 24 +-- docs/examples/meteo-inputs.md | 106 ++---------- docs/examples/parameters.md | 16 +- docs/javascripts/mathjax.js | 12 ++ mkdocs.yml | 5 + 10 files changed, 210 insertions(+), 359 deletions(-) create mode 100644 docs/api/distrrm.md create mode 100644 docs/javascripts/mathjax.js diff --git a/docs/api/distrrm.md b/docs/api/distrrm.md new file mode 100644 index 00000000..d2b25681 --- /dev/null +++ b/docs/api/distrrm.md @@ -0,0 +1,4 @@ +# Distributed Rainfall-Runoff Model + +## DistributedRRM +::: hapi.rrm.distrrm.DistributedRRM diff --git a/docs/dev/Installation.md b/docs/dev/Installation.md index 020b9aa4..10c6b324 100644 --- a/docs/dev/Installation.md +++ b/docs/dev/Installation.md @@ -2,141 +2,113 @@ ## Stable release -Please install Hapi in a Virtual environment so that its requirements don't tamper with your system's python -`Hapi` works with all Python versions +Please install Hapi in a virtual environment so that its requirements do not tamper with your system's Python. +Hapi requires **Python 3.11 or newer**. -## conda -the easiest way to install `Hapi` is using `conda` package manager. `Hapi` is available in the [conda-forge](https://conda-forge.org/) channel. To install -you can use the following command: +The distribution is published as `HAPI-Nile`, and the import package is `hapi`. + +## pip ```shell -conda install -c conda-forge hapi +pip install HAPI-Nile ``` -If this works it will install Hapi with all dependencies including Python and gdal, -and you skip the rest of the installation instructions. +To install a specific release: -## Installing Python and gdal dependencies - -The main dependencies for Hapi are an installation of Python 2.7+, and gdal +```shell +pip install HAPI-Nile=={release} +``` -## Installing Python +## conda -For Python we recommend using the Anaconda Distribution for Python 3, which is available -for download from https://www.anaconda.com/download/. The installer gives the option to -add `python` to your `PATH` environment variable. We will assume in the instructions -below that it is available in the path, such that `python`, `pip`, and `conda` are -all available from the command line. +`Hapi` is also available in the [conda-forge](https://conda-forge.org/) channel: -Note that there is no hard requirement specifically for Anaconda's Python, but often it -makes installation of required dependencies easier using the conda package manager. +```shell +conda install -c conda-forge hapi +``` -## Install as a conda environment +## Dependencies -The easiest and most robust way to install Hapi is by installing it in a separate -conda environment. In the root repository directory there is an `environment.yml` file. -This file lists all dependencies. Either use the `environment.yml` file from the master branch -(please note that the master branch can change rapidly and break functionality without warning), -or from one of the releases {release}. +Hapi installs its dependencies automatically. GDAL does **not** need to be installed +separately: it ships vendored inside the `pyramids-gis` wheel, which Hapi depends on. -Run this command to start installing all Hapi dependencies: +You can check the versions of the libraries Hapi depends on at +[libraries.io](https://libraries.io/github/serapeum-org/Hapi). -```shell -conda env create -f environment.yml -``` +## Install from GitHub -This creates a new environment with the name `hapi`. To activate this environment in -a session, run: +To install the latest development version (the HEAD of the `main` branch): ```shell -activate hapi +pip install git+https://github.com/serapeum-org/Hapi.git ``` -For the installation of Hapi there are two options (from the Python Package Index (PyPI) -or from Github). To install a release of Hapi from the PyPI (available from release 2018.1): + +Or a specific release: ```shell -pip install HAPI-Nile=={release} +pip install git+https://github.com/serapeum-org/Hapi.git@{release} ``` ## From sources -The sources for HapiSM can be downloaded from the -[Github repo](https://github.com/serapeum-org/Hapi). +The sources can be downloaded from the [GitHub repo](https://github.com/serapeum-org/Hapi). -You can either clone the public repository: +Clone the public repository: ```shell -$ git clone git://github.com/serapeum-org/Hapi +git clone https://github.com/serapeum-org/Hapi.git ``` -Or download the [tarball](https://github.com/serapeum-org/Hapi/tarball/master): -```shell -$ curl -OJL https://github.com/serapeum-org/Hapi/tarball/master -``` -Once you have a copy of the source, you can install it with: +Or download the [tarball](https://github.com/serapeum-org/Hapi/tarball/main): ```shell -$ python setup.py install +curl -OJL https://github.com/serapeum-org/Hapi/tarball/main ``` +Once you have a copy of the source, install it with: -To install directly from GitHub (from the HEAD of the master branch): - -+ `pip install git+https://github.com/serapeum-org/Hapi.git` - -or from Github from a specific release: - -+ `pip install git+https://github.com/serapeum-org/Hapi.git@{release}` - -Now you should be able to start this environment's Python with `python`, try -`import Hapi` to see if the package is installed. - - -More details on how to work with conda environments can be found here: -https://conda.io/docs/user-guide/tasks/manage-environments.html - +```shell +cd Hapi +pip install . +``` -If you are planning to make changes and contribute to the development of Hapi, it is -best to make a git clone of the repository, and do a editable install in the location -of you clone. This will not move a copy to your Python installation directory, but -instead create a link in your Python installation pointing to the folder you installed -it from, such that any changes you make there are directly reflected in your install. +## Development install -+ `git clone https://github.com/serapeum-org/Hapi.git` -+ `cd Hapi` -+ `activate Hapi` -+ `pip install -e .` +If you are planning to make changes and contribute to the development of Hapi, make a +git clone of the repository and do an editable install, so that any change you make is +directly reflected in your environment: -Alternatively, if you want to avoid using `git` and simply want to test the latest -version from the `master` branch, you can replace the first line with downloading -a zip archive from GitHub: https://github.com/serapeum-org/Hapi/archive/master.zip -[libraries.io](https://libraries.io/github/serapeum-org/Hapi). +```shell +git clone https://github.com/serapeum-org/Hapi.git +cd Hapi +pip install -e . +``` -## Install using pip +### Using pixi -Besides the recommended conda environment setup described above, you can also install -Hapi with `pip`. For the more difficult to install Python dependencies, it is best to -use the conda package manager: +The repository is managed with [pixi](https://pixi.sh), which resolves the whole +environment (including the test and documentation tooling) from `pyproject.toml` and +`pixi.lock`. This is what CI runs, so it is the most reliable way to reproduce a +development environment: ```shell -conda install numpy scipy gdal netcdf4 pyproj +pixi install -e dev +pixi run -e dev test-all ``` -you can check [libraries.io](https://libraries.io/github/serapeum-org/Hapi). to check versions of the libraries - +Useful tasks: -Then install a release {release} of Hapi (available from release 2018.1) with pip: - -```shell -pip install HAPI-Nile=={release} -```` +| Task | Command | +| --- | --- | +| Run the main test suite | `pixi run -e dev main` | +| Run the whole test suite | `pixi run -e dev test-all` | +| Type check | `pixi run -e dev mypy` | +| Serve the documentation locally | `pixi run -e docs mkdocs serve` | ## Check if the installation is successful -To check it the install is successful, go to the examples directory and run the following command: - ```shell -`python -m Hapi.*******` +python -c "import hapi; print(hapi.__name__)" ``` This should run without errors. @@ -144,10 +116,5 @@ This should run without errors. > **Note:** - This documentation was generated on today - - Documentation for the development version: - https://Hapi.readthedocs.org/en/latest/ - - Documentation for the stable version: - https://Hapi.readthedocs.org/en/stable/ + The documentation is built with MkDocs and published to GitHub Pages: + https://serapeum-org.github.io/Hapi diff --git a/docs/examples/distributed-model-calib.md b/docs/examples/distributed-model-calib.md index 42c38bdb..4e4c0dd8 100644 --- a/docs/examples/distributed-model-calib.md +++ b/docs/examples/distributed-model-calib.md @@ -35,7 +35,7 @@ class Catchment: - To instantiate the object you need to provide the `name`, `statedate`, `enddate`, and the `SpatialResolution` ```python -from Hapi.catchment import Catchment +from hapi.catchment import Catchment start = "2009-01-01" end = "2011-12-31" @@ -61,17 +61,17 @@ ParPathRun = Path + "/Parameter set-Avg/" - Then use the each method in the object to read the coresponding data ```python -Coello.readRainfall(PrecPath) -Coello.readTemperature(TempPath) -Coello.readET(Evap_Path) -Coello.readFlowAcc(FlowAccPath) -Coello.readFlowDir(FlowDPath) +Coello.read_rainfall(PrecPath) +Coello.read_temperature(TempPath) +Coello.read_et(Evap_Path) +Coello.read_flow_acc(FlowAccPath) +Coello.read_flow_dir(FlowDPath) ``` - To read the parameters you need to provide whether you need to consider the snow subroutine or not ```python Snow = 0 -Coello.readParameters(ParPathRun, Snow) +Coello.read_parameters(ParPathRun, Snow) ``` ## 2- Lumped Model @@ -80,23 +80,23 @@ Coello.readParameters(ParPathRun, Snow) and define the initial condition, and catchment area. ```python -from Hapi.rrm.hbv_bergestrom92 import HBVBergestrom92 as HBV +from hapi.rrm.hbv_bergestrom92 import HBVBergestrom92 as HBV CatchmentArea = 1530 InitialCond = [0,5,5,5,0] -Coello.readLumpedModel(HBV, CatchmentArea, InitialCond) +Coello.read_lumped_model(HBV, CatchmentArea, InitialCond) ``` - If the Inpus are consistent in dimensions you will get a the following message -![check_inputs](../img/check_inputs.PNG) +![check_inputs](../img/check_inputs.png) - to check the performance of the model we need to read the gauge hydrographs ```python -Coello.readGaugeTable("Hapi/Data/00inputs/Discharge/stations/gauges.csv", FlowAccPath) +Coello.read_gauge_table("Hapi/Data/00inputs/Discharge/stations/gauges.csv", FlowAccPath) GaugesPath = "Hapi/Data/00inputs/Discharge/stations/" -Coello.readDischargeGauges(GaugesPath, column='id', fmt="%Y-%m-%d") +Coello.read_discharge_gauges(GaugesPath, column='id', fmt="%Y-%m-%d") ``` ## 3-Run Object @@ -105,7 +105,7 @@ Coello.readDischargeGauges(GaugesPath, column='id', fmt="%Y-%m-%d") - import the Run object and use the `Catchment` object as a parameter to the `Run` object, then call the RunHapi method to start the simulation ```python -from Hapi.run import Run +from hapi.run import Run Run.RunHapi(Coello) ``` - the result of the simulation will be stored as attributes in the Catchment object as follow @@ -132,10 +132,10 @@ Outputs: ## 4-Extract Hydrographs - The final step is to extract the simulated Hydrograph from the cells at the location of the gauges to compare -- The `extractDischarge` method extracts the hydrographs, however you have to provide in the gauge file the coordinates of the gauges with the same coordinate system of the `FlowAcc` raster +- The `extract_discharge` method extracts the hydrographs, however you have to provide in the gauge file the coordinates of the gauges with the same coordinate system of the `FlowAcc` raster ```python -Coello.extractDischarge(Factor=Coello.GaugesTable['area ratio'].tolist()) +Coello.extract_discharge(factor=Coello.GaugesTable['area ratio'].tolist()) for i in range(len(Coello.GaugesTable)): gaugeid = Coello.GaugesTable.loc[i,'id'] @@ -149,155 +149,92 @@ for i in range(len(Coello.GaugesTable)): print("Pearson CC= " + str(round(Coello.Metrics.loc['Pearson-CC',gaugeid],2))) print("R2 = " + str(round(Coello.Metrics.loc['R2',gaugeid],2))) ``` -- The `extractDischarge` will print the performance metics +- The `extract_discharge` will print the performance metics ## 5-Visualization - Firts type of visualization we can do with the results is to compare the gauge hydrograph with the simulatied hydrographs -- Call the `plotHydrograph` method and provide the period you want to visualize with the order of the gauge +- Call the `plot_hydrograph` method and provide the period you want to visualize with the order of the gauge ```python gaugei = 5 plotstart = "2009-01-01" plotend = "2011-12-31" -Coello.plotHydrograph(plotstart, plotend, gaugei) +Coello.plot_hydrograph(plotstart, plotend, gaugei) ``` ![hydrograph](../img/hydrograph.png) ## 6-Animation -- the best way to visualize time series of distributed data is through visualization, for theis reason, The `Catchment` object has `plotDistributedResults` method which can animate all the results of the model +- The best way to visualize a time series of distributed data is an animation. The `Catchment` object + has a `plot_distributed_results` method which animates any of the model results. -```python -""" -============================================================================= -AnimateArray(Arr, Time, NoElem, TicksSpacing = 2, Figsize=(8,8), PlotNumbers=True, - NumSize= 8, Title = 'Total Discharge',titlesize = 15, Backgroundcolorthreshold=None, - cbarlabel = 'Discharge m3/s', cbarlabelsize = 12, textcolors=("white","black"), - Cbarlength = 0.75, Interval = 200,cmap='coolwarm_r', Textloc=[0.1,0.2], - Gaugecolor='red',Gaugesize=100, ColorScale = 1,gamma=1./2.,linthresh=0.0001, - linscale=0.001, midpoint=0, orientation='vertical', rotation=-90,IDcolor = "blue", - IDsize =10, **kwargs) -============================================================================= -Parameters ----------- -Arr : [array] - the array you want to animate. -Time : [dataframe] - dataframe contains the date of values. -NoElem : [integer] - Number of the cells that has values. -TicksSpacing : [integer], optional - Spacing in the colorbar ticks. The default is 2. -Figsize : [tuple], optional - figure size. The default is (8,8). -PlotNumbers : [bool], optional - True to plot the values intop of each cell. The default is True. -NumSize : integer, optional - size of the numbers plotted intop of each cells. The default is 8. -Title : [str], optional - title of the plot. The default is 'Total Discharge'. -titlesize : [integer], optional - title size. The default is 15. -Backgroundcolorthreshold : [float/integer], optional - threshold value if the value of the cell is greater, the plotted - numbers will be black and if smaller the plotted number will be white - if None given the maxvalue/2 will be considered. The default is None. -textcolors : TYPE, optional - Two colors to be used to plot the values i top of each cell. The default is ("white","black"). -cbarlabel : str, optional - label of the color bar. The default is 'Discharge m3/s'. -cbarlabelsize : integer, optional - size of the color bar label. The default is 12. -Cbarlength : [float], optional - ratio to control the height of the colorbar. The default is 0.75. -Interval : [integer], optional - number to controlthe speed of the animation. The default is 200. -cmap : [str], optional - color style. The default is 'coolwarm_r'. -Textloc : [list], optional - location of the date text. The default is [0.1,0.2]. -Gaugecolor : [str], optional - color of the points. The default is 'red'. -Gaugesize : [integer], optional - size of the points. The default is 100. -IDcolor : [str] - the ID of the Point.The default is "blue". -IDsize : [integer] - size of the ID text. The default is 10. -ColorScale : integer, optional - there are 5 options to change the scale of the colors. The default is 1. - 1- ColorScale 1 is the normal scale - 2- ColorScale 2 is the power scale - 3- ColorScale 3 is the SymLogNorm scale - 4- ColorScale 4 is the PowerNorm scale - 5- ColorScale 5 is the BoundaryNorm scale - ------------------------------------------------------------------ - gamma : [float], optional - value needed for option 2 . The default is 1./2.. - linthresh : [float], optional - value needed for option 3. The default is 0.0001. - linscale : [float], optional - value needed for option 3. The default is 0.001. - midpoint : [float], optional - value needed for option 5. The default is 0. - ------------------------------------------------------------------ -orientation : [string], optional - orintation of the colorbar horizontal/vertical. The default is 'vertical'. -rotation : [number], optional - rotation of the colorbar label. The default is -90. -**kwargs : [dict] - keys: - Points : [dataframe]. - dataframe contains two columns 'cell_row', and cell_col to - plot the point at this location - -Returns -------- -animation.FuncAnimation. +The keyword arguments are forwarded to `cleopatra.array_glyph.ArrayGlyph.animate`; see its +documentation for the full list. The commonly used ones are `figsize`, `interval`, `cmap`, +`ticks_spacing`, `color_scale` (`"linear"`, `"power"`, `"sym-lognorm"`, `"boundary-norm"`, +`"midpoint"`), `display_cell_value`, `background_color_threshold`, `text_loc`, `point_color`, +`pid_color` and `pid_size`. -""" -``` -- choose the period of time you want to animate and the result (total discharge, upper zone discharge, soil moisture,...) +`option` selects the variable to animate: + +| option | variable | option | variable | +| --- | --- | --- | --- | +| 1 | Total discharge | 7 | Lower zone | +| 2 | Upper zone discharge | 8 | Water content | +| 3 | Ground water | 9 | Precipitation | +| 4 | Snow pack | 10 | Evapotranspiration | +| 5 | Soil moisture | 11 | Temperature | +| 6 | Upper zone | | | ```python plotstart = "2009-01-01" -plotend = "2009-02-01" - -Anim = Coello.plotDistributedResults(plotstart, plotend, Figsize=(9,9), Option = 1,threshold=160, PlotNumbers=True, - TicksSpacing = 5,Interval = 200, Gauges=True, cmap='inferno', Textloc=[0.1,0.2], - Gaugecolor='red',ColorScale = 1, IDcolor='blue', IDsize=25) +plotend = "2009-04-20" + +anim = Coello.plot_distributed_results( + plotstart, + plotend, + option=1, + gauges=True, + figsize=(9, 9), + ticks_spacing=5, + interval=200, + cmap="inferno", + color_scale="linear", + display_cell_value=True, + text_loc=[0.1, 0.2], + point_color="red", + pid_color="blue", + pid_size=25, +) ``` ![Animation](../img/anim.gif) -- to save the animation +- To save the animation, the output format is taken from the file extension. GIF is written with + Pillow; `mov`, `avi` and `mp4` need [FFmpeg](https://ffmpeg.org/) installed and available on your + system. - - Please visit https://ffmpeg.org/ and download a version of ffmpeg compitable with your operating system - - Copy the content of the folder and paste it in the "c:/user/.matplotlib/ffmpeg-static/" - or - - - define the path where the downloaded folder "ffmpeg-static" exist to matplotlib using the following lines - -```python -import matplotlib as mpl -mpl.rcParams['animation.ffmpeg_path'] = "path where you saved the ffmpeg.exe/ffmpeg.exe" -``` ```python -Path = SaveTo + "anim.gif" -Coello.saveAnimation(VideoFormat="gif",Path=Path,SaveFrames=3) +Coello.save_animation("results/anim.gif", fps=2) ``` ## 7-Save the result into rasters - To save the results as rasters provide the period and the path ```python -StartDate = "2009-01-01" -EndDate = "2010-04-20" -Prefix = 'Qtot_' - -Coello.saveResults(FlowAccPath, Result=1, StartDate=StartDate, EndDate=EndDate, Path="F:/02Case studies/Coello/Hapi/Model/results/", Prefix=Prefix) +start = "2009-01-01" +end = "2010-04-20" +prefix = "Qtot_" + +Coello.save_results( + FlowAccPath, + result=1, + start=start, + end=end, + path="results/", + prefix=prefix, +) ``` diff --git a/docs/examples/distributed-model-run.md b/docs/examples/distributed-model-run.md index 7e0f5dcf..7513163d 100644 --- a/docs/examples/distributed-model-run.md +++ b/docs/examples/distributed-model-run.md @@ -5,8 +5,8 @@ After preparing all the meteorological, GIS inputs required for the model, and E import numpy as np import datetime as dt from osgeo import gdal -from Hapi.calibration import Calibration -import Hapi.rrm.hbv_bergestrom92 as HBV +from hapi.calibration import Calibration +import hapi.rrm.hbv_bergestrom92 as HBV import statista.descriptors as metrics @@ -32,20 +32,20 @@ name = "Coello" Coello = Calibration(name, Sdate, Edate, SpatialResolution = "Distributed") # Meteorological & GIS Data -Coello.readRainfall(PrecPath) -Coello.readTemperature(TempPath) -Coello.readET(Evap_Path) +Coello.read_rainfall(PrecPath) +Coello.read_temperature(TempPath) +Coello.read_et(Evap_Path) -Coello.readFlowAcc(FlowAccPath) -Coello.readFlowDir(FlowDPath) +Coello.read_flow_acc(FlowAccPath) +Coello.read_flow_dir(FlowDPath) # Lumped Model -Coello.readLumpedModel(HBV, AreaCoeff, InitialCond) +Coello.read_lumped_model(HBV, AreaCoeff, InitialCond) # Gauges Data -Coello.readGaugeTable(Path+"/stations/gauges.csv", FlowAccPath) +Coello.read_gauge_table(Path+"/stations/gauges.csv", FlowAccPath) GaugesPath = Path+"/stations/" -Coello.readDischargeGauges(GaugesPath, column='id', fmt="%Y-%m-%d") +Coello.read_discharge_gauges(GaugesPath, column='id', fmt="%Y-%m-%d") @@ -55,7 +55,7 @@ Coello.readDischargeGauges(GaugesPath, column='id', fmt="%Y-%m-%d") - The `DistParameters` distribute the parameter vector on the cells following some spatial logic (same set of parameters for all cells, different parameters for each cell, HRU, different parameters for each class in an additional map) ```python -from Hapi.rrm.distparameters import DistParameters as DP +from hapi.rrm.parameters import Parameters as DP raster = gdal.Open(FlowAccPath) #------------- @@ -84,7 +84,7 @@ coordinates = Coello.GaugesTable[['id','x','y','weight']][:] OF_args = [coordinates] def objective_function(Qobs, Qout, q_uz_routed, q_lz_trans, coordinates): - Coello.extractDischarge() + Coello.extract_discharge() all_errors=[] # error for all internal stations for i in range(len(coordinates)): @@ -121,12 +121,12 @@ OptimizationArgs=[ApiObjArgs, pll_type, ApiSolveArgs] ## Run Calibration algorithm ```python -cal_parameters = Coello.runCalibration(SpatialVarFun, OptimizationArgs,printError=0) +cal_parameters = Coello.run_calibration(SpatialVarFun, OptimizationArgs,printError=0) ``` ## Save results ```python SpatialVarFun.Function(Coello.Parameters, kub=SpatialVarFun.Kub, klb=SpatialVarFun.Klb) -SpatialVarFun.saveParameters(SaveTo) +SpatialVarFun.save_parameters(SaveTo) ``` diff --git a/docs/examples/lumped-model-calibration.md b/docs/examples/lumped-model-calibration.md index ab871238..ca76a97b 100644 --- a/docs/examples/lumped-model-calibration.md +++ b/docs/examples/lumped-model-calibration.md @@ -6,10 +6,10 @@ To calibrate the HBV lumped model inside Hapi you need to follow the same steps ```python import pandas as pd import datetime as dt - import Hapi.rrm.hbv_bergestrom92 as HBVLumped - from Hapi.calibration import Calibration - from Hapi.routing import Routing - from Hapi.run import Run + import hapi.rrm.hbv_bergestrom92 as HBVLumped + from hapi.calibration import Calibration + from hapi.routing import Routing + from hapi.run import Run import statista.metrics as metrics Parameterpath = Comp + "/data/lumped/Coello_Lumped2021-03-08_muskingum.txt" @@ -21,7 +21,7 @@ To calibrate the HBV lumped model inside Hapi you need to follow the same steps name = "Coello" Coello = Calibration(name, start, end) - Coello.readLumpedInputs(MeteoDataPath) + Coello.read_lumped_inputs(MeteoDataPath) # catchment area @@ -31,7 +31,7 @@ To calibrate the HBV lumped model inside Hapi you need to follow the same steps InitialCond = [0,10,10,10,0] # no snow subroutine Snow = 0 - Coello.readLumpedModel(HBVLumped, AreaCoeff, InitialCond) + Coello.read_lumped_model(HBVLumped, AreaCoeff, InitialCond) # Calibration boundaries UB = pd.read_csv(Path + "/lumped/UB-3.txt", index_col = 0, header = None) @@ -41,7 +41,7 @@ To calibrate the HBV lumped model inside Hapi you need to follow the same steps LB = LB[1].tolist() Maxbas = True - Coello.readParametersBounds(UB, LB, Snow, Maxbas=Maxbas) + Coello.read_parameters_bound(UB, LB, Snow, Maxbas=Maxbas) parameters = [] # Routing @@ -52,7 +52,7 @@ To calibrate the HBV lumped model inside Hapi you need to follow the same steps ### Objective function # outlet discharge - Coello.readDischargeGauges(Path+"Qout_c.csv", fmt="%Y-%m-%d") + Coello.read_discharge_gauges(Path+"Qout_c.csv", fmt="%Y-%m-%d") OF_args=[] OF=metrics.rmse diff --git a/docs/examples/lumped-model-run.md b/docs/examples/lumped-model-run.md index b26e15c2..45466ba6 100644 --- a/docs/examples/lumped-model-run.md +++ b/docs/examples/lumped-model-run.md @@ -4,10 +4,10 @@ To run the HBV lumped model inside Hapi you need to prepare the meteorological i - First load the prepared lumped version of the HBV module inside Hapi, the triangular routing function and the wrapper function that runs the lumped model `RUN`. ```python -import Hapi.rrm.hbv_bergestrom92 as HBVLumped -from Hapi.run import Run -from Hapi.catchment import Catchment -from Hapi.routing import Routing +import hapi.rrm.hbv_bergestrom92 as HBVLumped +from hapi.run import Run +from hapi.catchment import Catchment +from hapi.routing import Routing ``` - read the meteorological data, data has be in the form of numpy array with the following order [rainfall, ET, Temp, Tm], ET is the potential evapotranspiration, Temp is the temperature (C), and Tm is the long term monthly average temperature. @@ -20,7 +20,7 @@ start = "2009-01-01" end = "2011-12-31" name = "Coello" Coello = Catchment(name, start, end) -Coello.readLumpedInputs(MeteoDataPath) +Coello.read_lumped_inputs(MeteoDataPath) ``` - Meteorological data @@ -29,7 +29,7 @@ start = "2009-01-01" end = "2011-12-31" name = "Coello" Coello = Catchment(name, start, end) -Coello.readLumpedInputs(MeteoDataPath) +Coello.read_lumped_inputs(MeteoDataPath) ``` - Lumped model prepare the initial conditions, cathcment area and the lumped model. @@ -40,7 +40,7 @@ AreaCoeff = 1530 # [Snow pack, Soil moisture, Upper zone, Lower Zone, Water content] InitialCond = [0,10,10,10,0] -Coello.readLumpedModel(HBVLumped, AreaCoeff, InitialCond) +Coello.read_lumped_model(HBVLumped, AreaCoeff, InitialCond) ``` - Load the pre-estimated parameters snow option (if you want to simulate snow accumulation and snow melt or not) @@ -48,7 +48,7 @@ Coello.readLumpedModel(HBVLumped, AreaCoeff, InitialCond) ```python Snow = 0 # no snow subroutine # if routing using Maxbas True, if Muskingum False -Coello.readParameters(Parameterpath, Snow) +Coello.read_parameters(Parameterpath, Snow) ``` - Prepare the routing options. @@ -92,16 +92,16 @@ To plot the calculated and measured discharge import matplotlib gaugei = 0 plotstart = "2009-01-01" plotend = "2011-12-31" -Coello.plotHydrograph(plotstart, plotend, gaugei, Title= "Lumped Model") +Coello.plot_hydrograph(plotstart, plotend, gaugei, Title= "Lumped Model") ``` ![lumped-model](../img/lumpedmodel.png) - To save the results ```python -StartDate = "2009-01-01" -EndDate = "2010-04-20" +start = "2009-01-01" +end = "2010-04-20" Path = SaveTo + "Results-Lumped-Model" + str(dt.datetime.now())[0:10] + ".txt" -Coello.saveResults(Result=5, StartDate=StartDate, EndDate=EndDate, Path=Path) +Coello.save_results(result=5, start=start, end=end, path=Path) ``` diff --git a/docs/examples/meteo-inputs.md b/docs/examples/meteo-inputs.md index dc8bc1bb..34723115 100644 --- a/docs/examples/meteo-inputs.md +++ b/docs/examples/meteo-inputs.md @@ -15,99 +15,25 @@ To be able to run the hydrologic simulation with Hapi the following meteorologic Distributed meteorological data can be obtain from gauge data with some interpolation method or from remote sensing data -# Remote Sensing Module +# Remote Sensing Data -The remote sensing module includes two classes to download ECMWF, and CHRIPS data +The remote sensing module that used to download CHIRPS and ECMWF data was moved out of +Hapi into its own package, [earth2observe](https://pypi.org/project/earth2observe/): -# CHRIPS -The Climate Hazards Group InfraRed Precipitation with Station data (CHIRPS) is a quasi-global rainfall data set. As its title suggests it combines data from real-time observing meteorological stations with infra-red data to estimate precipitation. The data set runs from 1981 to the near present. - -CHIRPS incorporates 0.05° resolution satellite imagery with in-situ station data to create gridded rainfall time series for trend analysis and seasonal drought monitoring - -There are two main data sets. The first is quasi-global and covers the whole world from 50°N to 50°S. The second covers Africa and parts of the Middle-East. It covers the area from 40°N to 40°S and from 20°W to 55°E. The global data set has data on a 0.05° grid at monthly, pentad and daily times steps. This is equivalent to 31 km2. The ‘Africa’ data set also includes data at a 0.10° grid at a 6-hour time step. - -CHRIPS data are uploaded into a ftp server therefore and can be downloaded through the `CHRIPS` class in the `remotesensing` module - -- First import the class from the remotesensing module - -```python -from Hapi.remotesensing import CHIRPS -``` - -- Create the object with the following information - - Period of time (start and end date) - - Temporal resolution (daily/monthy) - - Extend (Longitude/Latitude) - - Path (directory to save the downloaded data) - -```python -StartDate = '2009-01-01' -EndDate = '2009-01-10' -Time = 'daily' -lat = [4.190755,4.643963] -lon = [-75.649243,-74.727286] -Path = "directory to save the data" -Coello = CHIRPS(StartDate=StartDate, EndDate=EndDate, Time=Time, - latlim=lat , lonlim=lon, Path=Path) -``` - -- Call the `Download` method - -```python -Coello.Download() +```shell +pip install earth2observe ``` -- A Progress bar will appear and be updated with percent of the download -![progress](../img/progress.png) - -- If the period is long and the Download method can run in parallel, to activate the parallel mode enter the number of cores with the keyword argument `cores` - -```python -Coello.Download(cores=4) -``` +earth2observe covers the same sources Hapi used to handle: -# ECMWF -ERA-Interim data set is a global atmospheric reanalysis that is available from 1 January 1979 to 31 August 2019 +- **CHIRPS** — the Climate Hazards Group InfraRed Precipitation with Station data, a + quasi-global rainfall data set combining satellite imagery with in-situ station data + on a 0.05 degree grid, from 1981 to near present. +- **ECMWF** — the ERA-Interim archive, which requires a registered account and an API + key set up on your machine (see the + [ECMWF registration](https://apps.ecmwf.int/registration/) and the + [API key instructions](https://confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets#AccessECMWFPublicDatasets-key)). -The ERA-Interim data assimilation and forecast suite produces: -• four analyses per day, at 00, 06, 12 and 18 UTC; -• two 10-day forecasts per day, initialized from analyses at 00 and 12 UTC - -- Most archived ERA-Interim data can be downloaded from the ECMWF Data Server at [http://data.ecmwf.int/data](http://data.ecmwf.int/data). - -- The ERA-Interim Archive is part of ECMWF’s Meteorological Archive and Retrieval System (MARS), which is accessible to registered users -- The RemoteSensing and the ECMWF classes can retrieve the data from the ECMWF servers, if you are registered and setup the API Key in your machine - - -so inorder to be able to use the following code to download ECMWF data you need to -- register and setup your account on the [ECMWF website](https://apps.ecmwf.int/registration/). - -- Install ECMWF key — [instructions here](https://confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets#AccessECMWFPublicDatasets-key). - -- ERA-Interim data set has a lot of meteorological variables which you can download -- You need to provide the name of the variable using the `Variables` object -- `Variables` contains the tame of the variable you need to give to the `ECMWF` object to get and the unit and description - -```python -from Hapi.remotesensing import Variables -Vars = Variables('daily') -Vars.__str__() -``` - -For information about ECMWF data, see [https://apps.ecmwf.int/codes/grib/param-db/](https://apps.ecmwf.int/codes/grib/param-db/). - -```python -StartDate = '2009-01-01' -EndDate = '2009-01-10' -Time = 'daily' -lat = [4.190755,4.643963] -lon = [-75.649243,-74.727286] -Path = "/data/satellite_data/" -# Temperature, Evapotranspiration -variables = ['T','E'] - -Coello = RS(StartDate=StartDate, EndDate=EndDate, Time=Time, - latlim=lat , lonlim=lon, Path=Path, Vars=variables) - -Coello.ECMWF(Waitbar=1) -``` +Once the rasters are downloaded, prepare them for the model with `hapi.inputs.Inputs`, +which aligns every raster to the catchment DEM — see +[GIS inputs](gis-inputs.md) and [Parameters](parameters.md). diff --git a/docs/examples/parameters.md b/docs/examples/parameters.md index afad7954..ae905bad 100644 --- a/docs/examples/parameters.md +++ b/docs/examples/parameters.md @@ -11,7 +11,7 @@ The only input we need to extract parameters to our catchment is the DEM or any - import the class from the inputs module ```python -from Hapi.inputs import Inputs +from hapi.inputs import Inputs ``` - define the paths to the DEM and the directory to save the parameters @@ -19,10 +19,10 @@ from Hapi.inputs import Inputs dem_path = "../../data/GIS/Hapi_GIS_Data/acc4000.tif" outputpath = "../../data/parameters/03/" ``` -- call the `extractParameters` method +- call the `extract_parameters` method ```python -Inputs.extractParameters(dem_path, '03', AsRaster=True, SaveTo=outputpath) +Inputs.extract_parameters(dem_path, '03', AsRaster=True, SaveTo=outputpath) ``` @@ -36,7 +36,7 @@ To Extract the parameters range needed for the Calibration you have to prepare a import geopandas as gpd import numpy as np import pandas as pd -import Hapi.inputs as IN +import hapi.inputs as IN BasinF = "Path to shapefile" Basin = gpd.read_file(BasinF) @@ -45,16 +45,16 @@ ind = ["tt","sfcf","cfmax","cwh","cfr","fc","beta","lp","k0","k1","k2","uzl","pe Par = pd.DataFrame(index = ind) ``` -the `inputs` module in Hapi has a `extractParametersBoundaries` method to overlay the basin shapefile with the global parameters rasters and extract the max and min parameter values within the basin and plots your basin shapefile in top of the world map to make sure of the projection transformation from whatever projection your basin shapefile to the `WGS64` that the parameters rasters have +the `inputs` module in Hapi has a `extract_parameters_boundaries` method to overlay the basin shapefile with the global parameters rasters and extract the max and min parameter values within the basin and plots your basin shapefile in top of the world map to make sure of the projection transformation from whatever projection your basin shapefile to the `WGS64` that the parameters rasters have ```python # extract parameters boundaries -Par['UB'], Par['LB'] = IN.extractParametersBoundaries(Basin) +Par['UB'], Par['LB'] = IN.extract_parameters_boundaries(Basin) ``` -To extract the parameters from one of the ten scenarios developed to derive the Global model `extractParameters` method takes the number of the scenario as a string and return the parameters +To extract the parameters from one of the ten scenarios developed to derive the Global model `extract_parameters` method takes the number of the scenario as a string and return the parameters ```python # extract parameters in a specific scenarion from the 10 scenarios -Par['1'] = IN.extractParameters(Basin,"01") +Par['1'] = IN.extract_parameters(Basin,"01") ``` the extracted parameters needs to be modified incase you are not considering the snow bucket the first 5 parameters are disregarded diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 00000000..80e81ba5 --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,12 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } +}; diff --git a/mkdocs.yml b/mkdocs.yml index 232f53ad..9e10cca6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,11 @@ plugins: group_by_category: false inherited_members: false docstring_style: google + docstring_options: + # The docstrings write returns as "type: description", not + # "name: description", so griffe must not read the leading token + # as a return value name. + returns_named_value: false - table-reader - tags - mike: From cd09479457218ab365e47641c7e301eb9d3c0cb8 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Sun, 2 Aug 2026 01:11:57 +0200 Subject: [PATCH 5/8] docs: fix the griffe warnings in the api reference --- src/hapi/calibration.py | 25 +++++++++++++++++++++---- src/hapi/catchment.py | 4 ++-- src/hapi/inputs.py | 2 +- src/hapi/routing.py | 14 +++++++------- src/hapi/rrm/base_model.py | 34 +++++++++++++++++----------------- src/hapi/rrm/distrrm.py | 10 +++++----- src/hapi/rrm/hbv.py | 38 +++++++++++++++++++------------------- src/hapi/rrm/parameters.py | 29 +++++++++++++++++++---------- src/hapi/run.py | 11 +++++++---- 9 files changed, 98 insertions(+), 69 deletions(-) diff --git a/src/hapi/calibration.py b/src/hapi/calibration.py index 277e9824..ef87515c 100644 --- a/src/hapi/calibration.py +++ b/src/hapi/calibration.py @@ -78,7 +78,9 @@ def __init__( self.OFArgs: list | None = None self.OFvalue: float | None = None - def read_objective_function(self, objective_function: Callable[..., Any], args): + def read_objective_function( + self, objective_function: Callable[..., Any], args: list | None + ): """Read and store the objective function and its arguments. Takes the objective function and any additional arguments that @@ -157,7 +159,12 @@ def extract_discharge( # return error - def run_calibration(self, SpatialVarFun, OptimizationArgs, printError=None): + def run_calibration( + self, + SpatialVarFun: Callable[..., Any], + OptimizationArgs: list, + printError: int | None = None, + ): """Run the calibration algorithm for the distributed hydrological model. Executes the Harmony Search optimization algorithm to calibrate @@ -312,7 +319,12 @@ def opt_fun(par): return res - def FW1Calibration(self, SpatialVarFun, OptimizationArgs, printError=None): + def FW1Calibration( + self, + SpatialVarFun: Callable[..., Any], + OptimizationArgs: list, + printError: int | None = None, + ): """Run calibration using the FW1 (Focussed Width-1) routing scheme. Executes the Harmony Search optimization algorithm to calibrate @@ -448,7 +460,12 @@ def opt_fun(par): return res - def lumpedCalibration(self, Basic_inputs, OptimizationArgs, printError=None): + def lumpedCalibration( + self, + Basic_inputs: dict, + OptimizationArgs: list, + printError: int | None = None, + ): """Run the calibration algorithm for the lumped hydrological model. Executes the Harmony Search optimization algorithm to calibrate diff --git a/src/hapi/catchment.py b/src/hapi/catchment.py index 31c9ba6a..cc4fa198 100644 --- a/src/hapi/catchment.py +++ b/src/hapi/catchment.py @@ -19,7 +19,7 @@ import inspect import math import os -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import geopandas as gpd import matplotlib.dates as dates @@ -1197,7 +1197,7 @@ def plot_distributed_results( fmt: str = "%Y-%m-%d", option: int = 1, gauges: bool = False, - **kwargs, + **kwargs: Any, ): """Animate distributed model results or meteorological inputs. diff --git a/src/hapi/inputs.py b/src/hapi/inputs.py index 6f42d05c..ac77e6b1 100644 --- a/src/hapi/inputs.py +++ b/src/hapi/inputs.py @@ -249,7 +249,7 @@ def extract_parameters( @staticmethod def create_lumped_inputs( path: str, - regex_string=r"\d{4}.\d{2}.\d{2}", + regex_string: str = r"\d{4}.\d{2}.\d{2}", date: bool = True, file_name_data_fmt: str | None = None, start: str | None = None, diff --git a/src/hapi/routing.py b/src/hapi/routing.py index 4331f998..a052cc0f 100644 --- a/src/hapi/routing.py +++ b/src/hapi/routing.py @@ -62,7 +62,7 @@ def muskingum(inflow, Qinitial, k, x, dt): Returns: numpy.ndarray: Routed outflow hydrograph with the same - length as ``inflow``, rounded to four decimal places. + length as ``inflow``, rounded to four decimal places. Examples: >>> import numpy as np @@ -119,7 +119,7 @@ def muskingum_v( Returns: numpy.ndarray: Routed outflow hydrograph with the same - length as ``inflow``. + length as ``inflow``. Examples: >>> import numpy as np @@ -163,7 +163,7 @@ def tf(maxbas): Returns: numpy.ndarray: Array of normalized weights with length - ``maxbas`` that sum to 1.0. + ``maxbas`` that sum to 1.0. Examples: >>> from hapi.routing import Routing @@ -204,7 +204,7 @@ def triangular_routing_2(q, maxbas=1): Returns: numpy.ndarray: Routed discharge time series with the same - length as ``q``. + length as ``q``. Raises: AssertionError: If ``maxbas`` is less than 1. @@ -249,8 +249,8 @@ def calculate_weights(maxbas): Returns: numpy.ndarray: Array of normalized routing weights. The - length is ``floor(MAXBAS)`` for integer values, or - ``floor(MAXBAS) + 1`` for non-integer values. + length is ``floor(MAXBAS)`` for integer values, or + ``floor(MAXBAS) + 1`` for non-integer values. Examples: >>> from hapi.routing import Routing @@ -344,7 +344,7 @@ def triangular_routing_1(Q, MAXBAS): Returns: numpy.ndarray: Routed output hydrograph with the same - length as ``Q``. + length as ``Q``. Examples: >>> import numpy as np diff --git a/src/hapi/rrm/base_model.py b/src/hapi/rrm/base_model.py index 4e6f7225..281457a2 100644 --- a/src/hapi/rrm/base_model.py +++ b/src/hapi/rrm/base_model.py @@ -95,7 +95,7 @@ def precipitation( Returns: tuple[float, float]: A tuple of ``(rainfall, snowfall)`` - in mm. + in mm. Examples: >>> from hapi.rrm.hbv import HBV @@ -134,10 +134,10 @@ def snow( Returns: tuple[float, float, float]: A tuple of - ``(infiltration, wc_new, sp_new)`` where - ``infiltration`` is the water draining into the soil - [mm], ``wc_new`` is the updated liquid water content - [mm], and ``sp_new`` is the updated snow pack [mm]. + ``(infiltration, wc_new, sp_new)`` where + ``infiltration`` is the water draining into the soil + [mm], ``wc_new`` is the updated liquid water content + [mm], and ``sp_new`` is the updated snow pack [mm]. Examples: >>> from hapi.rrm.hbv import HBV @@ -196,9 +196,9 @@ def soil( Returns: tuple[float, float]: A tuple of ``(sm_new, uz_int_1)`` - where ``sm_new`` is the new soil moisture [mm] and - ``uz_int_1`` is the new direct runoff into the upper - zone [mm]. + where ``sm_new`` is the new soil moisture [mm] and + ``uz_int_1`` is the new direct runoff into the upper + zone [mm]. Examples: >>> from hapi.rrm.hbv import HBV @@ -239,10 +239,10 @@ def response( Returns: tuple[float, float, float]: A tuple of - ``(q_new, uz_new, lz_new)`` where ``q_new`` is the total - discharge [m^3/s], ``uz_new`` is the updated upper zone - storage [mm], and ``lz_new`` is the updated lower zone - storage [mm]. + ``(q_new, uz_new, lz_new)`` where ``q_new`` is the total + discharge [m^3/s], ``uz_new`` is the updated upper zone + storage [mm], and ``lz_new`` is the updated lower zone + storage [mm]. Examples: >>> from hapi.rrm.hbv import HBV @@ -269,7 +269,7 @@ def routing(self, q: np.ndarray, maxbas: int = 1) -> np.ndarray: Returns: numpy.ndarray: Routed discharge time series with the same - length as ``q``. + length as ``q``. Raises: AssertionError: If ``maxbas`` is less than 1. @@ -321,13 +321,13 @@ def simulate( Returns: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]: - A tuple of ``(q_uz, q_lz, states)`` where: + A tuple of ``(q_uz, q_lz, states)`` where: - - ``q_uz``: Upper zone discharge array of length + - ``q_uz``: Upper zone discharge array of length ``n+1``. - - ``q_lz``: Lower zone discharge array of length + - ``q_lz``: Lower zone discharge array of length ``n+1``. - - ``states``: Model states array of shape ``(n+1, 5)``. + - ``states``: Model states array of shape ``(n+1, 5)``. Examples: >>> import numpy as np diff --git a/src/hapi/rrm/distrrm.py b/src/hapi/rrm/distrrm.py index 78808f58..b51238ba 100644 --- a/src/hapi/rrm/distrrm.py +++ b/src/hapi/rrm/distrrm.py @@ -43,7 +43,7 @@ def run_lumped_model(Model): ``state_variables``, ``quz``, and ``qlz``. Args: - Model: A catchment model object carrying the following + Model (Catchment): A catchment model object carrying the following attributes: - ``rows`` (int): Number of grid rows. @@ -120,7 +120,7 @@ def SpatialRouting(Model): ``quz_routed``, ``qlz_translated``, and ``Qtot``. Args: - Model: A catchment model object carrying the following + Model (Catchment): A catchment model object carrying the following attributes: - ``rows`` (int): Number of grid rows. @@ -225,7 +225,7 @@ def DistMaxbas1(Model): The ``Model.quz`` array is modified in place. Args: - Model: A catchment model object carrying the following + Model (Catchment): A catchment model object carrying the following attributes: - ``rows`` (int): Number of grid rows. @@ -257,7 +257,7 @@ def DistMaxbas2(Model): The ``Model.quz`` array is modified in place. Args: - Model: A catchment model object carrying the following + Model (Catchment): A catchment model object carrying the following attributes: - ``rows`` (int): Number of grid rows. @@ -319,7 +319,7 @@ def Dist_HBV2( cells and converted to m3/s. Args: - conceptual_model: Lumped model object with a ``simulate`` + conceptual_model (BaseConceptualModel): Lumped model object with a ``simulate`` method. lakecell (list[int]): Two-element list ``[row, col]`` giving the grid indices of the lake cell. diff --git a/src/hapi/rrm/hbv.py b/src/hapi/rrm/hbv.py index f333ff39..7c4b9959 100644 --- a/src/hapi/rrm/hbv.py +++ b/src/hapi/rrm/hbv.py @@ -133,7 +133,7 @@ def precipitation(temp, ltt, utt, prec, rfcf, sfcf): # type: ignore[override] Returns: tuple[float, float]: A tuple of ``(rainfall, snowfall)`` - in mm. + in mm. Examples: Temperature above the upper threshold produces only @@ -208,10 +208,10 @@ def snow( # type: ignore[override] Returns: tuple[float, float, float]: A tuple of - ``(infiltration, wc_new, sp_new)`` where - ``infiltration`` is the water draining into the soil - [mm], ``wc_new`` is the updated water content [mm], - and ``sp_new`` is the updated snow pack [mm]. + ``(infiltration, wc_new, sp_new)`` where + ``infiltration`` is the water draining into the soil + [mm], ``wc_new`` is the updated water content [mm], + and ``sp_new`` is the updated snow pack [mm]. Examples: When temperature exceeds the melt threshold, snow melts @@ -312,9 +312,9 @@ def soil( # type: ignore[override] # tfac, Returns: tuple[float, float]: A tuple of ``(sm_new, uz_int_1)`` - where ``sm_new`` is the new soil moisture [mm] and - ``uz_int_1`` is the new direct runoff into the upper - zone [mm]. + where ``sm_new`` is the new soil moisture [mm] and + ``uz_int_1`` is the new direct runoff into the upper + zone [mm]. Examples: Compute soil moisture update for a warm day with @@ -382,11 +382,11 @@ def response( # type: ignore[override] # tfac,area, Returns: tuple[float, float, float, float]: A tuple of - ``(q_0, q_1, uz_new, lz_new)`` where ``q_0`` is the - upper zone discharge [mm], ``q_1`` is the lower zone - discharge [mm], ``uz_new`` is the updated upper zone - storage [mm], and ``lz_new`` is the updated lower zone - storage [mm]. + ``(q_0, q_1, uz_new, lz_new)`` where ``q_0`` is the + upper zone discharge [mm], ``q_1`` is the lower zone + discharge [mm], ``uz_new`` is the updated upper zone + storage [mm], and ``lz_new`` is the updated lower zone + storage [mm]. Examples: Compute discharge from upper and lower zone storages: @@ -444,7 +444,7 @@ def tf(maxbas) -> np.ndarray: Returns: numpy.ndarray: Array of normalized weights with length - ``maxbas``. + ``maxbas``. Examples: >>> from hapi.rrm.hbv import HBV @@ -482,7 +482,7 @@ def routing(self, q, maxbas=1): Returns: numpy.ndarray: Routed discharge time series with the same - length as ``q``. + length as ``q``. Raises: AssertionError: If ``maxbas`` is less than 1. @@ -550,10 +550,10 @@ def step_run( Returns: tuple[float, float, list[float]]: A tuple of - ``(q_uz, q_lz, states)`` where ``q_uz`` is the upper - zone discharge [mm], ``q_lz`` is the lower zone - discharge [mm], and ``states`` is a list of five - updated state variables ``[sp, sm, uz, lz, wc]``. + ``(q_uz, q_lz, states)`` where ``q_uz`` is the upper + zone discharge [mm], ``q_lz`` is the lower zone + discharge [mm], and ``states`` is a list of five + updated state variables ``[sp, sm, uz, lz, wc]``. Raises: AssertionError: If ``snow=1`` and the parameter vector diff --git a/src/hapi/rrm/parameters.py b/src/hapi/rrm/parameters.py index 97abe422..a35d6a0f 100644 --- a/src/hapi/rrm/parameters.py +++ b/src/hapi/rrm/parameters.py @@ -28,7 +28,7 @@ class Parameters: def __init__( self, - raster, + raster: Dataset, no_parameters: int, no_lumped_par: int = 0, lumped_par_pos: list[int] | None = None, @@ -191,7 +191,7 @@ def __init__( pass - def par3d(self, par_g): # , kub=1,klb=0.5, Maskingum=True + def par3d(self, par_g: list | np.ndarray): # , kub=1,klb=0.5, Maskingum=True """Distribute parameters horizontally across grid cells. Takes a list of parameters (saved as one column or generated as a @@ -278,7 +278,7 @@ def par3d(self, par_g): # , kub=1,klb=0.5, Maskingum=True # klb # ) - def par3d_lumped(self, par_g): # , kub=1, klb=0.5, Maskingum = True + def par3d_lumped(self, par_g: list | np.ndarray): # , kub=1, klb=0.5, Maskingum = True r"""Distribute lumped parameters horizontally across grid cells. Takes a list of parameters (saved as one column or generated as a @@ -318,7 +318,9 @@ def par3d_lumped(self, par_g): # , kub=1, klb=0.5, Maskingum = True # self.Par3d[self.celli[i],self.cellj[i],-1], self.Par3d[self.celli[i],self.cellj[i],-2],kub,klb) @staticmethod - def calculate_k(x, position, upper_bound, lower_bound): + def calculate_k( + x: float, position: int, upper_bound: float, lower_bound: float + ) -> float: """Calculate K parameter for Muskingum routing. Takes the value of x parameter and generates 100 random values of @@ -336,7 +338,7 @@ def calculate_k(x, position, upper_bound, lower_bound): Returns: The K parameter value corresponding to the given position - within the constrained range. + within the constrained range. """ # k has to be smaller than this constraint constraint1 = 0.5 * 1 / (1 - x) @@ -351,9 +353,11 @@ def calculate_k(x, position, upper_bound, lower_bound): generated_k = np.linspace(constraint1, constraint2, 50) k = generated_k[int(round(position, 0))] - return k + return float(k) - def par2d_lumped_k1_lake(self, par_g, no_parameters_lake): # ,kub,klb + def par2d_lumped_k1_lake( + self, par_g: list | np.ndarray, no_parameters_lake: int + ): # ,kub,klb """Distribute parameters with a lumped K1 and lake parameters. Takes a list of parameters and distributes them horizontally on @@ -412,7 +416,7 @@ def par2d_lumped_k1_lake(self, par_g, no_parameters_lake): # ,kub,klb # return self.Par3d, lake_par - def hydrologic_response_units(self, par_g): # ,kub=1,klb=0.5 + def hydrologic_response_units(self, par_g: list | np.ndarray): # ,kub=1,klb=0.5 """Distribute parameters using Hydrologic Response Units (HRUs). Takes a list of parameters (saved as one column or generated as a @@ -503,7 +507,12 @@ def hydrologic_response_units(self, par_g): # ,kub=1,klb=0.5 self.Par3d[self.raster_array == self.values[i]] = self.Par2d[:, i] @staticmethod - def hru_hand(dem, flow_direction, flow_path_length, river): + def hru_hand( + dem: Dataset, + flow_direction: Dataset, + flow_path_length: Dataset, + river: Dataset, + ) -> tuple[np.ndarray, np.ndarray]: """Calculate Height Above Nearest Drainage (HAND) for HRU classification. Calculates inputs for the HAND method for land use @@ -642,7 +651,7 @@ def parameters_number(self): # if there is no lumped parameters self.ParametersNO = self.no_elem * self.no_parameters - def save_parameters(self, path): + def save_parameters(self, path: str | None): """Save distributed parameters as raster files. Takes the generated 3D parameter array and saves each parameter diff --git a/src/hapi/run.py b/src/hapi/run.py index d8c567d8..14295469 100644 --- a/src/hapi/run.py +++ b/src/hapi/run.py @@ -8,11 +8,14 @@ from __future__ import annotations +from collections.abc import Callable +from typing import Any + import numpy as np import pandas as pd from loguru import logger -from hapi.catchment import Catchment +from hapi.catchment import Catchment, Lake as LakeType # from hapi.hm.saintvenant import SaintVenant from hapi.wrapper import Wrapper @@ -145,7 +148,7 @@ def RunFloodModel(self): # SV.KinematicRaster(self) # print("1D model Run has finished") - def runHAPIwithLake(self, Lake): + def runHAPIwithLake(self, Lake: LakeType): """Run the distributed model with a lake component. Validates that all input arrays have consistent dimensions and @@ -237,7 +240,7 @@ def runFW1(self): print("Model Run has finished") - def RunFW1withLake(self, Lake): + def RunFW1withLake(self, Lake: LakeType): """Run the FW1 distributed model with a lake component. Validates that all input arrays have consistent dimensions and @@ -304,7 +307,7 @@ def RunFW1withLake(self, Lake): def runLumped( self, Route: int = 0, - RoutingFn=None, + RoutingFn: Callable[..., Any] | None = None, ): """Run the lumped conceptual model. From 0d4c7e8cf9f40942d227c35db45de820d39c76eb Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Sun, 2 Aug 2026 01:15:59 +0200 Subject: [PATCH 6/8] docs: rename the check inputs image to lowercase --- docs/img/{check_inputs.PNG => check_inputs.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/img/{check_inputs.PNG => check_inputs.png} (100%) diff --git a/docs/img/check_inputs.PNG b/docs/img/check_inputs.png similarity index 100% rename from docs/img/check_inputs.PNG rename to docs/img/check_inputs.png From b7c508b7c2848998272ff50a4e48752817c3ae48 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Sun, 2 Aug 2026 21:04:55 +0200 Subject: [PATCH 7/8] refactor!: rename camelCase parameters and reduce hru_hand complexity --- docs/javascripts/mathjax.js | 4 +- src/hapi/calibration.py | 128 ++++++++--------- src/hapi/rrm/parameters.py | 133 ++++++++++++------ src/hapi/run.py | 33 ++--- tests/calibration/distributed_mode_calib.py | 20 +-- tests/calibration/lumped_calibration.py | 12 +- tests/rrm/calibration/test_rrm_calibration.py | 8 +- tests/run/lumped_run.py | 8 +- tests/sensitivity_analysis.py | 20 +-- 9 files changed, 207 insertions(+), 159 deletions(-) diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js index 80e81ba5..0d950d8e 100644 --- a/docs/javascripts/mathjax.js +++ b/docs/javascripts/mathjax.js @@ -1,7 +1,7 @@ window.MathJax = { tex: { - inlineMath: [["\\(", "\\)"]], - displayMath: [["\\[", "\\]"]], + inlineMath: [[String.raw`\(`, String.raw`\)`]], + displayMath: [[String.raw`\[`, String.raw`\]`]], processEscapes: true, processEnvironments: true }, diff --git a/src/hapi/calibration.py b/src/hapi/calibration.py index ef87515c..c140649c 100644 --- a/src/hapi/calibration.py +++ b/src/hapi/calibration.py @@ -161,15 +161,15 @@ def extract_discharge( def run_calibration( self, - SpatialVarFun: Callable[..., Any], - OptimizationArgs: list, - printError: int | None = None, + spatial_var_fun: Callable[..., Any], + optimization_args: list, + print_error: int | None = None, ): """Run the calibration algorithm for the distributed hydrological model. Executes the Harmony Search optimization algorithm to calibrate parameters for the conceptual distributed hydrological model. - The method distributes parameters spatially using ``SpatialVarFun``, + The method distributes parameters spatially using ``spatial_var_fun``, runs the RRM model via ``Wrapper.RRMModel``, and evaluates performance using the stored objective function. @@ -185,19 +185,19 @@ def run_calibration( gauge metadata. Args: - SpatialVarFun: Spatial variable function object with a + spatial_var_fun: Spatial variable function object with a ``Function`` method that distributes parameters and a ``Par3d`` attribute holding the 3D parameter array, plus ``no_parameters`` and ``no_elem`` attributes. - OptimizationArgs: A list of three elements: - - ``OptimizationArgs[0]`` (dict): Harmony Search API + optimization_args: A list of three elements: + - ``optimization_args[0]`` (dict): Harmony Search API objective arguments (e.g., HMS, HMCR, PAR). - - ``OptimizationArgs[1]``: Parallel type for the + - ``optimization_args[1]``: Parallel type for the optimizer. - - ``OptimizationArgs[2]`` (dict): Solver arguments with + - ``optimization_args[2]`` (dict): Solver arguments with keys ``"store_sol"``, ``"display_opts"``, ``"store_hst"``, and ``"hot_start"``. - printError: If not 0, prints the error value and parameters + print_error: If not 0, prints the error value and parameters at each iteration. Default is None. Returns: @@ -231,15 +231,15 @@ def run_calibration( # basic inputs # check if all inputs are included - # assert all(["p2","init_st","UB","LB","snow "][i] in Basic_inputs.keys() - # for i in range(4)), "Basic_inputs should contain ['p2','init_st','UB','LB']" + # assert all(["p2","init_st","UB","LB","snow "][i] in basic_inputs.keys() + # for i in range(4)), "basic_inputs should contain ['p2','init_st','UB','LB']" ### optimization # get arguments - ApiObjArgs = OptimizationArgs[0] - pll_type = OptimizationArgs[1] - ApiSolveArgs = OptimizationArgs[2] + ApiObjArgs = optimization_args[0] + pll_type = optimization_args[1] + ApiSolveArgs = optimization_args[2] # check optimization arguement assert type(ApiObjArgs) is dict, "store_history should be 0 or 1" assert type(ApiSolveArgs) is dict, "history_fname should be of type string " @@ -250,10 +250,10 @@ def run_calibration( def opt_fun(par): try: # distribute the parameters - SpatialVarFun.Function( + spatial_var_fun.Function( par - ) # , kub=SpatialVarFun.Kub, klb=SpatialVarFun.Klb - self.Parameters = SpatialVarFun.Par3d + ) # , kub=spatial_var_fun.Kub, klb=spatial_var_fun.Klb + self.Parameters = spatial_var_fun.Par3d # run the model Wrapper.RRMModel(self) # calculate performance of the model @@ -261,7 +261,7 @@ def opt_fun(par): error = self.objective_function( self.QGauges, *[self.GaugesTable] ) # self.qout, self.quz_routed, self.qlz_translated, - f = list(range(9, len(par), SpatialVarFun.no_parameters)) + f = list(range(9, len(par), spatial_var_fun.no_parameters)) g = list() for i in range(len(f)): k = par[f[i]] @@ -274,7 +274,7 @@ def opt_fun(par): raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e # print error - if printError != 0: + if print_error != 0: print(round(error, 3)) print(par) @@ -293,7 +293,7 @@ def opt_fun(par): opt_prob.addObj("f") - for i in range(SpatialVarFun.no_elem): + for i in range(spatial_var_fun.no_elem): opt_prob.addCon("g" + str(i) + "-1", "i") opt_prob.addCon("g" + str(i) + "-2", "i") @@ -321,9 +321,9 @@ def opt_fun(par): def FW1Calibration( self, - SpatialVarFun: Callable[..., Any], - OptimizationArgs: list, - printError: int | None = None, + spatial_var_fun: Callable[..., Any], + optimization_args: list, + print_error: int | None = None, ): """Run calibration using the FW1 (Focussed Width-1) routing scheme. @@ -342,18 +342,18 @@ def FW1Calibration( gauge metadata. Args: - SpatialVarFun: Spatial variable function object with a + spatial_var_fun: Spatial variable function object with a ``Function`` method that distributes parameters and a ``Par3d`` attribute holding the 3D parameter array. - OptimizationArgs: A list of three elements: - - ``OptimizationArgs[0]`` (dict): Harmony Search API + optimization_args: A list of three elements: + - ``optimization_args[0]`` (dict): Harmony Search API objective arguments (e.g., HMS, HMCR, PAR). - - ``OptimizationArgs[1]``: Parallel type for the + - ``optimization_args[1]``: Parallel type for the optimizer. - - ``OptimizationArgs[2]`` (dict): Solver arguments with + - ``optimization_args[2]`` (dict): Solver arguments with keys ``"store_sol"``, ``"display_opts"``, ``"store_hst"``, and ``"hot_start"``. - printError: If not 0, prints the error value and parameters + print_error: If not 0, prints the error value and parameters at each iteration. Default is None. Returns: @@ -387,15 +387,15 @@ def FW1Calibration( # basic inputs # check if all inputs are included - # assert all(["p2","init_st","UB","LB","snow "][i] in Basic_inputs.keys() - # for i in range(4)), "Basic_inputs should contain ['p2','init_st','UB','LB']" + # assert all(["p2","init_st","UB","LB","snow "][i] in basic_inputs.keys() + # for i in range(4)), "basic_inputs should contain ['p2','init_st','UB','LB']" ### optimization # get arguments - ApiObjArgs = OptimizationArgs[0] - pll_type = OptimizationArgs[1] - ApiSolveArgs = OptimizationArgs[2] + ApiObjArgs = optimization_args[0] + pll_type = optimization_args[1] + ApiSolveArgs = optimization_args[2] # check optimization arguement assert type(ApiObjArgs) is dict, "store_history should be 0 or 1" assert type(ApiSolveArgs) is dict, "history_fname should be of type string " @@ -406,10 +406,10 @@ def FW1Calibration( def opt_fun(par): try: # distribute the parameters - SpatialVarFun.Function( + spatial_var_fun.Function( par - ) # , kub=SpatialVarFun.Kub, klb=SpatialVarFun.Klb, Maskingum=SpatialVarFun.Maskingum - self.Parameters = SpatialVarFun.Par3d + ) # , kub=spatial_var_fun.Kub, klb=spatial_var_fun.Klb, Maskingum=spatial_var_fun.Maskingum + self.Parameters = spatial_var_fun.Par3d # run the model Wrapper.FW1(self) # calculate performance of the model @@ -422,7 +422,7 @@ def opt_fun(par): raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e # print error - if printError != 0: + if print_error != 0: print(round(error, 3)) print(par) @@ -462,9 +462,9 @@ def opt_fun(par): def lumpedCalibration( self, - Basic_inputs: dict, - OptimizationArgs: list, - printError: int | None = None, + basic_inputs: dict, + optimization_args: list, + print_error: int | None = None, ): """Run the calibration algorithm for the lumped hydrological model. @@ -484,21 +484,21 @@ def lumpedCalibration( - ``dt``: Time step duration. Args: - Basic_inputs (dict): Dictionary containing: + basic_inputs (dict): Dictionary containing: - ``"Route"`` (int): Routing flag (1 to enable routing). - - ``"RoutingFn"`` (callable): Routing function to use. + - ``"routing_fn"`` (callable): Routing function to use. - ``"InitialValues"`` (list, optional): Initial parameter values for the optimizer. Defaults to an empty list if not provided. - OptimizationArgs: A list of three elements: - - ``OptimizationArgs[0]`` (dict): Harmony Search API + optimization_args: A list of three elements: + - ``optimization_args[0]`` (dict): Harmony Search API objective arguments (e.g., HMS, HMCR, PAR). - - ``OptimizationArgs[1]``: Parallel type for the + - ``optimization_args[1]``: Parallel type for the optimizer. - - ``OptimizationArgs[2]`` (dict): Solver arguments with + - ``optimization_args[2]`` (dict): Solver arguments with keys ``"store_sol"``, ``"display_opts"``, ``"store_hst"``, and ``"hot_start"``. - printError: If not 0, prints the error value and constraint + print_error: If not 0, prints the error value and constraint values at each iteration. Default is None. Returns: @@ -507,29 +507,29 @@ def lumpedCalibration( - res[1]: The optimal parameter set. Raises: - AssertionError: If ``Basic_inputs`` is missing required keys - ``"Route"`` or ``"RoutingFn"``, or if optimization + AssertionError: If ``basic_inputs`` is missing required keys + ``"Route"`` or ``"routing_fn"``, or if optimization arguments are not dictionaries. """ # basic inputs # check if all inputs are included - assert all( - ["Route", "RoutingFn"][i] in Basic_inputs.keys() for i in range(2) - ), "Basic_inputs should contain ['p2','init_st','UB','LB'] " - - Route = Basic_inputs["Route"] - RoutingFn = Basic_inputs["RoutingFn"] - if "InitialValues" in Basic_inputs.keys(): - InitialValues = Basic_inputs["InitialValues"] + assert all(["Route", "routing_fn"][i] in basic_inputs for i in range(2)), ( + "basic_inputs should contain ['p2','init_st','UB','LB'] " + ) + + Route = basic_inputs["Route"] + routing_fn = basic_inputs["routing_fn"] + if "InitialValues" in basic_inputs: + InitialValues = basic_inputs["InitialValues"] else: InitialValues = [] ### optimization # get arguments - ApiObjArgs = OptimizationArgs[0] - pll_type = OptimizationArgs[1] - ApiSolveArgs = OptimizationArgs[2] + ApiObjArgs = optimization_args[0] + pll_type = optimization_args[1] + ApiSolveArgs = optimization_args[2] # check optimization arguement assert isinstance(ApiObjArgs, dict), "store_history should be 0 or 1" assert isinstance(ApiSolveArgs, dict), "history_fname should be of type string " @@ -542,7 +542,7 @@ def opt_fun(par): # parameters self.Parameters = par # run the model - Wrapper.Lumped(self, Route, RoutingFn) + Wrapper.Lumped(self, Route, routing_fn) # calculate performance of the model try: error = self.objective_function( @@ -556,7 +556,7 @@ def opt_fun(par): # the objective function received fewer inputs than it needs raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e - if printError != 0: + if print_error != 0: print( f"Error = {round(error, 3)} Inequality Const = {np.round(g, 2)}" ) diff --git a/src/hapi/rrm/parameters.py b/src/hapi/rrm/parameters.py index a35d6a0f..9d69acc3 100644 --- a/src/hapi/rrm/parameters.py +++ b/src/hapi/rrm/parameters.py @@ -278,7 +278,9 @@ def par3d(self, par_g: list | np.ndarray): # , kub=1,klb=0.5, Maskingum=True # klb # ) - def par3d_lumped(self, par_g: list | np.ndarray): # , kub=1, klb=0.5, Maskingum = True + def par3d_lumped( + self, par_g: list | np.ndarray + ): # , kub=1, klb=0.5, Maskingum = True r"""Distribute lumped parameters horizontally across grid cells. Takes a list of parameters (saved as one column or generated as a @@ -555,66 +557,107 @@ def hru_hand( fpl_a = flow_path_length.read_array(band=0) # trace the flow direction to the nearest river reach and store the location - # of that nearst reach + # of that nearest reach + nearest_network = Parameters._trace_nearest_drainage( + dem_a, no_val, river_a, fd_index, rows, cols + ) + + # the elevation difference to the nearest drainage cell is the height above + # nearest drainage; the same difference over the flow path length raster is + # the distance to that drainage + hand = Parameters._difference_to_nearest_drainage( + dem_a, dem_a, no_val, nearest_network, rows, cols + ) + dist_to_nearest_drain = Parameters._difference_to_nearest_drainage( + fpl_a, dem_a, no_val, nearest_network, rows, cols + ) + + return hand, dist_to_nearest_drain + + @staticmethod + def _trace_nearest_drainage( + dem_a: np.ndarray, + no_val: float | np.floating, + river_a: np.ndarray, + fd_index: np.ndarray, + rows: int, + cols: int, + ) -> np.ndarray: + """Locate the nearest downstream river cell for every domain cell. + + Args: + dem_a (np.ndarray): DEM values, used to identify domain cells. + no_val (float): No-data value of the DEM. + river_a (np.ndarray): River raster; a value of 1 marks a river cell. + fd_index (np.ndarray): Downstream cell indices, shape (rows, cols, 2). + rows (int): Number of raster rows. + cols (int): Number of raster columns. + + Returns: + np.ndarray: Array of shape (rows, cols, 2) holding the row and column + of the nearest drainage cell, NaN outside the domain. + + Raises: + ValueError: If a flow path runs off the grid, which happens when the + catchment boundary has anomalies. + """ nearest_network = np.ones((rows, cols, 2)) * np.nan try: for i in range(rows): for j in range(cols): - if dem_a[i, j] != no_val: - f = river_a[i, j] - # a river cell is its own nearest drainage - new_row = i - new_cols = j - old_row = i - old_cols = j - - while f != 1: - # did not reach the river yet, go to the next downstream cell - new_row = int(fd_index[old_row, old_cols, 0]) - new_cols = int(fd_index[old_row, old_cols, 1]) - # go to the downstream cell - f = river_a[new_row, new_cols] - # the downstream cell becomes the current position - old_row = new_row - old_cols = new_cols - # store the position in the array - nearest_network[i, j, 0] = new_row - nearest_network[i, j, 1] = new_cols - + if dem_a[i, j] == no_val: + continue + # a river cell is its own nearest drainage + row, col = i, j + while river_a[row, col] != 1: + # not at the river yet, step to the downstream cell + row, col = ( + int(fd_index[row, col, 0]), + int(fd_index[row, col, 1]), + ) + nearest_network[i, j, 0] = row + nearest_network[i, j, 1] = col except (IndexError, ValueError) as e: raise ValueError( "please check the boundaries of your catchment. After cropping the catchment using a polygon, it " "creates anomalies at the boundary" ) from e - # calculate the elevation difference between the cell and the nearest drainage cell - # or height above nearst drainage - hand = np.ones((rows, cols)) * np.nan + return nearest_network - for i in range(rows): - for j in range(cols): - if dem_a[i, j] != no_val: - hand[i, j] = ( - dem_a[i, j] - - dem_a[ - int(nearest_network[i, j, 0]), int(nearest_network[i, j, 1]) - ] - ) + @staticmethod + def _difference_to_nearest_drainage( + values: np.ndarray, + dem_a: np.ndarray, + no_val: float | np.floating, + nearest_network: np.ndarray, + rows: int, + cols: int, + ) -> np.ndarray: + """Subtract each cell's nearest-drainage value from the cell's own value. - # calculate the distance to the nearest drainage c ell using flow path length or distance to nearest drainage - dist_to_nearest_drain = np.ones((rows, cols)) * np.nan + Args: + values (np.ndarray): Raster to difference (elevation or flow path length). + dem_a (np.ndarray): DEM values, used to identify domain cells. + no_val (float): No-data value of the DEM. + nearest_network (np.ndarray): Nearest drainage indices from + `_trace_nearest_drainage`. + rows (int): Number of raster rows. + cols (int): Number of raster columns. + Returns: + np.ndarray: The difference for every domain cell, NaN elsewhere. + """ + difference = np.ones((rows, cols)) * np.nan for i in range(rows): for j in range(cols): - if dem_a[i, j] != no_val: - dist_to_nearest_drain[i, j] = ( - fpl_a[i, j] - - fpl_a[ - int(nearest_network[i, j, 0]), int(nearest_network[i, j, 1]) - ] - ) + if dem_a[i, j] == no_val: + continue + drain_row = int(nearest_network[i, j, 0]) + drain_col = int(nearest_network[i, j, 1]) + difference[i, j] = values[i, j] - values[drain_row, drain_col] - return hand, dist_to_nearest_drain + return difference def parameters_number(self): """Calculate the total number of parameters for the optimization. diff --git a/src/hapi/run.py b/src/hapi/run.py index 14295469..0f834537 100644 --- a/src/hapi/run.py +++ b/src/hapi/run.py @@ -15,7 +15,8 @@ import pandas as pd from loguru import logger -from hapi.catchment import Catchment, Lake as LakeType +from hapi.catchment import Catchment +from hapi.catchment import Lake as LakeType # from hapi.hm.saintvenant import SaintVenant from hapi.wrapper import Wrapper @@ -148,7 +149,7 @@ def RunFloodModel(self): # SV.KinematicRaster(self) # print("1D model Run has finished") - def runHAPIwithLake(self, Lake: LakeType): + def runHAPIwithLake(self, lake: LakeType): """Run the distributed model with a lake component. Validates that all input arrays have consistent dimensions and @@ -157,7 +158,7 @@ def runHAPIwithLake(self, Lake: LakeType): the Wrapper. Args: - Lake: Lake object containing lake configuration and + lake: Lake object containing lake configuration and meteorological data. Must have a ``MeteoData`` attribute with shape ``(time_steps, >= 3)`` where columns are rain, ET, and temperature. @@ -190,15 +191,15 @@ def runHAPIwithLake(self, Lake: LakeType): np.shape(self.Prec)[2] == np.shape(self.ET)[2] == np.shape(self.Temp)[2] ), "all meteorological input data should have the same length" - assert np.shape(Lake.MeteoData)[0] == np.shape(self.Prec)[2], ( + assert np.shape(lake.MeteoData)[0] == np.shape(self.Prec)[2], ( "Lake meteorological data has to have the same length as the distributed raster data" ) - assert np.shape(Lake.MeteoData)[1] >= 3, ( + assert np.shape(lake.MeteoData)[1] >= 3, ( "Lake Meteo data has to have at least three columns of rain, ET, and Temp" ) # run the model - Wrapper.RRMWithlake(self, Lake) + Wrapper.RRMWithlake(self, lake) print("Model Run has finished") @@ -240,7 +241,7 @@ def runFW1(self): print("Model Run has finished") - def RunFW1withLake(self, Lake: LakeType): + def RunFW1withLake(self, lake: LakeType): """Run the FW1 distributed model with a lake component. Validates that all input arrays have consistent dimensions and @@ -248,7 +249,7 @@ def RunFW1withLake(self, Lake: LakeType): then executes the FW1 model with lake routing via the Wrapper. Args: - Lake: Lake object containing lake configuration and + lake: Lake object containing lake configuration and meteorological data. Must have a ``MeteoData`` attribute with shape ``(time_steps, >= 3)`` where columns are rain, ET, and temperature. @@ -294,20 +295,20 @@ def RunFW1withLake(self, Lake: LakeType): np.shape(self.Prec)[2] == np.shape(self.ET)[2] == np.shape(self.Temp)[2] ), "all meteorological input data should have the same length" - assert np.shape(Lake.MeteoData)[0] == np.shape(self.Prec)[2], ( + assert np.shape(lake.MeteoData)[0] == np.shape(self.Prec)[2], ( "Lake meteorological data has to have the same length as the distributed raster data" ) - assert np.shape(Lake.MeteoData)[1] >= 3, ( + assert np.shape(lake.MeteoData)[1] >= 3, ( "Lake Meteo data has to have at least three columns rain, ET, and Temp" ) # run the model - Wrapper.FW1Withlake(self, Lake) + Wrapper.FW1Withlake(self, lake) def runLumped( self, Route: int = 0, - RoutingFn: Callable[..., Any] | None = None, + routing_fn: Callable[..., Any] | None = None, ): """Run the lumped conceptual model. @@ -320,7 +321,7 @@ def runLumped( Route: Flag to decide whether to route the generated discharge hydrograph. Use 0 for no routing or 1 to enable routing. Defaults to 0. - RoutingFn: Function to route the discharge hydrograph. + routing_fn: Function to route the discharge hydrograph. If None, an empty list is used. Defaults to None. Note: @@ -342,8 +343,8 @@ def runLumped( - ``Snow``: Whether to use the snow subroutine (0 or 1). - ``q_init``: Initial discharge value. """ - if RoutingFn is None and Route != 0: - raise ValueError("RoutingFn must be a callable when Route != 0") + if routing_fn is None and Route != 0: + raise ValueError("routing_fn must be a callable when Route != 0") if self.temporal_resolution.lower() == "daily": ind = pd.date_range(self.start, self.end, freq="D") else: @@ -351,7 +352,7 @@ def runLumped( Qsim = pd.DataFrame(index=ind) - Wrapper.Lumped(self, Route, RoutingFn) + Wrapper.Lumped(self, Route, routing_fn) Qsim["q"] = self.Qsim self.Qsim = Qsim[:] logger.info("Lumped model run has finished successfully") diff --git a/tests/calibration/distributed_mode_calib.py b/tests/calibration/distributed_mode_calib.py index 607d36f5..42059a37 100644 --- a/tests/calibration/distributed_mode_calib.py +++ b/tests/calibration/distributed_mode_calib.py @@ -24,7 +24,7 @@ FlowDPath = Path + "/GIS/fd4000.tif" CalibPath = Path + "/calibration" SaveTo = Path + "/results" -# %% Basic_inputs +# %% basic_inputs AreaCoeff = 1530 # [sp,sm,uz,lz,wc] InitialCond = [0, 5, 5, 5, 0] @@ -58,7 +58,7 @@ for muskingum parameters k & x include the upper and lower bound in both UB & LB with the order of Klb then kub function inside the calibration algorithm is written as following -par_dist=SpatialVarFun(par,*SpatialVarArgs,kub=kub,klb=klb) +par_dist=spatial_var_fun(par,*SpatialVarArgs,kub=kub,klb=klb) """ raster = Dataset.read_file(FlowAccPath) @@ -71,7 +71,7 @@ no_lumped_par = 1 lumped_par_pos = [7] -SpatialVarFun = DP( +spatial_var_fun = DP( raster, no_parameters, no_lumped_par=no_lumped_par, @@ -81,7 +81,7 @@ kub=kub, ) # calculate no of parameters that optimization algorithm is going to generate -SpatialVarFun.ParametersNO +spatial_var_fun.ParametersNO # %% Gauges Coello.read_gauge_table(Path + "/stations/gauges.csv", FlowAccPath) GaugesPath = Path + "/stations/" @@ -134,10 +134,14 @@ def objective_function(Qobs, Qout, q_uz_routed, q_lz_trans, coordinates): ApiSolveArgs = dict(store_sol=True, display_opts=True, store_hst=True, hot_start=False) -OptimizationArgs = [ApiObjArgs, pll_type, ApiSolveArgs] +optimization_args = [ApiObjArgs, pll_type, ApiSolveArgs] # %% run calibration -cal_parameters = Coello.runCalibration(SpatialVarFun, OptimizationArgs, printError=0) +cal_parameters = Coello.runCalibration( + spatial_var_fun, optimization_args, print_error=0 +) # %% convert parameters to rasters # Coello.Parameters = [0.700, 399, 1.704, 0.1021, 0.4622, 0.6237, 0.1251, 0.005, 59.85, 5.241, 94.91, 0.2075] -SpatialVarFun.Function(Coello.Parameters, kub=SpatialVarFun.Kub, klb=SpatialVarFun.Klb) -SpatialVarFun.save_parameters(SaveTo) +spatial_var_fun.Function( + Coello.Parameters, kub=spatial_var_fun.Kub, klb=spatial_var_fun.Klb +) +spatial_var_fun.save_parameters(SaveTo) diff --git a/tests/calibration/lumped_calibration.py b/tests/calibration/lumped_calibration.py index 6dd435e0..ddf4f4b4 100644 --- a/tests/calibration/lumped_calibration.py +++ b/tests/calibration/lumped_calibration.py @@ -20,7 +20,7 @@ Coello = Calibration(name, start, end) Coello.read_lumped_inputs(MeteoDataPath) -# %% Basic_inputs +# %% basic_inputs # catchment area AreaCoeff = 1530 # temporal resolution @@ -43,9 +43,9 @@ parameters = [] # Routing Route = 1 -RoutingFn = Routing.triangular_routing_1 +routing_fn = Routing.triangular_routing_1 -Basic_inputs = dict(Route=Route, RoutingFn=RoutingFn, InitialValues=parameters) +basic_inputs = dict(Route=Route, routing_fn=routing_fn, InitialValues=parameters) # %% ### Objective function # outlet discharge @@ -84,11 +84,11 @@ ApiSolveArgs = dict(store_sol=True, display_opts=True, store_hst=True, hot_start=False) -OptimizationArgs = [ApiObjArgs, pll_type, ApiSolveArgs] +optimization_args = [ApiObjArgs, pll_type, ApiSolveArgs] # %% # run calibration cal_parameters = Coello.lumpedCalibration( - Basic_inputs, OptimizationArgs, printError=None + basic_inputs, optimization_args, print_error=None ) print("Objective Function = " + str(round(cal_parameters[0], 2))) @@ -96,7 +96,7 @@ print("Time = " + str(round(cal_parameters[2]["time"] / 60, 2)) + " min") # %% run the model Coello.Parameters = cal_parameters[1] -Run.runLumped(Coello, Route, RoutingFn) +Run.runLumped(Coello, Route, routing_fn) # %% calculate performance criteria Metrics = dict() diff --git a/tests/rrm/calibration/test_rrm_calibration.py b/tests/rrm/calibration/test_rrm_calibration.py index 73fc2b84..4b9ce84d 100644 --- a/tests/rrm/calibration/test_rrm_calibration.py +++ b/tests/rrm/calibration/test_rrm_calibration.py @@ -45,9 +45,9 @@ def test_lumped_calibration( parameters = [] # Routing Route = 1 - RoutingFn = Routing.triangular_routing_1 + routing_fn = Routing.triangular_routing_1 - Basic_inputs = dict(Route=Route, RoutingFn=RoutingFn, InitialValues=parameters) + basic_inputs = dict(Route=Route, routing_fn=routing_fn, InitialValues=parameters) # discharge gauges Coello.read_discharge_gauges(lumped_gauges_path, fmt=coello_gauges_date_fmt) @@ -77,9 +77,9 @@ def test_lumped_calibration( store_sol=True, display_opts=True, store_hst=False, hot_start=False ) - OptimizationArgs = [ApiObjArgs, pll_type, ApiSolveArgs] + optimization_args = [ApiObjArgs, pll_type, ApiSolveArgs] - # cal_parameters = Coello.lumpedCalibration(Basic_inputs, OptimizationArgs, printError=None) + # cal_parameters = Coello.lumpedCalibration(basic_inputs, optimization_args, print_error=None) # assert len(Coello.Qsim) == 1095 and Coello.Qsim.columns.to_list() == ['q'] diff --git a/tests/run/lumped_run.py b/tests/run/lumped_run.py index 6dc05871..3916bdbd 100644 --- a/tests/run/lumped_run.py +++ b/tests/run/lumped_run.py @@ -20,7 +20,7 @@ Coello = Catchment(name, start, end) Coello.read_lumped_inputs(MeteoDataPath) # %% -### Basic_inputs +### basic_inputs # catchment area AreaCoeff = 1530 # [Snow pack, Soil moisture, Upper zone, Lower Zone, Water content] @@ -34,11 +34,11 @@ # %% observed flow Coello.read_discharge_gauges(Path + "Qout_c.csv", fmt="%Y-%m-%d") # %% Routing -# RoutingFn = Routing.triangular_routing_2 -RoutingFn = Routing.muskingum_v +# routing_fn = Routing.triangular_routing_2 +routing_fn = Routing.muskingum_v Route = 1 ### run the model -Run.runLumped(Coello, Route, RoutingFn) +Run.runLumped(Coello, Route, routing_fn) # %% calculate performance criteria Metrics = dict() diff --git a/tests/sensitivity_analysis.py b/tests/sensitivity_analysis.py index 7b3dd3f1..4e2e624e 100644 --- a/tests/sensitivity_analysis.py +++ b/tests/sensitivity_analysis.py @@ -19,7 +19,7 @@ Coello = Catchment(name, start, end) Coello.read_lumped_inputs(MeteoDataPath) -### Basic_inputs +### basic_inputs # catchment area CatArea = 1530 # temporal resolution @@ -49,11 +49,11 @@ ) ### Routing Route = 1 -# RoutingFn=Routing.triangular_routing_2 -RoutingFn = Routing.muskingum +# routing_fn=Routing.triangular_routing_2 +routing_fn = Routing.muskingum # %% ### run the model -Run.runLumped(Coello, Route, RoutingFn) +Run.runLumped(Coello, Route, routing_fn) # %% Metrics = dict() @@ -88,7 +88,7 @@ for the lumped HBV model and the RMSE of the calculated discharge. the first function "Run.runLumped" takes some arguments we need to pass through - the one_at_a_time method [ConceptualModel,data,p2,init_st,snow,Routing, RoutingFn] + the one_at_a_time method [ConceptualModel,data,p2,init_st,snow,Routing, routing_fn] with the same order in the defined function "wrapper" the second function is RMSE takes the calculated discharge from the first function @@ -110,19 +110,19 @@ # For Type 1 -def WrapperType1(Randpar, Route, RoutingFn, Qobs): +def WrapperType1(Randpar, Route, routing_fn, Qobs): Coello.Parameters = Randpar - Run.runLumped(Coello, Route, RoutingFn) + Run.runLumped(Coello, Route, routing_fn) rmse = metrics.rmse(Qobs, Coello.Qsim["q"]) return rmse # For Type 2 -def WrapperType2(Randpar, Route, RoutingFn, Qobs): +def WrapperType2(Randpar, Route, routing_fn, Qobs): Coello.Parameters = Randpar - Run.runLumped(Coello, Route, RoutingFn) + Run.runLumped(Coello, Route, routing_fn) rmse = metrics.rmse(Qobs, Coello.Qsim["q"]) return rmse, Coello.Qsim["q"] @@ -136,7 +136,7 @@ def WrapperType2(Randpar, Route, RoutingFn, Qobs): Sen = SA(parameters, Coello.LB, Coello.UB, fn, n_values=5, return_values=Type) -Sen.one_at_a_time(Route, RoutingFn, Qobs) +Sen.one_at_a_time(Route, routing_fn, Qobs) # %% From = "" To = "" From 726fbc9d6703df93887331d6e63b9c682ffd67d3 Mon Sep 17 00:00:00 2001 From: Mostafa Farrag Date: Sun, 2 Aug 2026 21:20:44 +0200 Subject: [PATCH 8/8] refactor(calibration): rename camelCase local variables --- src/hapi/calibration.py | 84 ++++++++++--------- tests/calibration/lumped_calibration.py | 2 +- tests/rrm/calibration/test_rrm_calibration.py | 2 +- 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/hapi/calibration.py b/src/hapi/calibration.py index c140649c..1e8cc438 100644 --- a/src/hapi/calibration.py +++ b/src/hapi/calibration.py @@ -237,12 +237,12 @@ def run_calibration( ### optimization # get arguments - ApiObjArgs = optimization_args[0] + api_obj_args = optimization_args[0] pll_type = optimization_args[1] - ApiSolveArgs = optimization_args[2] + api_solve_args = optimization_args[2] # check optimization arguement - assert type(ApiObjArgs) is dict, "store_history should be 0 or 1" - assert type(ApiSolveArgs) is dict, "history_fname should be of type string " + assert type(api_obj_args) is dict, "store_history should be 0 or 1" + assert type(api_solve_args) is dict, "history_fname should be of type string " print("Calibration starts") @@ -299,12 +299,12 @@ def opt_fun(par): print(opt_prob) - opt_engine = HSapi(pll_type=pll_type, options=ApiObjArgs) + opt_engine = HSapi(pll_type=pll_type, options=api_obj_args) - store_sol = ApiSolveArgs["store_sol"] - display_opts = ApiSolveArgs["display_opts"] - store_hst = ApiSolveArgs["store_hst"] - hot_start = ApiSolveArgs["hot_start"] + store_sol = api_solve_args["store_sol"] + display_opts = api_solve_args["display_opts"] + store_hst = api_solve_args["store_hst"] + hot_start = api_solve_args["hot_start"] res = opt_engine( opt_prob, @@ -393,12 +393,12 @@ def FW1Calibration( ### optimization # get arguments - ApiObjArgs = optimization_args[0] + api_obj_args = optimization_args[0] pll_type = optimization_args[1] - ApiSolveArgs = optimization_args[2] + api_solve_args = optimization_args[2] # check optimization arguement - assert type(ApiObjArgs) is dict, "store_history should be 0 or 1" - assert type(ApiSolveArgs) is dict, "history_fname should be of type string " + assert type(api_obj_args) is dict, "store_history should be 0 or 1" + assert type(api_solve_args) is dict, "history_fname should be of type string " print("Calibration starts") @@ -440,12 +440,12 @@ def opt_fun(par): print(opt_prob) - opt_engine = HSapi(pll_type=pll_type, options=ApiObjArgs) + opt_engine = HSapi(pll_type=pll_type, options=api_obj_args) - store_sol = ApiSolveArgs["store_sol"] - display_opts = ApiSolveArgs["display_opts"] - store_hst = ApiSolveArgs["store_hst"] - hot_start = ApiSolveArgs["hot_start"] + store_sol = api_solve_args["store_sol"] + display_opts = api_solve_args["display_opts"] + store_hst = api_solve_args["store_hst"] + hot_start = api_solve_args["hot_start"] res = opt_engine( opt_prob, @@ -486,7 +486,7 @@ def lumpedCalibration( Args: basic_inputs (dict): Dictionary containing: - ``"Route"`` (int): Routing flag (1 to enable routing). - - ``"routing_fn"`` (callable): Routing function to use. + - ``"RoutingFn"`` (callable): Routing function to use. - ``"InitialValues"`` (list, optional): Initial parameter values for the optimizer. Defaults to an empty list if not provided. @@ -508,31 +508,33 @@ def lumpedCalibration( Raises: AssertionError: If ``basic_inputs`` is missing required keys - ``"Route"`` or ``"routing_fn"``, or if optimization + ``"Route"`` or ``"RoutingFn"``, or if optimization arguments are not dictionaries. """ # basic inputs # check if all inputs are included - assert all(["Route", "routing_fn"][i] in basic_inputs for i in range(2)), ( + assert all(["Route", "RoutingFn"][i] in basic_inputs for i in range(2)), ( "basic_inputs should contain ['p2','init_st','UB','LB'] " ) - Route = basic_inputs["Route"] - routing_fn = basic_inputs["routing_fn"] + route = basic_inputs["Route"] + routing_fn = basic_inputs["RoutingFn"] if "InitialValues" in basic_inputs: - InitialValues = basic_inputs["InitialValues"] + initial_values = basic_inputs["InitialValues"] else: - InitialValues = [] + initial_values = [] ### optimization # get arguments - ApiObjArgs = optimization_args[0] + api_obj_args = optimization_args[0] pll_type = optimization_args[1] - ApiSolveArgs = optimization_args[2] + api_solve_args = optimization_args[2] # check optimization arguement - assert isinstance(ApiObjArgs, dict), "store_history should be 0 or 1" - assert isinstance(ApiSolveArgs, dict), "history_fname should be of type string " + assert isinstance(api_obj_args, dict), "store_history should be 0 or 1" + assert isinstance(api_solve_args, dict), ( + "history_fname should be of type string " + ) print("Calibration starts") @@ -542,7 +544,7 @@ def opt_fun(par): # parameters self.Parameters = par # run the model - Wrapper.Lumped(self, Route, routing_fn) + Wrapper.Lumped(self, route, routing_fn) # calculate performance of the model try: error = self.objective_function( @@ -571,14 +573,14 @@ def opt_fun(par): ### define the optimization components opt_prob = Optimization("HBV Calibration", opt_fun) - if InitialValues != []: + if initial_values != []: for i in range(len(self.LB)): opt_prob.addVar( f"x{i}", type="c", lower=self.LB[i], upper=self.UB[i], - value=InitialValues[i], + value=initial_values[i], ) else: for i in range(len(self.LB)): @@ -589,19 +591,19 @@ def opt_fun(par): opt_prob.addCon("g1", "i") opt_prob.addCon("g2", "i") # print(opt_prob) - opt_engine = HSapi(pll_type=pll_type, options=ApiObjArgs) + opt_engine = HSapi(pll_type=pll_type, options=api_obj_args) - # parse the ApiSolveArgs inputs + # parse the api_solve_args inputs # availablekeys = ['store_sol',"display_opts","store_hst","hot_start"] - store_sol = ApiSolveArgs["store_sol"] - display_opts = ApiSolveArgs["display_opts"] - store_hst = ApiSolveArgs["store_hst"] - hot_start = ApiSolveArgs["hot_start"] + store_sol = api_solve_args["store_sol"] + display_opts = api_solve_args["display_opts"] + store_hst = api_solve_args["store_hst"] + hot_start = api_solve_args["hot_start"] # for i in range(len(availablekeys)): - # if availablekeys[i] in ApiSolveArgs.keys(): - # exec(availablekeys[i] + "=" + str(ApiSolveArgs[availablekeys[i]])) - # print(availablekeys[i] + " = " + str(ApiSolveArgs[availablekeys[i]])) + # if availablekeys[i] in api_solve_args.keys(): + # exec(availablekeys[i] + "=" + str(api_solve_args[availablekeys[i]])) + # print(availablekeys[i] + " = " + str(api_solve_args[availablekeys[i]])) res = opt_engine( opt_prob, diff --git a/tests/calibration/lumped_calibration.py b/tests/calibration/lumped_calibration.py index ddf4f4b4..f7b62ba0 100644 --- a/tests/calibration/lumped_calibration.py +++ b/tests/calibration/lumped_calibration.py @@ -45,7 +45,7 @@ Route = 1 routing_fn = Routing.triangular_routing_1 -basic_inputs = dict(Route=Route, routing_fn=routing_fn, InitialValues=parameters) +basic_inputs = dict(Route=Route, RoutingFn=routing_fn, InitialValues=parameters) # %% ### Objective function # outlet discharge diff --git a/tests/rrm/calibration/test_rrm_calibration.py b/tests/rrm/calibration/test_rrm_calibration.py index 4b9ce84d..432bfb99 100644 --- a/tests/rrm/calibration/test_rrm_calibration.py +++ b/tests/rrm/calibration/test_rrm_calibration.py @@ -47,7 +47,7 @@ def test_lumped_calibration( Route = 1 routing_fn = Routing.triangular_routing_1 - basic_inputs = dict(Route=Route, routing_fn=routing_fn, InitialValues=parameters) + basic_inputs = dict(Route=Route, RoutingFn=routing_fn, InitialValues=parameters) # discharge gauges Coello.read_discharge_gauges(lumped_gauges_path, fmt=coello_gauges_date_fmt)