Skip to content
Back to skills

SuperflexPy

ASecurity

**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 ---

  • 200 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 11, 2026
testingpythongobashnodedebugginggitapidatabaseperformancedocumentation

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 19 files and shows the line behind each finding

Scanned September 11, 2026

npx -y skills add lzwei196/KISS---Knowledge-Infrastructure-for-Scientific-Simulation --skill SuperflexPy --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of SuperflexPy?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for SuperflexPy
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lzwei196-superflexpy/badge)](https://www.skillsdirectory.com/skills/lzwei196-superflexpy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
> **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
```

Files in this skill

  • SKILL.md33.4 KB
  • dag.yaml32 KB
  • diagnostics/triplets.yaml26.5 KB
  • docs/REFERENCES.md1.7 KB
  • docs/format_spec.yaml26.4 KB
  • docs/papers.json5.1 KB
  • docs/s1_forcing_preparation.md2.8 KB
  • docs/s2_parameter_setup.md3.3 KB
  • docs/s3_model_assembly.md4.3 KB
  • docs/s5_model_execution.md3.6 KB
  • docs/s6_output_extraction.md3.3 KB
  • docs/validation_convention.yaml12.1 KB
  • knowledge_infrastructure.yaml2.7 KB
  • preflight_check.py9.1 KB
  • tools/convert_forcing.py19.5 KB
  • tools/convert_parameters.py10.5 KB
  • tools/parse_output.py16.2 KB
  • tools/run_superflexpy.py33.8 KB
  • workflow/workflow.md1.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…