Imported from lzwei196/KISS-Knowledge-Infrastructure-for-Scientific-Simulation (
models/mHM/SKILL.md). Install upstream withnpx skills add lzwei196/KISS-Knowledge-Infrastructure-for-Scientific-Simulation --skill mHM. Copyright stays with the author.
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:
- Check
diagnostics/triplets.yamlfor a matching error pattern- Try the fix suggested in the triplet's
remedysection- 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. Doing so produces scientifically invalid results and defeats the purpose of the KI.
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:
- Check triplets —
diagnostics/triplets.yamlmay already cover this error- Read official docs — The model's own documentation for expected formats/units
- Find working examples — Check
outputs/or the model's shipped test data- 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 — 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/ (19 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 (11 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 (37 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. |
Projected 2026-08-17 from the KI's actual contents — 9 components present. Refresh: python3 ki_tools_common/generate_skill_map.py --ki_dir <this KI>.
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/s0_config/configure_mhm_basin.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s0_config/configure_mhm_basin.py --help |
tools/s10_regionalize/transfer_mpr_params.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s10_regionalize/transfer_mpr_params.py --help |
tools/s1_domain/delineate_basin_merit.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s1_domain/delineate_basin_merit.py --help |
tools/s1_domain/generate_latlon_files.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s1_domain/generate_latlon_files.py --help |
tools/s1_domain/setup_mhm_domain.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s1_domain/setup_mhm_domain.py --help |
tools/s2_morphology/generate_gauge_grid.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_morphology/generate_gauge_grid.py --help |
tools/s2_morphology/glim_to_mhm_geology.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_morphology/glim_to_mhm_geology.py --help |
tools/s2_morphology/hwsd_to_mhm_soil.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_morphology/hwsd_to_mhm_soil.py --help |
tools/s2_morphology/landcover_to_mhm_luse.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_morphology/landcover_to_mhm_luse.py --help |
tools/s2_morphology/prepare_morpho_data.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_morphology/prepare_morpho_data.py --help |
tools/s2_morphology/validate_morph_grids.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_morphology/validate_morph_grids.py --help |
tools/s3_mpr/generate_mhm_parameters.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s3_mpr/generate_mhm_parameters.py --help |
tools/s4_forcing/convert_forcing_to_mhm.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s4_forcing/convert_forcing_to_mhm.py --help |
tools/s5_gauge/prepare_mhm_gauge.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s5_gauge/prepare_mhm_gauge.py --help |
tools/s6_namelist/generate_mhm_namelists.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s6_namelist/generate_mhm_namelists.py --help |
tools/s7_execute/run_mhm.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s7_execute/run_mhm.py --help |
tools/s8_postprocess/compare_mhm_vic.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s8_postprocess/compare_mhm_vic.py --help |
tools/s8_postprocess/parse_mhm_output.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s8_postprocess/parse_mhm_output.py --help |
tools/s9_calibration/setup_mhm_calibration.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s9_calibration/setup_mhm_calibration.py --help |
19 public tools; _-prefixed helpers and packaging files excluded.
Data Preparation
Forcing data
Data Sources: Use from ki_tools_common.load_forcing import load_daily_forcing for CMFD/MSWX/NASA POWER.
Then convert to mHM NetCDF format using this KI's tool: tools/s4_forcing/convert_forcing_to_mhm.py
Data Validation Reference: See data_ki/CMFD/SKILL.md for CMFD unit documentation and known traps.
mHM Knowledge Infrastructure for HydroCraft
Model Overview
mHM (mesoscale Hydrological Model) is a grid-based distributed hydrological model developed at the Helmholtz Centre for Environmental Research (UFZ), Leipzig. Its defining innovation is Multiscale Parameter Regionalization (MPR) -- a methodology that derives spatially distributed model parameters from high-resolution physiographic data through transfer functions with a small set of global parameters (~50-70). Because these global parameters encode process understanding (not location-specific values), they can be calibrated on gauged basins and transferred directly to ungauged basins.
Binary: KISSPATH_BINARIES/mhm/mhm
Source: KISSPATH_BINARIES/mhm_src/ (v5.13.1, built from GitHub)
Test domain: KGE=0.75, NSE=0.77 on bundled Mosel basin test case
What Makes mHM Unique in HydroCraft
| Capability | VIC | WRF-Hydro | mHM |
|---|---|---|---|
| Parameter regionalization | No (per-cell) | No (lookup) | Yes (MPR) |
| Transfer to ungauged basins | Recalibration needed | Recalibration needed | Direct transfer |
| Calibration parameters | 6 per cell | ~15 per cell | ~70 global |
| Multi-basin simultaneous | No | No | Yes (native) |
| Built-in optimization | No | No | Yes (DDS, SCE) |
| Internal routing | External | Internal | MRM (internal) |
Strategic Value
mHM enables a "calibrate once, apply everywhere" paradigm. Calibrate global parameters on well-gauged basins, then predict discharge for any ungauged basin using the same parameters + local physiographic data. This is transformative for data-sparse regions.
6. Output Description
Source of truth: dag.yaml. The dag defines what this KI predicts and which
observable output is used as the headline validation target. If this section and
dag.yaml ever disagree, dag.yaml wins.
Headline output (validation_rank: 1):
mRM_runoff (routed discharge)— Routed streamflow discharge on the L11 routing network, extracted at gauge cells. (m3 s-1)
Output variable (dag var) |
Rank | Unit | Description |
|---|---|---|---|
mRM_runoff (routed discharge) |
1 | m3 s-1 |
Routed streamflow discharge on the L11 routing network, extracted at gauge cells. |
aET (actual evapotranspiration) |
other dag output | see dag.yaml |
Other observable output declared by the dag. |
SM (soil moisture) |
other dag output | see dag.yaml |
Other observable output declared by the dag. |
total_runoff (Q) |
other dag output | see dag.yaml |
Other observable output declared by the dag. |
baseflow (QB) |
other dag output | see dag.yaml |
Other observable output declared by the dag. |
recharge |
other dag output | see dag.yaml |
Other observable output declared by the dag. |
For HydroCraft coupling, the routed-discharge output is the primary judged
quantity. ET, soil moisture, total runoff, baseflow, and recharge are exposed for
diagnostics, cross-model checks, and downstream coupling, but their units and
observability metadata must be read from dag.yaml before binding or scoring.
Three-Grid Architecture (CRITICAL)
mHM operates on three hierarchical grids:
| Grid | Alias | Typical Resolution | Content |
|---|---|---|---|
| L0 | Morphological | ~100-1000 m | Raw physiographic data (DEM, soil, geology, land cover) |
| L1 | Hydrological | ~1-25 km | Model grid where water balance is computed |
| L11 | Routing | ~1-25 km | River routing network (MRM) |
CRITICAL CONSTRAINT: L1 and L11 resolutions must be integer multiples of L0 resolution. If L0=500m, then L1 must be 1000, 1500, 2000, ... etc. Violation causes ERROR: Resolution mismatch at startup.
CRITICAL CONSTRAINT — L0 MUST BE SQUARE (dt_r12). Make ncols_L0 == nrows_L0,
with the side an exact multiple of the L1/L11 ratio. setup_mhm_domain.py now
enforces this by padding with nodata cells.
Why: mHM's read_header_ascii() declares (header_ncols, header_nrows, …) but
every call site passes (nrows, ncols, …) positionally, e.g.
mo_meteo_handler.f90: call read_header_ascii(fName, unit, nrows2, ncols2, …).
The ASCII ncols line therefore lands in mHM's nrows, and
calculate_grid_properties() then derives xllcornerOut from ncolsIn — mixing
the x-origin with the y-extent. For a non-square L0 no geographically correct
header can satisfy the L2 check: at Wangjiaba (239 × 213 @ 0.01°) mHM demanded
xll=113.12, yll=31.35 while the true origin is (113.24, 31.46), and aborted with
L2_variable_init: size mismatch in grid file for level2. mHM's own test domains
are square (240 × 240), so upstream never trips over it. With a square L0 of side
N and ratio R, xllOut = xll0 + N·cs0 − (N/R)·cs1 = xll0 under either reading.
Coordinate system flag (iFlag_cordinate_sys in mhm.nml):
0= regular X-Y (projected/metric coordinates, cellsize in METERS)1= regular lat-lon (geographic coordinates, cellsize in DEGREES)
CRITICAL: For all HydroCraft basins using EPSG:4326 (lat/lon), set iFlag_cordinate_sys = 1. Using 0 with geographic coordinates causes cell areas to be computed as cellsize^2 in m^2 (e.g., 0.25*0.25 = 0.0625 m^2 instead of ~680 km^2), producing ZERO routed discharge.
Resolution units must match the coordinate system: When iFlag_cordinate_sys = 1, resolution_Hydrology and resolution_Routing must be in DEGREES (e.g., 0.25), NOT in meters.
Step 0 — Verify the basin BEFORE anything else (dt_s09)
Cross-check the shapefile's area against the gauge's published drainage area.
A truncated domain is unfixable downstream: if the grid contains half the
contributing area, simulated discharge is ~half of observed and no parameter set
can recover it. You will see good timing (r > 0.8) with a stubborn
PBIAS ≈ −50%, and you will waste the whole calibration chasing it.
Measured at Wangjiaba (2026-07-09):
| Source | Area | vs documented 30,630 km² |
|---|---|---|
data/shp/wangjiaba_shp (3 nested polygons, largest) |
15,952 km² | −48% |
whitebox D8 on china_dem_90m at the gauge |
15,782 km² | −48% |
MERIT-Hydro upa at the snapped outlet |
30,844 km² | +0.7% |
| MERIT-Hydro reverse-traced catchment | 29,638 km² | −3.2% |
Bare-earth D8 reproduces the shapefile's error rather than exposing it: on the
flat, leveed Huai plain the China 90 m DEM routes the Hong River (洪河, ~12,000 km²)
to a confluence just downstream of Wangjiaba. Two wrong methods agreeing is not
corroboration. MERIT-Hydro's flow direction is hydrologically corrected against
observed river networks and ships upa (upstream area, km²), so the outlet can be
snapped to the documented area instead of guessed:
python tools/s1_domain/delineate_basin_merit.py \
--outlet_lon 115.617 --outlet_lat 32.433 --target_area_km2 30630 \
--bbox 112.5 31.0 116.2 34.4 --snap_deg 0.06 \
--out_shp runs/basin/wangjiaba_merit.shp
It refuses to emit a basin more than --max_area_error_pct (default 10%) from the
target, and also writes the *_dir.tif / *_upa.tif clips that s2 consumes.
MERIT-Hydro's D8 encoding is exactly mHM's ArcGIS convention
(1=E, 2=SE, … 128=NE; 0 = mouth, −1 = inland depression, 255 = nodata), so
prepare_morpho_data.py --fdir_source merit --merit_dir_tif <…>_dir.tif upscales
it straight onto L0. Use merit on any flat or engineered basin; keep
--fdir_source dem only where relief genuinely controls the drainage.
Pipeline Stages
s1_domain delineate_basin_merit <-- FIRST: get the TRUE catchment
|
s0_config Configuration & directory setup
|
s1_domain Domain grid setup (L0, L1, L11)
|
s2_morphology Morphological data (DEM, soil, geology, land cover) -> ESRI ASCII grids
|
s3_mpr MPR parameter file (transfer function coefficients)
|
s4_forcing Meteorological forcing (CMFD/MSWX -> mHM NetCDF)
|
s5_gauge Observed discharge preparation
|
s6_namelist Namelist assembly (4 .nml files)
|
s7_execute Model execution
|
s8_postprocess Output extraction & analysis
|
s9_calibrate Built-in DDS/SCE calibration (optional)
|
s10_regionalize Transfer to ungauged basins (THE KEY STAGE)
Tools Reference
| Stage | Tool | Script Path | Purpose |
|---|---|---|---|
| s1 | delineate_basin_merit | tools/s1_domain/delineate_basin_merit.py |
Run FIRST. True catchment from MERIT-Hydro D8, snapped to the gauge's documented drainage area |
| s0 | configure_mhm_basin | tools/s0_config/configure_mhm_basin.py |
Create directory structure and initial config |
| s1 | setup_mhm_domain | tools/s1_domain/setup_mhm_domain.py |
Compute L0/L1/L11 grids from shapefile (pads L0 to a square) |
| s1 | generate_latlon_files | tools/s1_domain/generate_latlon_files.py |
Create lat/lon NetCDF for domain |
| s2 | prepare_morpho_data | tools/s2_morphology/prepare_morpho_data.py |
DEM, slope, aspect, flow dir, flow acc -> ASCII grids |
| s2 | hwsd_to_mhm_soil | tools/s2_morphology/hwsd_to_mhm_soil.py |
HWSD -> mHM soil classes + classdefinition.txt |
| s2 | glim_to_mhm_geology | tools/s2_morphology/glim_to_mhm_geology.py |
GLiM -> mHM geology classes |
| s2 | landcover_to_mhm_luse | tools/s2_morphology/landcover_to_mhm_luse.py |
AVHRR -> mHM land use classes |
| s2 | generate_gauge_grid | tools/s2_morphology/generate_gauge_grid.py |
Gauge location ASCII grid |
| s2 | validate_morph_grids | tools/s2_morphology/validate_morph_grids.py |
Check all grids have identical headers |
| s3 | generate_mhm_parameters | tools/s3_mpr/generate_mhm_parameters.py |
Create mhm_parameter.nml with climate-zone-aware defaults |
| s4 | convert_forcing_to_mhm | tools/s4_forcing/convert_forcing_to_mhm.py |
CMFD/MSWX -> mHM NetCDF forcing |
| s5 | prepare_mhm_gauge | tools/s5_gauge/prepare_mhm_gauge.py |
Convert obs Q to mHM gauge format |
| s6 | generate_mhm_namelists | tools/s6_namelist/generate_mhm_namelists.py |
Assemble all 4 .nml files |
| s7 | run_mhm | tools/s7_execute/run_mhm.py |
Execute with progress monitoring |
| s8 | parse_mhm_output | tools/s8_postprocess/parse_mhm_output.py |
Extract Q, ET, SM from NetCDF output |
| s8 | compare_mhm_vic | tools/s8_postprocess/compare_mhm_vic.py |
Cross-model comparison |
| s9 | setup_mhm_calibration | tools/s9_calibration/setup_mhm_calibration.py |
Configure/run DDS/SCE calibration, extract best MPR params |
| s10 | transfer_mpr_params | tools/s10_regionalize/transfer_mpr_params.py |
Apply calibrated params to new basin |
MPR Calibration and Regionalization Workflow (THE KEY WORKFLOW)
This is the workflow that makes mHM uniquely valuable. Without calibration, MPR parameters are German defaults (Mosel basin) that produce mediocre results elsewhere.
Reference (Wangjiaba 51030, Huai, 29,638 km², CMFD 0.25°, Oudin PET, L0 0.01° / L1 = L11 0.25°, spin-up 1980):
| Parameters | NSE | KGE | r | PBIAS |
|---|---|---|---|---|
| German MPR defaults, full 1981–1990 | 0.442 | 0.456 | 0.861 | +41.2% |
| defaults, cal 1981–1985 | 0.302 | 0.301 | 0.887 | +48.2% |
The uncalibrated defaults over-predict runoff in a humid subtropical basin
(they under-evaporate), the mirror image of the sign you get in dry basins. The
+41% bias, not the correlation, is what calibration must fix — r = 0.86
already shows the routing and forcing are right.
A prior Wangjiaba run reported
NSE = −0.344, PBIAS = −94.7%. That was not an mHM property: it was three stacked input defects — a 52%-truncated basin (dt_s09), an ×10800 CMFD daily precipitation factor (dt_s10), and a Hargreaves PET of 98 mm/day from a synthesised diurnal range (dt_s11). Fixing the inputs moved the uncalibrated model from −0.344 to +0.442 before a single parameter was touched.
11. Validated Results
Validation status: binary_only. The KI can execute the real mHM binary and
records benchmark and Wangjiaba reference results, but the field judgement of a
run comes from docs/validation_convention.yaml, not from intuition.
Headline Variable Judged by the Dag
The dag's rank-1 output is mRM_runoff (routed discharge) with unit m3 s-1:
Routed streamflow discharge on the L11 routing network, extracted at gauge cells.
Recorded Results in This Body
| Case | Period | Setup | NSE | KGE | r | PBIAS |
|---|---|---|---|---|---|---|
| Bundled Mosel basin test case | bundled test case | mHM v5.13.1 binary/source noted above | 0.77 | 0.75 | not stated | not stated |
| Wangjiaba 51030, Huai, 29,638 km2 | full 1981-1990 | German MPR defaults, CMFD 0.25 deg, Oudin PET, L0 0.01 deg / L1 = L11 0.25 deg, spin-up 1980 | 0.442 | 0.456 | 0.861 | +41.2% |
| Wangjiaba 51030, Huai, 29,638 km2 | cal 1981-1985 | German MPR defaults, CMFD 0.25 deg, Oudin PET, L0 0.01 deg / L1 = L11 0.25 deg, spin-up 1980 | 0.302 | 0.301 | 0.887 | +48.2% |
| Prior defective Wangjiaba run | not stated | truncated basin plus forcing/PET defects; kept only as a diagnostic anti-example | -0.344 | not stated | not stated | -94.7% |
Convention Bars from docs/validation_convention.yaml
| Dag variable | Metric | Direction | Bands and citation keys |
|---|---|---|---|
mRM_runoff (routed discharge) |
nse |
maximize | satisfactory: 0.5 (moriasi2007, moriasi2015, arnold2012); good: 0.65 (moriasi2007, moriasi2015, arnold2012); very_good: 0.75 (moriasi2007, moriasi2015, arnold2012) |
mRM_runoff (routed discharge) |
pbias |
zero_centered | satisfactory: 25 (moriasi2007, moriasi2015); good: 15 (moriasi2007, moriasi2015); very_good: 10 (moriasi2007, moriasi2015) |
aET (actual evapotranspiration) |
spaef |
maximize | satisfactory: no cited threshold (koch2018, demirel2018, dembele2022); good: no cited threshold (koch2018, demirel2018, dembele2022); very_good: no cited threshold (koch2018, demirel2018, dembele2022) |
aET (actual evapotranspiration) |
nse |
maximize | satisfactory: no cited threshold (convention cites no keys) |
For the rank-1 discharge variable, the recorded Wangjiaba default runs do not
meet the nse satisfactory threshold of 0.5 (moriasi2007, moriasi2015,
arnold2012) and do not meet the pbias satisfactory threshold of 25
(moriasi2007, moriasi2015). They are useful setup diagnostics, not final
calibrated validation results.
Step 1: Calibrate on a Gauged Basin
# 1a. Set up basin through s0-s8 as usual (data prep + forward run to verify setup)
# 1b. Configure calibration (generates mhm.nml with optimize=.TRUE.)
python tools/s9_calibration/setup_mhm_calibration.py \
--run_dir /path/to/gauged_basin \
--opti_method 1 \
--opti_function 10 \
--n_iterations 1000 \
--basin_type humid_subtropical
# 1c. Run calibration (this executes the mHM binary with optimization enabled)
python tools/s9_calibration/setup_mhm_calibration.py \
--run_dir /path/to/gauged_basin \
--execute
# 1d. Parse results (extracts best parameters from FinalParam.nml)
python tools/s9_calibration/setup_mhm_calibration.py \
--run_dir /path/to/gauged_basin \
--parse_results
Optimizer options:
opti_method=1(DDS, recommended): Fast convergence, 1000 iterations usually sufficientopti_method=3(SCE): More robust but slower, good for complex parameter spaces
opti_function — verified against the v5.13.1 source. DO NOT GUESS.
src/mRM/mo_mrm_objective_function_runoff.F90 (discharge-only) and
src/mHM/mo_objective_function.F90 (multi-variable):
| Code | Objective (minimised) | Needs &optional_data? |
|---|---|---|
| 1 | 1 - NSE(Q) — use when NSE is the reported metric |
no |
| 2 | 1 - lnNSE(Q) (low flows) |
no |
| 3 | 1 - 0.5*(NSE+lnNSE)(Q) |
no |
| 9 | 1 - KGE(Q) — the real KGE-of-discharge objective |
no |
| 14 | multi-gauge KGE, power-6 norm | no |
| 10–13 | KGE / PD / SSE / corr of soil moisture | YES |
| 15 | KGE(Q) * RMSE(TWS) |
YES |
| 17 | KGE of neutrons | YES |
| 27, 29, 30 | objectives involving ET | YES |
TRAP (dt_r15).
opti_function=10is NOT KGE of discharge — it is1 - KGE of catchment-average SOIL MOISTURE. Earlier revisions of this file recommended it; following that advice aborts the run withPOSITION_NML: namelist /optional_data/ MISSING. For discharge calibration use 1 (1−NSE) or 9 (1−KGE).
TRAP (dt_r14).
nIterationsmust be ≥ 6. FORCESmo_dds.F90:169doesstop 'Error DDS: max function evals must be minimum 6', and a Fortranstop '<msg>'exits with status 0 — so mHM looks like a clean success while writing noFinalParam.nml. Never trust the exit code alone; assert thatFinalParam.nmlexists.
TRAP (dt_r16).
setup_mhm_calibration.pyused to rewritemhm.nmlon every invocation. Because the documented recipe calls it three times (configure →--execute→--parse_results) and the last two carry no optimizer flags, the--executecall silently stamped the argparse defaults back intomhm.nml. Alwaysgrep opti_function nIterations mhm.nmlimmediately before launching.
Calibration / validation split. eval_Per is the window the optimizer sees.
Set it to the CALIBRATION years only, and give the spin-up as warming_Days
(mHM warms up on the days immediately before eval_Per, so the forcing must
start earlier). Then re-run forward over cal+val with --param_nml FinalParam.nml
and score the two periods separately:
# calibrate on 1981-1985 only, 1980 as spin-up
python tools/s6_namelist/generate_mhm_namelists.py --config ... --domain_info ... \
--warmup_days 365 --eval_start_year 1981 --eval_end_year 1985 \
--optimize --opti_method 1 --opti_function 1 --n_iterations 2000
# forward run over 1981-1990 with the calibrated parameters
python tools/s6_namelist/generate_mhm_namelists.py --config ... --domain_info ... \
--warmup_days 365 --eval_start_year 1981 --eval_end_year 1990 \
--param_nml <cal_dir>/FinalParam.nml
Basin type presets adjust parameter bounds for the optimizer:
humid_subtropical: wider infiltration/interflow, narrower snowsemi_arid: high infiltration shape, narrow interflow, low rechargecold_alpine: wide snow parameters, moderate infiltrationtropical: no snow, high interflow, intense rainfall infiltration
Step 2: Transfer to Ungauged Basin
# 2a. Set up ungauged basin through s0-s7 (same L0 data sources!)
# 2b. Transfer calibrated MPR parameters
python tools/s10_regionalize/transfer_mpr_params.py \
--calibrated_nml /path/to/gauged_basin/mhm_parameter_calibrated.nml \
--target_dir /path/to/ungauged_basin \
--source_basin "Bengbu" \
--target_basin "Wangjiaba"
# 2c. Run forward simulation with transferred parameters
python tools/s7_execute/run_mhm.py --run_dir /path/to/ungauged_basin
CRITICAL REQUIREMENT: Both calibration and target basins MUST use the SAME L0 data sources (HWSD for soil, GLiM for geology, AVHRR for land cover). The MPR transfer functions map physical properties to model parameters -- if the property definitions change, the transfer functions become invalid.
Why This Works (MPR Theory)
Traditional models calibrate per-cell parameters (e.g., Ksat at each grid cell). These cannot be transferred because they encode both process AND location information.
MPR expresses parameters as transfer functions of measurable properties:
Ksat(cell) = f(sand%, clay%, bulk_density; gamma_1, gamma_2, gamma_3)
The gamma values are the global parameters (~70 total). They encode PROCESS
understanding ("how does sand content relate to hydraulic conductivity?") rather
than location-specific values. When you apply the same gammas to a new basin with
different soil/geology/land cover, the transfer functions automatically produce
locally appropriate Ksat values.
Climate-Zone-Aware Parameter Initialization
The generate_mhm_parameters.py tool (s3) now supports --climate_zone with
four presets: humid_subtropical, semi_arid, cold_alpine, tropical.
These provide literature-informed starting values that are closer to optimal
for each climate, reducing calibration time and improving convergence.
python tools/s3_mpr/generate_mhm_parameters.py \
--config config.json \
--climate_zone humid_subtropical
Critical Domain Knowledge
D8 Flow Direction Convention
mHM uses the standard ArcGIS D8 convention:
32 64 128
16 X 1
8 4 2
- 1=E, 2=SE, 4=S, 8=SW, 16=W, 32=NW, 64=N, 128=NE
- 0 or -9999 = sink/outlet
This is the SAME convention used by GDAL and most GIS tools. WhiteboxTools uses a DIFFERENT convention -- must convert.
Input Format: ESRI ASCII Grid
ALL morphological data must be in ESRI ASCII grid format (.asc):
ncols <N>
nrows <M>
xllcorner <X>
yllcorner <Y>
cellsize <C>
NODATA_value -9999
<data rows>
ALL grids for a domain MUST have identical headers (ncols, nrows, xllcorner, yllcorner, cellsize).
Soil Class Definition Format
Tab-separated, first line: nSoil_Types <N>
Header: MU_GLOBAL HORIZON UD[mm] LD[mm] CLAY[%] SAND[%] BD[gcm-3]
Data: one row per soil type per horizon
Geology Class Definition Format
First line: nGeo_Formations <N>
Header: GeoParam(i) ClassUnit Karstic Description
Karstic: 1=karstic, 0=non-karstic
LAI Class Definition Format
First line: NoLAIclasses <N>
Header: ID LAND-USE Jan. Feb. Mar. ... Dec.
Monthly LAI values per class
TRAP (dt_s12). The shipped land-cover raster is
data/landcover/AVHRR_1km_LANDCOVER_1981_1994.GLOBAL.tifand it uses the UMD 14-class legend, not IGBP (its.vat.dbfglobal counts confirm it: value 11 = 11.8M cells = cropland; value 12 = 87M = bare). Under IGBP, 11 = Permanent Wetlands — so an IGBP lookup relabels all Huai cropland as wetland and replaces the seasonal crop LAI curve (0.0–5.2) with a flat 2–5 year-round. Use--legend umd. A uniform land-cover grid is never a valid result: it silently disables every land-cover-dependent MPR transfer function.
Observed Discharge (Gauge) Format
Five header lines, then six columns — verified against
mhm_src/test_domain/input/gauge/00398.txt:
00398:GAUGE 1 (Abfluss) DAILY
nodata -9999
n 1 measurements per day [1, 1440]
start 1990 01 01 00 00 (YYYY MM DD HH MM)
end 1993 12 31 00 00 (YYYY MM DD HH MM)
1990 01 01 00 00 157.000
TRAP (dt_s13).
mo_read_timeseries.f90skips five header lines and parsesYYYY MM DD HH MM Q. A bare 4-columnYYYY MM DD Qfile makes mHM eat the first data rows as the header and then read discharge out of the DAY column.
Meteorological Forcing Format
Verified against mhm_src/test_domain_2/input/meteo/:
input/meteo/pre/pre.nc + header.txt
input/meteo/tavg/tavg.nc + header.txt
input/meteo/pet/pet.nc + header.txt # processCase(5) = 0
- ONE concatenated file per variable spanning the whole period — not one
file per year. mHM opens
<dir>/<varname>.nc. - The NetCDF variable name equals the directory name (
pre,tavg,pet). - Dimensions
(time, yc, xc); georeferencing comes fromheader.txt. - Write the L2 (forcing) grid identical to the L1 grid — same
xllcorner,yllcorner,cellsizeand shape. This is whattest_domain_2does and it removes all L0→L2cellFactorambiguity. latDESCENDING (N→S);_FillValue = -9999.0, never NaN;timeas int32.
Unit Conversion Table (Silent Error Prevention)
Always load through ki_tools_common.load_forcing.load_daily_forcing — it is
the one validated place where source units are normalised (precip_mm → mm/day,
temp_mean_c → °C). Do not hand-roll the arithmetic in the s4 tool.
| Variable | CMFD 3-hourly | CMFD daily | MSWX | mHM | Note |
|---|---|---|---|---|---|
| Precipitation | kg m⁻² s⁻¹ | kg m⁻² s⁻¹ (daily mean rate) | mm/3hr | mm/day | ×10800 per 3-h step, but ×86400 for the daily product |
| Temperature | K | K | degC | degC | CMFD: −273.15 |
| Wind speed | m/s | m/s | m/s | m/s | Penman-Monteith only |
| Radiation (SW) | W/m² | W/m² | W/m² | W/m² | Penman-Monteith only |
TRAP (dt_s10). The
Data_forcing_01dy_*(daily) CMFD product stores a daily mean rate, not a 3-hourly accumulation. Applying the 3-hourly factor 10800 delivers exactly 1/8 of the true rainfall — a silent dry bias that drives PBIAS to ≈ −95% and that no calibration can undo. Gate on basin-mean precipitation ∈ [200, 4000] mm/yr.
PET: which processCase(5) can this forcing support?
| Method | processCase(5) |
Requires |
|---|---|---|
| PET read from file | 0 | input/meteo/pet/pet.nc |
| Hargreaves-Samani | 1 | tmin/ and tmax/ with a real diurnal range |
| Priestley-Taylor | 2 | net radiation |
| Penman-Monteith | 3 | wind, humidity, radiation, pressure |
TRAP (dt_s11). CMFD daily carries a single temperature field, so
temp_max_c == temp_min_c == temp_mean_c. Hargreaves scales withsqrt(Tmax − Tmin)and is therefore identically zero. Synthesising a fake diurnal range (tavg ± 5 K) does not recover the missing information — it fabricates the quantity the method integrates, and produced a basin-mean PET of 98 mm/day (~20× physical) that evaporated the whole water balance.For temperature-only daily forcing use
--pet_method 0: the s4 tool pre-computes Oudin (2005) PET fromtavg+ latitude and mHM reads it (processCase(5)=0). At Wangjiaba this gives 925 mm/yr, max 6.5 mm/day.convert_forcing_to_mhm.pyrefuses--pet_method 1when the mean diurnal range is < 0.1 K.
Skill Documents
The per-stage procedure documents below are thin operational wrappers around
this SKILL.md, dag.yaml, docs/format_spec.yaml,
docs/validation_convention.yaml, and the validated scripts in tools/.
| ID | Stage | Document | Key Content |
|---|---|---|---|
| sd01 | s0 | docs/s0_config_skill.md |
Resolution selection, PET method, coordinate system |
| sd02 | s1 | docs/s1_domain_skill.md |
True catchment delineation, L0/L1/L11 relationship, grid alignment |
| sd03 | s2 | docs/s2_morphology_skill.md |
ASCII grid format, HWSD/GLiM/AVHRR mapping, gauge grid |
| sd04 | s3 | docs/s3_mpr_skill.md |
Transfer function theory, parameter ranges, climate presets |
| sd05 | s4 | docs/s4_forcing_skill.md |
CMFD/MSWX -> mHM conversion, PET and unit gates |
| sd06 | s5 | docs/s5_gauge_skill.md |
Gauge format and observed-discharge conversion |
| sd07 | s6 | docs/s6_namelist_skill.md |
Namelist reference, process selection, calibration flags |
| sd08 | s7 | docs/s7_execute_skill.md |
Runtime expectations, actual binary execution, error interpretation |
| sd09 | s8 | docs/s8_postprocess_skill.md |
Output structure, performance metrics, validation convention |
| sd10 | s9 | docs/s9_calibration_skill.md |
Calibration setup, optimizer selection, convergence |
| sd11 | s10 | docs/s10_regionalize_skill.md |
Transfer protocol, data-source consistency |
Diagnostic Triplets
See diagnostics/triplets.yaml for the full set. Key categories:
- Grid alignment (dt_r01, dt_r02): ASCII header mismatch, resolution not integer multiple
- Forcing format (dt_r03, dt_r06): Wrong NetCDF variable names, wrong units
- D8 convention (dt_r05): Wrong flow direction encoding = zero discharge
- Soil/geology mapping (dt_s01, dt_r07): Classdefinition format errors, missing classes
- Silent errors (dt_s02-dt_s08): Wrong PET method, wrong aspect, biased calibration
Error Handling
mHM hangs after Initialize domains ... at ~100% CPU with no error (dt_r13):
there is a cycle in the L0 flow-direction grid; mRM's L11 tracer follows fdir
until it reaches an outlet and never terminates. This bites when upscaling a fine
D8 network: a river meandering across a cell boundary yields A→B and B→A.
Guard: accept a downstream neighbour only when its max upstream area is strictly
greater (upa increases monotonically downstream, so the coarse network becomes a
DAG), map fine→coarse with integer arithmetic ((mr - r0)//ratio, never a lon/lat
round-trip — the grids are offset by a fraction of a fine cell), and assert
acyclicity before writing fdir.asc. A cycle checker must treat fdir == 0 as an
outlet, not a cycle.
mHM exits 0 having done nothing. Fortran stop '<msg>' returns status 0.
Two places do this: DDS with nIterations < 6 (dt_r14), and any stop in the
namelist readers. Never trust the return code alone — assert the expected output
file exists (FinalParam.nml, daily_discharge.out).
When mHM produces unexpected results: 0. Check the basin area first (dt_s09) and the forcing totals (dt_s10/dt_s11): basin-mean precipitation ∈ [200, 4000] mm/yr and PET ≲ 8 mm/day. A structural area or unit error looks exactly like a "calibration problem" and is not one.
- Check
ConfigFile.log— look forResolution [m]: 0.orEffective Area: 0.000which indicate wrongiFlag_cordinate_sys - Verify ALL ASCII grids have identical headers: use
validate_morph_grids.py - Check forcing NetCDF variable names match mhm.nml expectations
- If discharge is zero: Check THREE things in order:
a.
iFlag_cordinate_sys— must be1for lat/lon grids (this is the #1 cause of zero discharge) b. Gauge-to-L11 mapping — the gauge L0 cell may map to an L11 cell with no upstream drainage; usemove_gauge.pyafter an initial run to place the gauge at the max-Q L11 cell c. D8 flow direction convention — must be ArcGIS (1=E, 2=SE, ... 128=NE) - If discharge is wildly wrong: check precipitation units (mm/day, not mm/3hr)
- If baseflow is zero: check geology class mapping (all mapped to one class?)
- Gauge placement after L0-to-L11 upscaling: The highest-facc cell at L0 may NOT map to the highest-Q cell at L11 due to resolution upscaling. After the first run, read
mRM_Fluxes_States.ncto find the L11 cell with max Qrouted, then place the gauge at the max-facc L0 cell within that L11 cell.
Coupling with HydroCraft
Input Coupling (from HydroCraft global databases)
- DEM: China DEM 90m / Copernicus GLO-30 ->
dem.asc - Soil: HWSD global raster + MDB ->
soil_class.asc+soil_classdefinition.txt - Geology: GLiM lithology ->
geology_class.asc+geology_classdefinition.txt - Land cover: AVHRR 1km ->
lc_YYYY.asc - Forcing: CMFD (China) or MSWX (global) -> NetCDF per variable
Output Coupling (to downstream models)
- Total runoff -> CaMa-Flood (flood inundation)
- Soil moisture -> DSSAT/WOFOST (crop models)
- Actual ET -> MODIS validation
- Baseflow -> MODFLOW (groundwater recharge)