**Package**: superflexpy-ki v1.0.0 **Model**: SuperflexPy v1.3.2 — Flexible Conceptual Hydrological Modelling Framework **Authors**: Marco Dal Molin, Fabrizio Fenicia, Dmitri Kavetski **Created by**: Hydrocraft / auto_dissect **Last updated**: 2026-03-25 **Stats**: 4 tools | 5 skill docs | 21 diagnostic triplets | ~2800 lines ---
Scanned 9/11/2026
Install to Claude Code
npx -y skills add lzwei196/KISS---Knowledge-Infrastructure-for-Scientific-Simulation --skill SuperflexPy --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of SuperflexPy?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/lzwei196-superflexpy)More formats (shields.io, HTML) on the badges page.
> **MANDATORY EXECUTION POLICY** — READ BEFORE PROCEEDING
>
> You MUST run the **actual model binary or package** described in this document.
> If the model fails to import, compile, or execute, you MUST:
> 1. Check `diagnostics/triplets.yaml` for a matching error pattern
> 2. Try the fix suggested in the triplet's `remedy` section
> 3. If still failing, report the error to the user with full details
>
> You MUST NOT substitute a simplified Python formula, regression equation,
> or hand-coded approximation in place of the real model.
>
>
> Before starting, run: `python preflight_check.py` (in this KI directory)
> to verify that the model binary/package and required data are available.
>
> **DEBUGGING PROTOCOL** — When something goes wrong, follow this order:
> 1. **Check triplets** — `diagnostics/triplets.yaml` may already cover this error
> 2. **Read official docs** — The model's own documentation for expected formats/units
> 3. **Find working examples** — Check `outputs/` or the model's shipped test data
> 4. **Fix the tool** — With knowledge of what "correct" looks like
>
> Do NOT write custom debug scripts. The answers are in the docs and examples.
<!-- KI-MAP:BEGIN (projected by generate_skill_map.py — edit the KI, not this table) -->
## KI map — what to read, and when
| when you need | read | why |
|---|---|---|
| FIRST, always | `preflight_check.py` | run it (`python preflight_check.py`): proves env/binary/data are usable and emits a machine-readable `PREFLIGHT_REPORT=` line. Do not debug a run that never had a healthy environment. |
| to run the pipeline stages | `tools/` (4 tools) | the executable pipeline. Read each tool's argparse (`--help`) before composing a command; SKILL.md's stage table says which tool serves which stage. |
| before running a stage | `docs/s*_*.md` (5 stage docs) | per-stage procedure, verification and traps — the how-to that SKILL.md's overview compresses. |
| on ANY error, before debugging | `diagnostics/triplets.yaml` (21 entries) | symptom → diagnosis → remedy for this model's known failure modes. Check here FIRST; the answer usually exists. Never renumber or rewrite entries. |
| to know what an output IS | `dag.yaml` | the model's identity: every output's medium, units, `validation_rank` (1 = the headline variable) and observability. Scoring and obs-binding read THIS — when asked 'what does this model predict', the dag is the answer, not a guess. |
| when building inputs / parsing outputs | `docs/format_spec.yaml` | exact I/O shapes + `known_issues`, projected from dag + triplets. Regenerate with `ki_tools_common/generate_format_spec.py` after changing either — never hand-edit. |
| to judge a run's skill | `docs/validation_convention.yaml` | how this model's field judges it validated: per-`dag_variable` metrics, directions and CITED pass-bands. A run is graded against these, not against intuition. |
| for claims and thresholds | `docs/gathered_papers.json` (17 papers) + `docs/papers_index.md` | the literature this KI is judged by; each entry's `text_path` is fetched full text in the central paper cache. `role: benchmark` marks the model's own skill paper. |
| for a machine-readable summary | `knowledge_infrastructure.yaml` | the manifest (package, pipeline, validation tier, counts) — projected by `ki_tools_common/generate_ki_manifest.py`; regenerate after structural changes, never hand-edit. |
| what past runs learned | `.kdt_evolution.jsonl` | append-only memory of previous runs and fixes on this KI. |
*Projected 2026-08-21 from the KI's actual contents — 10 components present. Refresh: `python3 ki_tools_common/generate_skill_map.py --ki_dir <this KI>`.*
<!-- KI-MAP:END -->
<!-- KI-TOOL-INDEX:BEGIN (projected by generate_skill_map.py — the discoverability contract: every public tool, exact path; PURPOSE stays human-authored elsewhere) -->
### Executable tool index (projected — complete by construction)
Every public tool in this KI, by exact path. What each is FOR lives in the
human-written Tool Inventory above; `--help` on any of these prints its arguments.
| tool (exact path) | invocation |
|---|---|
| `tools/convert_forcing.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/convert_forcing.py --help` |
| `tools/convert_parameters.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/convert_parameters.py --help` |
| `tools/parse_output.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/parse_output.py --help` |
| `tools/run_superflexpy.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/run_superflexpy.py --help` |
*4 public tools; `_`-prefixed helpers and packaging files excluded.*
<!-- KI-TOOL-INDEX:END -->
# SuperflexPy (v1.3.2) — Knowledge Infrastructure
**Package**: superflexpy-ki v1.0.0
**Model**: SuperflexPy v1.3.2 — Flexible Conceptual Hydrological Modelling Framework
**Authors**: Marco Dal Molin, Fabrizio Fenicia, Dmitri Kavetski
**Created by**: Hydrocraft / auto_dissect
**Last updated**: 2026-03-25
**Stats**: 4 tools | 5 skill docs | 21 diagnostic triplets | ~2800 lines
---
## WHICH PYTHON — read this before you run anything
**The interpreter named in the projected tool index above CANNOT run these tools.**
`KISSPATH_PYTHON_ENV/bin/python` has no `superflexpy`.
The only interpreter on this machine that does is the model's own work venv:
```bash
SFP=KISSPATH_INTERNAL_NOT_SHIPPED/auto_dissect/_work/SuperflexPy/venv/bin/python
$SFP KISSPATH_KI_ROOT/SuperflexPy/knowledge_infrastructure/tools/run_superflexpy.py --help
```
`preflight_check.py` already resolves this (it probes both and picks the one that
imports `superflexpy`) — run it first and use the interpreter it reports.
That venv has numpy 1.24.4 / numba 0.57.1 / scipy / pandas / xarray / netCDF4,
but **not** `ki_tools_common` — `parse_output.py` adds the canonical checkout to
`sys.path` itself, and any script of your own must do the same:
```python
sys.path.insert(0, "KISSPATH_KI_TOOLS_COMMON")
```
Its `netCDF4` reads Caravan files fine, while `python_env`'s raises
`OSError: [Errno -101] NetCDF: HDF error` on every one of them.
---
## Data Preparation
### Forcing data
**Data Sources**: Use `from ki_tools_common.load_forcing import load_daily_forcing` for CMFD/MSWX/NASA POWER.
**Data Validation Reference**: unit documentation and known traps live in
`ki_tools_common` (`load_forcing`, `soil_utils`) — **not** in `data_ki/<X>/tools/`.
Those `data_ki/CMFD/SKILL.md`, `data_ki/HWSD/SKILL.md`, `data_ki/ObservedQ/SKILL.md`
references are stale KDT-4 paths; KDT 5.0 removed tools from `data_ki`.
### Caravan / GRDC-Caravan (the bound observation for this model)
A Caravan per-gauge `.nc` is the ideal input for a lumped SuperflexPy model: it
already carries **basin-averaged** ERA5-Land forcing and the gauge's observed
discharge on one daily axis, so no basin averaging or area conversion is needed.
```bash
$SFP tools/convert_forcing.py --format caravan \
--input KISSPATH_DATA/observed_data/dischargeandwatershed/GRDC-Caravan-extension-nc/timeseries/netcdf/grdc/GRDC_6503201.nc \
--start 1979-01-01 --end 1990-12-31 --area 1517.1 --pet-min 0 \
--output forcing.json
```
| Caravan variable | Meaning | Unit | Trap |
|---|---|---|---|
| `total_precipitation_sum` | basin-mean P | **mm/d** | no `units` attribute on the variable |
| `potential_evaporation_sum` | basin-mean PET | **mm/d**, positive = demand | already sign-flipped by Caravan; goes slightly negative on condensation days — pass `--pet-min 0` (dt_021) |
| `streamflow` | observed discharge | **mm/d, basin-averaged — NOT m³/s** | the file's global `Units` attribute documents every ERA5 field and says *nothing* about `streamflow`; applying an area conversion is a silent area-squared error |
**Verify the unit claim, don't take it on faith**: GRDC_1159100 (Orange R. at
Vioolsdrif, 786,038 km²) has mean `streamflow` 0.0248 → 225 m³/s, matching the
published GRDC mean.
**PET product bias (dt_020)** — ERA5-Land PET is a well-watered potential and in
humid maritime catchments exceeds P. GR4J's `InterceptionFilter` removes
`min(P, PET)` first, so raw Caravan PET can cap the model below the *observed*
runoff. On the Irish gauges: P 995, PET 1208, Q_obs 604 mm/yr — interception
alone removes 444 mm/yr, leaving a 551 mm/yr ceiling under a 604 mm/yr
observation. Calibrate a PET coefficient: `--free-params ... cpet` for GR4J,
`Ce` for HBV (calibrated values 0.44 and 0.39 on the same catchment).
## Overview
SuperflexPy is an open-source Python framework for building flexible, conceptual,
distributed hydrological models. Unlike monolithic rainfall-runoff models, SuperflexPy
provides a **component-based architecture** with four hierarchical levels — Elements,
Units, Nodes, and Networks — allowing modelers to assemble arbitrary model structures
from simple lumped configurations to complex semi-distributed catchment networks.
The framework ships with pre-built implementations of well-known models (GR4J, HBV,
HYMOD) while supporting custom element creation. All differential equations are solved
numerically using configurable approximators (Explicit/Implicit Euler, Runge-Kutta 4)
with pluggable root finders (Pegasus, Newton). Numba JIT compilation provides optional
performance acceleration.
**Publication**: Dal Molin et al. (2021), *Geoscientific Model Development*, 14, 7047–7072,
doi:10.5194/gmd-14-7047-2021
**Repository**: https://github.com/dalmo1991/superflexPy
**Documentation**: https://superflexpy.readthedocs.io
---
## Installation
### From PyPI (recommended)
```bash
python3 -m venv venv
source venv/bin/activate
pip install superflexpy
```
### From source
```bash
python3 -m venv venv
source venv/bin/activate
cd source/repo
pip install -e .
```
### Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| numpy | 1.24.3 | Array operations, numerical computation |
| numba | 0.57.1 | JIT compilation for ODE solvers |
### Optional dependencies (for examples/calibration)
| Package | Purpose |
|---------|---------|
| pandas | Data I/O for CSV/tabular forcing data |
| scipy | Optimization for parameter calibration |
| matplotlib | Plotting results |
| spotpy | Advanced calibration algorithms |
---
## Architecture: Four-Level Hierarchy
SuperflexPy organizes hydrological models into four nested levels:
### Level 1: Elements (Atomic Components)
Elements are the smallest building blocks. Types include:
| Element Type | Base Class | Description | Examples |
|-------------|------------|-------------|----------|
| Reservoir | `ODEsElement` | Storage governed by dS/dt = In - Out | PowerReservoir, UnsaturatedReservoir, ProductionStore, RoutingStore |
| Lag Function | `LagElement` | Time-delay convolution | UnitHydrograph1, UnitHydrograph2 |
| Connector | `BaseElement` | Route/split/merge fluxes | Splitter, Junction, Transparent |
| Filter | `BaseElement` | Transform fluxes without storage | InterceptionFilter, FluxAggregator |
### Level 2: Units (Element Networks)
A Unit connects Elements into a **directed acyclic graph (DAG)** organized in layers.
Elements in the same layer can run in parallel. A Unit represents a single model
structure (e.g., one complete GR4J or HBV model).
### Level 3: Nodes (Parallel Units)
A Node aggregates multiple Units running in parallel with fractional weights,
representing Hydrological Response Units (HRUs) within a sub-catchment.
### Level 4: Network (Routed Nodes)
A Network connects Nodes in a tree topology for semi-distributed modelling,
routing accumulated flows from headwater to outlet.
---
## Pipeline (8 stages)
| # | Stage | Tool(s) | Description |
|---|-------|---------|-------------|
| s1 | Forcing Preparation | `convert_forcing.py` | Convert global/station data to numpy arrays (mm/d) |
| s2 | Parameter Setup | `convert_parameters.py` | Prepare parameters from soil/land-use data |
| s3 | Model Assembly | Python API | Build Element→Unit→Node→Network hierarchy |
| s4 | Initial Conditions | Python API | Set initial states (S0, lag arrays) |
| s5 | Model Execution | `run_superflexpy.py` | Run simulation via `get_output()` |
| s6 | Output Extraction | `parse_output.py` | Extract streamflow, states to CSV |
| s7 | Calibration | `run_superflexpy.py --calibrate` | Differential evolution on a date-bounded calibration window |
| s8 | Validation | `parse_output.py --val-start/--val-end` | NSE/KGE/PBIAS from `ki_tools_common.metrics.all_metrics` on the hold-out period |
**s7/s8 are TOOL stages, not "write your own loop".** They were documented as
"Python API" until 2026-08-21, which is why agents kept hand-rolling calibration
loops — and a hand-rolled loop that omits `model.reset_states()` between trials
is silently wrong (dt_019: the same parameter vector re-scores up to 5.45 mm/d
differently). Use the tools.
```bash
# s1 -> s8, one gauge
$SFP tools/convert_forcing.py --format caravan --input <gauge>.nc \
--start 1979-01-01 --end 1990-12-31 --pet-min 0 --output forcing.json
$SFP tools/convert_parameters.py --model gr4j --output params.json
$SFP tools/run_superflexpy.py --model gr4j --forcing forcing.json \
--params-json params.json --architecture numba --diagnostics \
--calibrate --cal-start 1981-01-01 --cal-end 1985-12-31 \
--val-start 1986-01-01 --val-end 1990-12-31 \
--free-params x1 x3 x4 x2 cpet --output results.json
$SFP tools/parse_output.py --input results.json --output series.csv --warmup 731 \
--cal-start 1981-01-01 --cal-end 1985-12-31 \
--val-start 1986-01-01 --val-end 1990-12-31 --metrics-json metrics.json
```
`--params-json` is the s2 → s5 hand-off: it takes both the values AND the
calibration ranges that `convert_parameters.py` publishes, so the search space
is the one s2 declared rather than a range retyped by hand.
`--architecture numba` is ~30x faster than `python` and gives bit-identical
results (verified) — always use it for calibration.
`--diagnostics` emits dag outputs `AET`, `S` and `F` plus a term-by-term water
balance built from the model's own element fluxes (not from a residual, which
would always close and prove nothing).
---
## Tools Reference
| Tool | Stage | Script Path | Lines | Purpose |
|------|-------|-------------|-------|---------|
| convert_forcing | s1 | tools/convert_forcing.py | ~200 | Convert CSV/station forcing to model input arrays |
| convert_parameters | s2 | tools/convert_parameters.py | ~180 | Map soil/land-use to model parameters |
| run_superflexpy | s5 | tools/run_superflexpy.py | ~220 | Execute model and save outputs |
| parse_output | s6 | tools/parse_output.py | ~170 | Parse raw output to standardized CSV |
---
## Input Format and Units
### Forcing Data
SuperflexPy accepts forcing as **lists of numpy arrays**. Each array is one flux
time series with length = number of timesteps.
| Variable | Symbol | Unit | Array Index | Notes |
|----------|--------|------|-------------|-------|
| Precipitation | P | mm/d (or mm/timestep) | 0 or 1 | Total rainfall |
| Potential Evapotranspiration | PET | mm/d (or mm/timestep) | 0 or 1 | Depends on element |
| Temperature | T | °C | varies | Only if snow module used |
**CRITICAL**: Input order varies by element type:
- `InterceptionFilter`: [PET, P] — PET is index 0
- `UnsaturatedReservoir`: [P, PET] — P is index 0
- `PowerReservoir`: [P] — single input
- `Unit` (e.g., GR4J): [PET, P] — follows first element's convention
### Input File Format (test data)
The test data uses space-separated format with 6-line header:
```
Maimai data courtesy Jeff McDonnell
1.0 ! stepsize (d)
ST1: All data in mm/d
ST2
ST3
* ! list directed format ok here
year, month, day, hour, min, QC, P (mm/d), E (mm/d), Q_obs (mm/d)
1985 1 2 0 0 0 1.000000e-001 0.000000e+000 2.646000e-001
```
### Parameter Format
Parameters are set as Python dictionaries:
```python
parameters = {
'element_id_param_name': value, # Prefixed format
}
# Example for GR4J Unit:
model.set_parameters({
'model_ps_x1': 300.0, # Production store capacity [mm]
'model_ps_alpha': 2.0, # PS exponent [-]
'model_rs_x2': 0.5, # Exchange coefficient [mm/d]
'model_rs_x3': 50.0, # Routing store capacity [mm]
'model_uh1_lag-time': 2.0, # UH1 lag [d]
'model_uh2_lag-time': 4.0, # UH2 lag [d]
})
```
---
## Output Format
### Direct API Output
`model.get_output()` returns a **list of numpy arrays** — one per output flux.
For most single-outlet models, the result is `[Q_array]` where Q is in mm/timestep.
### Network Output
`network.get_output()` returns a **dict** mapping node IDs to lists of output arrays.
### Reference Results Format (CSV)
```csv
# Q_FR, S_FR
3.159780e-05, 9.996840e-02
3.166701e-01, 3.983298e+00
```
Column order is model-specific. For the FR test: streamflow Q (mm/d), storage S (mm).
---
## Output Description
Source: `dag.yaml`. The dag is the authority for observable outputs and validation
rank; if this section ever disagrees with `dag.yaml`, the dag wins.
**Headline output**: `Q_sim` is the dag's rank-1 variable, so this model is judged by
simulated total streamflow at the catchment / Unit outlet.
> `Q_sim` — Simulated total streamflow at the catchment / Unit outlet; model.get_output() returns a list of numpy arrays (one per output flux). (`mm/d`)
| Output variable (dag `var`) | Rank / role | Unit | Description |
|---|---:|---|---|
| `Q_sim` | 1 | mm/d | Simulated total streamflow at the catchment / Unit outlet; model.get_output() returns a list of numpy arrays (one per output flux). |
| `Network_Q` | dag output | see `dag.yaml` | Listed as an additional dag output. |
| `AET` | dag output | see `dag.yaml` | Listed as an additional dag output. |
| `S` | dag output | see `dag.yaml` | Listed as an additional dag output. |
| `F` | dag output | see `dag.yaml` | Listed as an additional dag output. |
---
## Unit Conversion Table
Exact shapes live in `docs/format_spec.yaml`. This table restates the units already
documented in this KI body and the dag headline output; verify raw source metadata
before adding new conversions.
| Variable | Source unit documented here | Model / output unit | Factor | Type |
|---|---|---|---|---|
| Precipitation (`P`) | mm/d | mm/d | x1 | already model-ready |
| Potential evapotranspiration (`PET`) | mm/d | mm/d | x1 | already model-ready |
| Temperature (`T`) | degC | degC | x1 | already model-ready when snow modules use it |
| Timestep (`dt`) | days (float) | days (float) | x1 | already model-ready |
| Storage (`S`, `S0`) | mm | mm | x1 | already model-ready |
| Catchment area | km2 | km2 | x1 | already model-ready for Node area inputs |
| HRU weights | fraction [0,1] | fraction [0,1] | x1 | already model-ready |
| `Q_sim` | model output | mm/d | x1 | dag rank-1 output |
---
## Unit Trap Table
These are the most common unit-related errors that cause silent model failures.
| # | Trap ID | Variable | Expected Unit | Common Wrong Unit | Impact |
|---|---------|----------|---------------|-------------------|--------|
| 1 | dt_001 | Precipitation | mm/d | m/d, mm/h, in/d | Flows off by 1000x, 24x, or 25.4x |
| 2 | dt_002 | PET | mm/d | mm/month, W/m² | Severe water balance error |
| 3 | dt_003 | Timestep | days (float) | hours, seconds | ODE solver mismatch |
| 4 | dt_004 | Storage (S0) | mm | m, cm | Initial transient or instability |
| 5 | dt_005 | x1 (PS capacity) | mm | m, cm | Ps fraction (S/x1) corrupted |
| 6 | dt_006 | x3 (RS capacity) | mm | m | Qr equation breaks |
| 7 | dt_007 | x4 (lag time) | d | h, timesteps | UH weights malformed |
| 8 | dt_008 | Area (Node) | km² | m², ha | Network routing flux wrong |
| 9 | dt_009 | Weights (Node) | fraction [0,1] | percentage [0,100] | Outputs scaled 100x |
| 10 | dt_010 | x2 (exchange) | mm/d | mm/h | Exchange flux wrong |
---
## Critical Domain Knowledge
### dk_001: Input array order is element-specific (SILENT)
The order of fluxes in `set_input()` varies between element types. The
`InterceptionFilter` expects `[PET, P]` while `UnsaturatedReservoir` expects
`[P, PET]`. Swapping these produces nonsensical results with no error message.
Always check the element's `set_input()` docstring.
### dk_002: Timestep must match forcing resolution (SILENT)
`element.set_timestep(dt)` sets the ODE integration step in **days** (float).
If forcing is daily, `dt=1.0`. If hourly, `dt=1/24`. A mismatch means the
ODE solver integrates over the wrong time interval, producing silently wrong
storage dynamics.
### dk_003: Numba JIT compilation delay on first call
The first call to `get_output()` with Numba-based solvers triggers JIT
compilation, which can take 5–30 seconds. Subsequent calls are fast. This is
not a hang — do not kill the process.
### dk_004: Parameter prefixing follows hierarchy
When accessing parameters through a Unit or higher, names are automatically
prefixed: `unit_id_element_id_param_name`. Getting this wrong raises a
`KeyError`. Use `model.get_parameters_name()` to discover the actual names.
### dk_005: Lag states must be initialized as None
For `LagElement` subclasses (UnitHydrograph1/2), the initial state must be
`{'lag': None}`. The framework auto-creates the lag array from the lag-time
parameter. Providing a pre-allocated array of wrong length causes crashes.
### dk_006: Splitter weight/direction matrices must be consistent
The `Splitter` element uses `weight` and `direction` matrices to distribute
fluxes. Dimension mismatches between these matrices and the number of
upstream/downstream elements cause cryptic index errors. The weight matrix
shape is `[n_downstream][n_upstream_fluxes]` and direction is
`[n_downstream][n_upstream_fluxes]`.
### dk_007: Network topology must be a tree (no cycles, no branching)
The `Network` class only supports tree topologies: each node has at most one
downstream connection. Cycles cause infinite loops. Multiple downstream
connections are not supported. Use `Splitter` elements within a Unit instead.
### dk_008: EVERY repeat `get_output()` needs `reset_states()` first (SILENT)
Superseded measurement, 2026-08-21 — the hedge that used to be here ("the lag
function state behavior depends on the implementation") is resolved:
* `Unit.reset_states()` DOES restore both reservoir storage and the
`LagElement` memory (`element.py:712` re-deepcopies a `None` init state back
to `None`). You do **not** need to reinitialize lag states by hand.
* But you MUST call it. SuperflexPy carries state forward across calls, so a
second `get_output()` with **identical parameters** continues from the first
run's storage. Measured on GR4J over 500 days: max difference 5.45 mm/d
without the reset, exactly 0.0 with it.
* Consequence for calibration: without the reset, every trial starts from
whatever the previous trial left behind, so the optimiser is fitting
evaluation ORDER. Nothing errors. See dt_019.
The package's own example fixes the ordering — `set_parameters` →
`reset_states` → `get_output` (`examples/02_calibrate_a_model.ipynb`).
`run_superflexpy.py --calibrate` already does this; prefer it to a hand loop.
---
## Pre-Built Models
### GR4J (4 parameters)
**Structure**: InterceptionFilter → ProductionStore → Splitter(90/10) →
[UH1 | UH2] → [RoutingStore | Transparent] → Junction → FluxAggregator
Defaults below are the ones the TOOLS actually ship (`convert_parameters.py`
`MODEL_DEFAULTS` and `run_superflexpy.py` `build_model`), corrected 2026-08-21 —
this table previously said x1=50, x3=20, x4=3.5, which matched neither tool.
| Parameter | Symbol | Default | Range | Units | Controls |
|-----------|--------|---------|-------|-------|----------|
| PS capacity | x1 | 350.0 | 10–2000 | mm | Production store size |
| Exchange coeff | x2 | 0.0 | -5–5 | mm/d | Groundwater exchange (**+ = LOSS**, see dt_018) |
| RS capacity | x3 | 90.0 | 1–500 | mm | Routing store size |
| UH time | x4 | 1.7 | 0.5–10 | d | Unit hydrograph base time; sets `uh1_lag-time = x4` AND `uh2_lag-time = 2·x4` |
| PS exponent | alpha | 2.0 | fixed | - | Evap/precip partition |
| Perc exponent | beta | 5.0 | fixed | - | Percolation rate |
| Perc coeff | ni | 4/9 | fixed | - | Percolation scaling |
| RS exponent | gamma | 5.0 | fixed | - | Routing outflow |
| Exchange exp | omega | 3.5 | fixed | - | Exchange flux |
**Input**: `[PET, P]` in mm/d
**Output**: `[Q]` in mm/d
### HBV-style (UnsaturatedReservoir + PowerReservoir)
| Parameter | Default | Range | Units | Element |
|-----------|---------|-------|-------|---------|
| Smax | 200.0 | 10–1000 | mm | UnsaturatedReservoir |
| Ce | 1.0 | 0.1–2.0 | - | UnsaturatedReservoir — this IS the PET correction coefficient; with a reanalysis PET product it is a first-order lever, not a nuisance (dt_020) |
| m | 0.01 | 0.001–1.0 | - | UnsaturatedReservoir |
| beta | 2.0 | 0.5–5.0 | - | UnsaturatedReservoir |
| k | 0.05 | 0.001–1.0 | 1/d | PowerReservoir |
| alpha | 2.5 | 1.0–5.0 | - | PowerReservoir |
Defaults corrected 2026-08-21 to match `convert_parameters.py` /
`run_superflexpy.py` (this table said Smax 50.0 and k 0.01, matching neither).
HBV has **no interception filter**, so unlike GR4J it does not lose
`min(P, PET)` off the top — which is why it needs `Ce` rather than `cpet`.
**Input**: `[P, PET]` in mm/d
**Output**: `[Q]` in mm/d
### HYMOD
| Parameter | Default | Range | Units | Element |
|-----------|---------|-------|-------|---------|
| Smax | 50.0 | 10–500 | mm | UpperZone |
| m | 0.01 | 0.001–1.0 | - | UpperZone |
| beta | 2.0 | 0.5–5.0 | - | UpperZone |
| k | 0.1 | 0.01–1.0 | 1/d | LinearReservoir (×3 + lower) |
---
## Numerical Solvers
| Solver | Type | Stability | Speed | Use Case |
|--------|------|-----------|-------|----------|
| ExplicitEuler | Explicit | Conditional | Fast | Quick tests, smooth forcings |
| ImplicitEuler | Implicit | Unconditional | Medium | Production runs, stiff ODEs |
| RungeKutta4 | Explicit | Conditional | Slow | High accuracy needs |
| Root Finder | Method | Architecture | Notes |
|------------|--------|-------------|-------|
| PegasusPython | Bracketing | Python | Robust, default choice |
| PegasusNumba | Bracketing | Numba | Fast, JIT-compiled |
---
## Calibration Parameters (Priority Order)
For GR4J — calibrate these first, in this order:
| Priority | Parameter | Range | Sensitivity | Notes |
|----------|-----------|-------|-------------|-------|
| 1 | x1 | 10–2000 mm | Very High | Controls production/infiltration split |
| 2 | x3 | 1–500 mm | High | Controls routing store depletion rate |
| 3 | x4 | 0.5–10 d | High | Controls peak timing. **Not a SuperflexPy parameter name** — use the `x4` alias in `run_superflexpy.py`, which sets `gr4j_uh1_lag-time = x4` and `gr4j_uh2_lag-time = 2·x4` |
| 4 | x2 | -5–5 mm/d | Medium | **Positive = EXPORT (loss), negative = IMPORT (gain)** — corrected 2026-08-21 against `RoutingStore.get_output()` ("Exchange flux (F), positive if loss"); the old "positive = import" reading here and in dt_018 was inverted |
| 5 | cpet | 0.2–2.0 – | High with a reanalysis PET product | PET-forcing correction; GR4J has no internal PET coefficient (dt_020) |
For HBV-style:
| Priority | Parameter | Range | Sensitivity |
|----------|-----------|-------|-------------|
| 1 | Smax | 10–1000 mm | Very High |
| 2 | beta | 0.5–5.0 | High |
| 3 | k | 0.001–1.0 | High |
| 4 | alpha | 1.0–5.0 | Medium |
| 5 | Ce | 0.1–2.0 | Medium |
| 6 | m | 0.001–1.0 | Low |
---
## Quick Start
```python
import numpy as np
from superflexpy.implementation.models.gr4j import model
# Load forcing data
P = np.array([0.1, 4.2, 0.0, 13.2, 2.4, 14.3, 4.4, 1.7, 0.0, 50.6]) # mm/d
PET = np.zeros(10) # mm/d
# Set timestep
model.set_timestep(1.0) # daily
# Set input: [PET, P] for GR4J
model.set_input([PET, P])
# Run model
Q = model.get_output() # Returns [Q_array] in mm/d
# Access states
states = model.get_states()
param_names = model.get_parameters_name()
# Reset for next run
model.reset_states()
```
### Running unit tests
```bash
cd source/repo
python -m pytest test/unittest/01_FR.py -v
```
---
## Coupling Points
| # | Source | Target | Variable | Notes |
|---|--------|--------|----------|-------|
| 1 | Climate reanalysis | SuperflexPy | P, PET (mm/d) | Must match timestep |
| 2 | Soil databases (HWSD) | SuperflexPy | Smax, beta | Pedotransfer functions |
| 3 | SuperflexPy | Routing model | Q (mm/d or m³/s) | Multiply by area for volumetric |
| 4 | SuperflexPy | Water quality | Q partitioned by path | Use splitter outputs |
| 5 | Calibration framework | SuperflexPy | Parameters dict | Via set_parameters() |
---
## Data Requirements
| Data | Source | Format | Units |
|------|--------|--------|-------|
| Precipitation | Station/gridded | numpy array | mm/d |
| PET | Calculated/gridded | numpy array | mm/d |
| Observed Q | Gauging station | numpy array | mm/d |
| Catchment area | GIS | scalar float | km² |
| HRU weights | GIS/analysis | list of floats | fraction [0,1] |
---
## Validated Results
Validated result values are not stated in the extracted KI facts. When a run is
evaluated, judge it against `docs/validation_convention.yaml`; do not substitute
remembered thresholds.
### Performance Metrics — judged against the field's bar, not intuition
Source: `docs/validation_convention.yaml`. Every cited threshold below carries the
citation key provided by the convention. A null convention band is written as
`no cited threshold`.
| Dag variable | Metric | Direction | Convention bar |
|---|---|---|---|
| `Q_sim` | NSE | maximize | very_good >= 0.8 (`moriasi2015`); good >= 0.7 (`moriasi2015`); satisfactory >= 0.5 (`moriasi2015`) |
| `Q_sim` | PBIAS | zero_centered | very_good band = 5.0 (`moriasi2015`); good band = 10.0 (`moriasi2015`); satisfactory band = 15.0 (`moriasi2015`) |
| `Q_sim` | PBIAS | zero_centered | satisfactory: no cited threshold |
| `Network_Q` | NSE | maximize | very_good >= 0.8 (`moriasi2015`); good >= 0.7 (`moriasi2015`); satisfactory >= 0.5 (`moriasi2015`) |
| `Network_Q` | PBIAS | zero_centered | very_good band = 5.0 (`moriasi2015`); good band = 10.0 (`moriasi2015`); satisfactory band = 15.0 (`moriasi2015`) |
| Metric | Calibration | Validation | Full Period | Bar (convention, cited) |
|---|---|---|---|---|
| NSE for `Q_sim` | not stated in extracted facts | not stated in extracted facts | not stated in extracted facts | satisfactory >= 0.5 (`moriasi2015`), good >= 0.7 (`moriasi2015`), very_good >= 0.8 (`moriasi2015`) |
| PBIAS for `Q_sim` | not stated in extracted facts | not stated in extracted facts | not stated in extracted facts | satisfactory band = 15.0 (`moriasi2015`), good band = 10.0 (`moriasi2015`), very_good band = 5.0 (`moriasi2015`); duplicate satisfactory row: no cited threshold |
### Data Replacement Tracking
| Component | Source | Status | Notes |
|---|---|---|---|
| Forcing | Pipeline | Pending | Use `convert_forcing.py`; units documented as mm/d. |
| Parameters | Pipeline | Pending | Use `convert_parameters.py`; parameter units are documented in the model sections above. |
| Model execution | SuperflexPy package | Pending | Must run the real package; `Q_sim` is the rank-1 output. |
| Output parsing | Pipeline | Pending | Use `parse_output.py`; bind validation to `Q_sim` first. |
---
## Diagnostic Triplets Summary
| ID | Severity | Domain | Summary |
|----|----------|--------|---------|
| dt_001 | silent | unit_conversion | Precipitation units wrong (m vs mm) |
| dt_002 | silent | unit_conversion | PET in mm/month instead of mm/d |
| dt_003 | silent | unit_conversion | Timestep mismatch with forcing |
| dt_004 | silent | unit_conversion | Storage initial state wrong units |
| dt_005 | silent | parameter_format | Input array order swapped |
| dt_006 | fatal | parameter_format | Parameter prefix wrong |
| dt_007 | fatal | parameter_format | Lag state not set to None |
| dt_008 | silent | parameter_format | Splitter weight matrix wrong shape |
| dt_009 | degraded | runtime | Numba JIT first-call delay |
| dt_010 | fatal | runtime | Negative storage from explicit solver |
| dt_011 | silent | architecture | Network has cycle |
| dt_012 | silent | architecture | Node weights don't sum to 1 |
| dt_013 | silent | unit_conversion | Area in m² instead of km² |
| dt_014 | silent | unit_conversion | Node weights as percentages |
| dt_015 | degraded | runtime | Root finder non-convergence |
| dt_016 | silent | parameter_format | Time-varying param wrong length |
| dt_017 | fatal | dependency | Numba version incompatible with numpy |
| dt_018 | silent | unit_conversion | x2 exchange sign: **positive = LOSS** (entry corrected 2026-08-21; it previously said the opposite) |
| dt_019 | silent | runtime | Repeat `get_output()` without `reset_states()` — calibration fits evaluation order |
| dt_020 | silent | unit_conversion | Reanalysis PET exceeds the demand the structure needs; calibrate `cpet` (GR4J) / `Ce` (HBV) |
| dt_021 | silent | unit_conversion | Negative reanalysis PET makes GR4J's interception filter ADD precipitation; use `--pet-min 0` |
See `diagnostics/triplets.yaml` for full symptom→diagnosis→remedy entries (21 entries; dt_019-dt_021 added 2026-08-21, dt_018 corrected).
---
## File Structure
```
ki/
├── SKILL.md # This file — main reference
├── tools/
│ ├── convert_forcing.py # s1: Forcing data converter
│ ├── convert_parameters.py # s2: Parameter converter
│ ├── run_superflexpy.py # s5: Model execution wrapper
│ └── parse_output.py # s6: Output parser
├── docs/
│ ├── s1_forcing_preparation.md # Forcing preparation skill
│ ├── s2_parameter_setup.md # Parameter configuration skill
│ ├── s3_model_assembly.md # Model assembly skill
│ ├── s5_model_execution.md # Model execution skill
│ └── s6_output_extraction.md # Output parsing skill
└── diagnostics/
└── triplets.yaml # 21 diagnostic triplets
```
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!