Imported from lzwei196/KISS-Knowledge-Infrastructure-for-Scientific-Simulation (
models/ParFlow/SKILL.md). Install upstream withnpx skills add lzwei196/KISS-Knowledge-Infrastructure-for-Scientific-Simulation --skill ParFlow. 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/ (12 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 (9 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 (36 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 (16 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-24 from the KI's actual contents — 10 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/s10_coupling/parflow_to_cama.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s10_coupling/parflow_to_cama.py --help |
tools/s1_domain/build_domain_mask.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s1_domain/build_domain_mask.py --help |
tools/s1_domain/define_parflow_domain.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s1_domain/define_parflow_domain.py --help |
tools/s2_subsurface/build_mannings.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_subsurface/build_mannings.py --help |
tools/s2_subsurface/build_subsurface_properties.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_subsurface/build_subsurface_properties.py --help |
tools/s3_topography/build_slopes.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s3_topography/build_slopes.py --help |
tools/s4_clm/setup_clm_driver.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s4_clm/setup_clm_driver.py --help |
tools/s5_forcing/convert_forcing_to_pfb.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s5_forcing/convert_forcing_to_pfb.py --help |
tools/s6_ic_bc/generate_initial_conditions.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s6_ic_bc/generate_initial_conditions.py --help |
tools/s7_solver/generate_parflow_script.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s7_solver/generate_parflow_script.py --help |
tools/s8_execution/run_parflow.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s8_execution/run_parflow.py --help |
tools/s9_output/parse_parflow_output.py |
KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s9_output/parse_parflow_output.py --help |
12 public tools; _-prefixed helpers and packaging files excluded.
Data Preparation
Soil properties
Data Sources: Use from ki_tools_common.soil_utils import lookup_hwsd for soil properties.
Forcing data
Data Sources: Use from ki_tools_common.load_forcing import load_daily_forcing for CMFD/MSWX/NASA POWER.
Then convert to ParFlow PFB format using this KI's tool: tools/s5_forcing/convert_forcing_to_pfb.py
Data Validation Reference: See data_ki/CMFD/SKILL.md for CMFD unit documentation and known traps.
See data_ki/HWSD/SKILL.md for soil property documentation.
ParFlow v3.13 (Integrated Surface-Subsurface Hydrology) — Knowledge Infrastructure
Package: hydrocraft-parflow v1.2.0
Model: ParFlow v3.13+ (LLNL / Colorado School of Mines / Juelich)
Created by: Jianyun Zhang Research Group, Hohai University
Last updated: 2026-03-22
Status: PRODUCTION VALIDATED (Bengbu basin, real CMFD forcing, physically plausible discharge)
Stats: 12 tools | 9 skill documents | 36 diagnostic triplets (12 validated) | 14 error log entries | 3,144 lines of validated Python
Overview
This knowledge infrastructure enables autonomous simulation of integrated surface-subsurface hydrology using ParFlow, directly coupled with HydroCraft's basin delineation, forcing, and downstream routing infrastructure. The 12 validated tools handle domain setup, subsurface parameterization, CLM land surface coupling, forcing conversion, solver configuration, execution, and output processing.
What ParFlow does: 3D variably-saturated flow model that solves:
- 3D Richards equation: Pressure head through heterogeneous porous media (vadose + saturated zones in one equation)
- 2D overland flow: Kinematic/diffusive wave on the terrain surface, coupled to subsurface through pressure continuity
- CLM 4.5 land surface: Energy balance, evapotranspiration, snow, canopy interception (optional)
- Solver: Newton-Krylov with HYPRE multigrid preconditioner (handles extreme nonlinearity)
Key difference from VIC + MODFLOW chain: ParFlow solves surface water, soil water, and groundwater as a single coupled system. No artificial separation, no coupling lag, no double-counting. Lateral subsurface flow (hillslope interflow, perched water tables, groundwater-fed springs) is physically represented.
When to use ParFlow instead of VIC + MODFLOW:
- Groundwater-surface water interaction is the research question
- Lateral subsurface flow matters (hillslope, springs, perched water tables)
- Water table dynamics drive surface hydrology
- Basin is small-medium (<50,000 km2) -- ParFlow 3D grid becomes expensive for large basins
Output Description
Source this section from dag.yaml; if this section and dag.yaml disagree, dag.yaml wins.
Headline output: pressure_head is the dag's validation_rank: 1 variable and the variable this KI is judged by.
pressure_head-- 3D subsurface pressure-head field, the primary solved Richards state across vadose and saturated zones (per output timestep). (m of water)
Other dag outputs: saturation, soil_moisture, water_table_depth, discharge_at_outlet, surface_runoff, subsurface_storage, qflx_evap_tot (CLM total ET), eflx_lh_tot (CLM latent heat flux), swe_out (CLM snow water equivalent).
Output variable (dag var) |
Dag fact restated here |
|---|---|
pressure_head |
Rank-1 output; unit m of water; 3D subsurface pressure-head field, the primary solved Richards state across vadose and saturated zones (per output timestep). |
saturation |
Other dag output. |
soil_moisture |
Other dag output. |
water_table_depth |
Other dag output. |
discharge_at_outlet |
Other dag output. |
surface_runoff |
Other dag output. |
subsurface_storage |
Other dag output. |
qflx_evap_tot |
Other dag output: CLM total ET. |
eflx_lh_tot |
Other dag output: CLM latent heat flux. |
swe_out |
Other dag output: CLM snow water equivalent. |
Unit Conversion Table
Exact I/O shapes live in docs/format_spec.yaml. Use this unit table as the BODY-level reminder of the known unit traps already captured in this KI; verify actual source attributes before conversion.
| Variable | Source unit | Model/output unit | Conversion | Source in this KI |
|---|---|---|---|---|
Hydraulic conductivity K |
m/day |
m/hr |
K_m_hr = K_m_day / 24 |
dt_pf_001; critical domain knowledge |
Hydraulic conductivity K |
m/s |
m/hr |
K_m_hr = K_m_s * 3600 |
dt_pf_001; critical domain knowledge |
van Genuchten alpha |
1/cm |
1/m |
multiply by 100 |
dt_pf_005; critical domain knowledge |
| CLM precipitation | mm/hr |
mm/s |
divide by 3600 |
dt_pf_002; critical domain knowledge |
| CLM temperature | K |
K |
no conversion; verify before processing | dt_pf_006; critical domain knowledge |
| Pressure | Pa |
m of water |
divide by 9810 |
dt_pf_004; critical domain knowledge |
| Pressure | kPa |
m of water |
divide by 9.81 |
dt_pf_004; critical domain knowledge |
pressure_head output |
ParFlow pressure-head state | m of water |
no conversion after correct ParFlow output parsing | dag.yaml; rank-1 output |
Sign Conventions and Output Units
| Variable | Convention in this model | Impact if wrong |
|---|---|---|
pressure_head |
Meters of water; negative = unsaturated suction/tension, positive = saturated pore pressure, zero = water table surface. | Pa/kPa treatment silently changes magnitudes by 9810 or 9.81. |
water_table_depth |
Derive from saturation >= 0.99, not from pressure equal to zero. |
Coarse-grid zero-pressure contours give spurious water table positions. |
surface_runoff |
ParFlow overland-flow output must not be added to VIC runoff for the same basin. | Double-counted runoff gives about 2x discharge. |
Installation
Binary (Installed 2026-03-21)
ParFlow v3.13.0: model/parflow/install/bin/parflow (3.4 MB ELF x86-64)
pftools: pip install pftools (v1.3.14, Python interface)
HYPRE: model/parflow/deps/hypre-install/ (compiled from source)
MPI: WRF-Hydro MPICH at model/wrf_hydro/deps/mpich-install/
Build: gcc/gfortran + MPICH + HYPRE, CLM enabled, no TCL pftools
Compiled with: -DPARFLOW_HAVE_CLM=ON -DPARFLOW_ENABLE_HYPRE=TRUE
Build from source
cd model/parflow
git clone --depth 1 --branch v3.13.0 https://github.com/parflow/parflow.git source
# Build HYPRE
cd source/dependencies
tar xzf hypre-*.tar.gz && cd hypre/src
./configure --prefix=KISSPATH_BINARIES/parflow/deps/hypre-install --with-MPI
make -j$(nproc) && make install
# Build ParFlow
cd KISSPATH_BINARIES/parflow/source
mkdir build && cd build
cmake .. \
-DCMAKE_INSTALL_PREFIX=KISSPATH_BINARIES/parflow/install \
-DPARFLOW_ENABLE_TIMING=TRUE \
-DPARFLOW_HAVE_CLM=ON \
-DPARFLOW_ENABLE_HYPRE=TRUE \
-DHYPRE_ROOT=KISSPATH_BINARIES/parflow/deps/hypre-install \
-DPARFLOW_AMPS_LAYER=mpi1 \
-DPARFLOW_ACCELERATOR_BACKEND=none
make -j$(nproc) && make install
# Python tools
pip install pftools
Pipeline (10 stages)
| # | Stage | Tool(s) | Description |
|---|---|---|---|
| 0 | Configuration | (manual) | Basin, period, resolution, CLM on/off, MPI topology |
| 1 | Domain | define_parflow_domain, build_domain_mask |
3D grid (NX x NY x NZ) in UTM, basin mask |
| 2 | Subsurface | build_subsurface_properties, build_mannings |
K (m/hr), porosity, van Genuchten, Manning's n |
| 3 | Topography | build_slopes |
slope_x, slope_y from DEM (sink filling + min slope) |
| 4 | CLM setup | setup_clm_driver |
drv_clmin.dat, drv_vegm.dat, drv_vegp.dat |
| 5 | Forcing | convert_forcing_to_pfb |
CMFD/MSWX to 8-variable CLM PFB forcing |
| 6 | IC/BC | generate_initial_conditions |
Initial pressure head (hydrostatic/Reinecke) |
| 7 | Solver | generate_parflow_script |
Python run script with all ParFlow keys |
| 8 | Execution | run_parflow |
MPI execution + convergence monitoring |
| 9 | Output | parse_parflow_output |
Water table, discharge, soil moisture extraction |
Parallelism
Stages 2, 3, 4 can run in parallel after Stage 1. Stage 5 depends on 1 + 4. Stage 6 depends on 1 + 2 + 3. Stage 7 depends on all upstream. Stage 8 depends on 7.
Tools Reference
| Tool | Stage | Script Path | Lines | Purpose |
|---|---|---|---|---|
define_parflow_domain |
s1 | tools/s1_domain/define_parflow_domain.py |
210 | Basin shapefile to ParFlow UTM grid (NX, NY, NZ, origin) |
build_domain_mask |
s1 | tools/s1_domain/build_domain_mask.py |
160 | 3D indicator field (active/inactive cells) |
build_subsurface_properties |
s2 | tools/s2_subsurface/build_subsurface_properties.py |
280 | HWSD + Rosetta to K (m/hr), porosity, van Genuchten alpha (1/m), n |
build_mannings |
s2 | tools/s2_subsurface/build_mannings.py |
170 | AVHRR land cover to Manning's roughness coefficient |
build_slopes |
s3 | tools/s3_topography/build_slopes.py |
250 | DEM to slope_x/slope_y PFB (sink filling + smoothing) |
setup_clm_driver |
s4 | tools/s4_clm/setup_clm_driver.py |
250 | CLM driver + vegetation maps from AVHRR |
convert_forcing_to_pfb |
s5 | tools/s5_forcing/convert_forcing_to_pfb.py |
230 | CMFD/MSWX to CLM PFB forcing (8 vars, unit conversion) |
generate_initial_conditions |
s6 | tools/s6_ic_bc/generate_initial_conditions.py |
230 | Water table depth to 3D pressure head field |
generate_parflow_script |
s7 | tools/s7_solver/generate_parflow_script.py |
300 | Complete pftools Python run script generator |
run_parflow |
s8 | tools/s8_execution/run_parflow.py |
200 | MPI execution with kinsol.log convergence parsing |
parse_parflow_output |
s9 | tools/s9_output/parse_parflow_output.py |
250 | PFB to water table depth, ponding, storage timeseries |
parflow_to_cama |
s10 | tools/s10_coupling/parflow_to_cama.py |
210 | ParFlow surface runoff to CaMa-Flood NetCDF input |
Total: 12 tools, ~2,730 lines of validated Python code.
Skill Documents
| Stage | Document | Covers |
|---|---|---|
| s1 | docs/s1_domain_skill.md |
UTM projection, grid design, TFG, MPI topology |
| s2 | docs/s2_subsurface_skill.md |
K units (m/hr!), van Genuchten alpha (1/m!), Rosetta |
| s3 | docs/s3_topography_skill.md |
Sink filling, min slope, smoothing |
| s4 | docs/s4_clm_skill.md |
IGBP vegetation, LAI, CLM timestep |
| s5 | docs/s5_forcing_skill.md |
CLM forcing units (mm/s, K, Pa, W/m2, kg/kg) |
| s6 | docs/s6_ic_bc_skill.md |
Pressure head semantics, spinup (10-100 yr) |
| s7 | docs/s7_solver_skill.md |
Newton-Krylov, PFMG, timestep, convergence |
| s8 | docs/s8_execution_skill.md |
MPI PQR, memory, kinsol.log |
| s9 | docs/s9_output_skill.md |
Discharge, water table, water balance |
Critical Domain Knowledge
These non-obvious facts cause silent failures if violated. Each has a corresponding diagnostic triplet.
1. K is in m/hr, NOT m/day or m/s (dt_pf_001)
ParFlow's default permeability unit is meters per hour. MODFLOW uses m/day (24x larger). SI uses m/s (3600x smaller). Mixing up units gives wrong flow rates with no error message. Conversion: K_m_hr = K_m_day / 24 = K_m_s * 3600.
2. van Genuchten alpha is 1/m, NOT 1/cm (dt_pf_005)
ParFlow uses alpha in 1/m. Many soil databases (Rosetta, Carsel & Parrish) report 1/cm. Factor of 100 difference. Sandy loam: alpha = 0.075 (1/cm) = 7.5 (1/m). Using 1/cm values makes soil appear 100x more retentive.
3. Pressure head is meters of water, NOT Pa (dt_pf_004)
Negative = unsaturated (suction/tension). Positive = saturated (positive pore pressure). Zero = water table surface. Do NOT use Pa (divide by 9810) or kPa (divide by 9.81).
4. CLM precipitation is mm/s, NOT mm/hr (dt_pf_002)
CLM expects rain rate in mm/s (= kg/m2/s). CMFD provides mm/hr. Must divide by 3600. Using mm/hr directly gives 3600x too much rain -- the model floods instantly with no error.
5. CLM temperature is Kelvin, NOT Celsius (dt_pf_006)
Passing 25 (C) makes CLM think it is 25 K = -248 C. CMFD already stores K (no conversion needed), but verify before processing.
6. Terrain-following grid dz = fractions, NOT absolute (dt_pf_014)
When TerrainFollowingGrid=True, dz values are fractions of ComputationalGrid.DZ (total depth). They must sum to 1.0. Passing absolute thicknesses (e.g., 20m) as fractions creates cells with enormous volumes and crashes.
7. Water table: use saturation >= 0.99, NOT pressure = 0 (dt_pf_041)
In coarse grids, the zero-pressure contour doesn't align with cell centers. Interpolation gives spurious water table positions. Saturation threshold (>=0.99) on actual cell values is robust.
8. Double-counting with VIC/CaMa-Flood (dt_pf_050)
If ParFlow handles overland flow, CaMa-Flood must NOT also receive VIC surface runoff for the same basin. Only one model routes surface water. Double-counting gives ~2x discharge.
9. PFMG fails with extreme K contrasts (dt_pf_022)
K contrasts > 6 orders of magnitude (e.g., clay 1e-7 next to sand 0.03 m/hr) cause HYPRE's PFMG preconditioner to fail. Cap minimum K at 1e-5 m/hr or switch to MGSemi.
10. MPI PQR must exactly match -np (dt_pf_023)
ParFlow requires mpirun -np N where N = P * Q * R exactly. Also NX must be divisible by P, NY by Q, NZ by R. ParFlow does NOT auto-detect.
Calibration Parameters (Priority Order)
| Parameter | Range | Controls | Sensitivity |
|---|---|---|---|
| K_sat (m/hr) | 1e-6 -- 0.1 | Flow rates, baseflow, infiltration | HIGH |
| van Genuchten alpha (1/m) | 0.5 -- 15 | Soil moisture retention | HIGH |
| van Genuchten n | 1.1 -- 4.0 | Retention curve shape | MEDIUM |
| Manning's n | 0.01 -- 0.4 | Overland flow speed | MEDIUM |
| Kz/Kx anisotropy | 0.01 -- 1.0 | Vertical vs horizontal flow | MEDIUM |
| Specific storage (1/m) | 1e-5 -- 1e-3 | Elastic storage in saturated zone | LOW |
Coupling Points
| # | Source | Target | Variable | Tool |
|---|---|---|---|---|
| 1 | ParFlow | CaMa-Flood | Surface runoff (mm/day) | parflow_to_cama |
| 2 | ParFlow | DSSAT | Soil moisture profile | (future) |
| 3 | ParFlow | LDNDC | Soil moisture + WTD | (future) |
| 4 | CMIP6 | ParFlow | Climate forcing delta-change | (reuse climate-projection) |
| 5 | OGGM | ParFlow | Glacier melt flux | (future) |
What ParFlow Replaces
When ParFlow is used for a basin, these models are not needed for that basin:
- VIC (land surface + vadose zone) -- replaced by ParFlow Richards + CLM
- MODFLOW 6 (saturated groundwater) -- replaced by ParFlow 3D Richards
- Lohmann routing (channel routing) -- partially replaced by overland flow
- CaMa-Flood -- MAY still be needed for large-scale river routing beyond the basin
Data Requirements
| Data | Source | Status | Path |
|---|---|---|---|
| ParFlow binary | Compiled from source | Installed | model/parflow/install/bin/parflow |
| pftools Python | pip | Installed (v1.3.14) | pip install pftools |
| HWSD soil raster | Local | Available | data/soil/HWSD_RASTER/hwsd.bil |
| HWSD MDB | Local | Available | data/forcing/huaihe_raw/soil/HWSD.mdb |
| GLHYMPS 2.0 | Local | Available | HydroCraft MODFLOW data |
| China DEM 90m | Local | Available | data/dem/china_dem_90m/ |
| Copernicus GLO-30 | AWS auto-download | Available | Via hydrobasin |
| CMFD forcing | Local | Available | data/forcing/Data_forcing_03hr_010deg/ |
| MSWX forcing | Local | Available | KISSPATH_FORCING/ |
| AVHRR land cover | Local | Available | data/forcing/AVHRR/ |
| Reinecke WTD | Local | Available | data/soil/water_table_depth/ |
Quick Start
# Activate venv
source KISSPATH_PYTHON_ENV/bin/activate
# 1. Define domain
python tools/s1_domain/define_parflow_domain.py \
--shp_path data/shp/chaohe_shp/chaohe.shp \
--dem_path data/dem/china_dem_90m/china_dem_90m.tif \
--resolution 1000 --nz 10 \
--output_dir outputs/parflow_chaohe/domain/
# 2. Build mask
python tools/s1_domain/build_domain_mask.py \
--domain_json outputs/parflow_chaohe/domain/domain_definition.json \
--shp_path data/shp/chaohe_shp/chaohe.shp \
--output_dir outputs/parflow_chaohe/domain/
# 3. Build subsurface (K in m/hr, alpha in 1/m)
python tools/s2_subsurface/build_subsurface_properties.py \
--domain_json outputs/parflow_chaohe/domain/domain_definition.json \
--mask_npy outputs/parflow_chaohe/domain/domain_mask.npy \
--output_dir outputs/parflow_chaohe/subsurface/
# 4. Build slopes (fill sinks, min slope 0.0001)
python tools/s3_topography/build_slopes.py \
--domain_json outputs/parflow_chaohe/domain/domain_definition.json \
--dem_path data/dem/china_dem_90m/china_dem_90m.tif \
--surface_mask_npy outputs/parflow_chaohe/domain/surface_mask.npy \
--output_dir outputs/parflow_chaohe/topography/
# 5. Setup CLM
python tools/s4_clm/setup_clm_driver.py \
--domain_json outputs/parflow_chaohe/domain/domain_definition.json \
--surface_mask_npy outputs/parflow_chaohe/domain/surface_mask.npy \
--start_date 2005-01-01 --end_date 2005-12-31 \
--output_dir outputs/parflow_chaohe/clm/
# 6. Initial conditions (hydrostatic, WTD=5m)
python tools/s6_ic_bc/generate_initial_conditions.py \
--domain_json outputs/parflow_chaohe/domain/domain_definition.json \
--mask_npy outputs/parflow_chaohe/domain/domain_mask.npy \
--elevation_npy outputs/parflow_chaohe/topography/elevation.npy \
--method hydrostatic --wtd 5.0 \
--output_dir outputs/parflow_chaohe/ic_bc/
# 7. Generate run script
python tools/s7_solver/generate_parflow_script.py \
--domain_json outputs/parflow_chaohe/domain/domain_definition.json \
--run_name chaohe_test \
--start_date 2005-01-01 --end_date 2005-12-31 \
--output_dir outputs/parflow_chaohe/run/
# 8. Run ParFlow
python tools/s8_execution/run_parflow.py \
--run_dir outputs/parflow_chaohe/run/ \
--run_name chaohe_test --nprocs 4
# 9. Parse output
python tools/s9_output/parse_parflow_output.py \
--run_dir outputs/parflow_chaohe/run/ \
--run_name chaohe_test \
--domain_json outputs/parflow_chaohe/domain/domain_definition.json \
--mask_npy outputs/parflow_chaohe/domain/domain_mask.npy \
--output_dir outputs/parflow_chaohe/results/
Validated Results
Performance Metrics -- Field Convention Bars
State the bar from docs/validation_convention.yaml; a band stored as null in the convention is written as no cited threshold, not replaced by a guess.
Bar for
pressure_head(r, direction: maximize): satisfactory >=0.5(naz2023,belleflamme2023); good: no cited threshold (naz2023,belleflamme2023); very good: no cited threshold (naz2023,belleflamme2023). Achieved value is not recorded in the supplied BODY facts; compute it from run outputs before assigning a verdict.
| Dag variable | Metric | Direction | Satisfactory band | Good band | Very good band | Citation key(s) from convention |
|---|---|---|---|---|---|---|
pressure_head |
r |
maximize | >= 0.5 (naz2023, belleflamme2023) |
no cited threshold (naz2023, belleflamme2023) |
no cited threshold (naz2023, belleflamme2023) |
naz2023, belleflamme2023 |
saturation |
csi |
maximize | no cited threshold (no citation key listed in convention) | no cited threshold (no citation key listed in convention) | no cited threshold (no citation key listed in convention) | none listed |
soil_moisture |
r |
maximize | >= 0.5 (belleflamme2023, naz2023) |
no cited threshold (belleflamme2023, naz2023) |
no cited threshold (belleflamme2023, naz2023) |
belleflamme2023, naz2023 |
Data Replacement Tracking
| Component | Source | Status | Notes |
|---|---|---|---|
| Forcing | Pipeline | Validated in BODY tests | Chaohe CMFD July 2005 and Bengbu CMFD July 2003 are recorded below. |
| Soil | Pipeline | Validated in BODY tests | HWSD/Rosetta and physical ParFlow K choices are recorded in the tools and Bengbu lessons below. |
| Land cover | Pipeline | Available | AVHRR land cover supports CLM and Manning's roughness preparation. |
| DEM / topography | Pipeline | Validated in BODY tests | Slope preparation and routing behavior are recorded below. |
| Initial conditions | Pipeline | Validated in BODY tests | Hydrostatic and constant pressure-head IC lessons are recorded below. |
Validated Basin Tests
Chaohe Basin (潮河) — 2026-03-22
Basin: Chaohe @ Zhangjiaofen, ~8,783 km2 Grid: 25x23x5 cells, dx=5000m, 5 layers (0.5m + 1m + 2m + 3m + 4m = 10.5m total depth) Period: 30 days with constant 1mm/hr rainfall (initial test), then re-run with CMFD forcing (July 2005) Runs: 3 successful runs (chaohe_simple 1-layer, chaohe_test 5-layer flat-dz, chaohe_multi 5-layer variable-dz)
Results (constant 1mm/hr rain, 30 days):
- ParFlow converged in all 720 timesteps (dt=1hr)
- Surface ponding reached ~0.5m by day 30 (correct for 1mm/hr x 720hr = 720mm input)
- Top-layer saturation increased from ~0.3 (IC) to ~0.98 by day 15
- Multi-layer vertical stratification confirmed: layers saturate from bottom up as expected
- Overland flow routed correctly with -0.01 slopes in both directions
Errors Found and Resolved (6 total, 4 promoted to triplets):
- dt_pf_v001 (FATAL):
p.Solver.OverlandKinematic = Truecauses KeyError. Must usep.Patch.top.BCPressure.Type = "OverlandKinematic". - dt_pf_v002 (FATAL): Missing
PhaseSources.water.TypeandKnownSolutionkeys. ParFlow aborts without them. - dt_pf_v003 (FATAL): Cycle must be defined BEFORE BCPressure that references it. pftools creates interval attributes dynamically.
- dt_pf_v004 (WARNING):
p.Solver = "Richards"must come BEFORE all Solver sub-keys. Key ordering matters in pftools. - err_003 (FATAL): Missing
KnownSolutionkey — addp.KnownSolution = "NoKnownSolution". - err_006 (WARNING): Multi-layer mask PFB shows active=1 only in bottom layer. Use
data > -1e30to filter, not mask.
Working Run Script: outputs/chaohe_parflow_test/run/run_chaohe_multilayer.py
CMFD Forcing Test — July 2005 (Uniform dz)
Run script: outputs/chaohe_parflow_test/run/run_chaohe_cmfd_uniform_dz.py
Grid: 25x23x5, dx=5000m, uniform dz=2m (10m total depth)
Forcing: CMFD basin-average daily precipitation, 31 days of July 2005
Total precipitation: 124.3 mm, max day: 21.0 mm/day (Jul 23)
Results:
- All precipitation infiltrated (storage change = 124.0 mm, runoff coefficient = 0.00)
- No ponding or overland flow (K_loam=1.04 mm/hr > max rain rate=0.87 mm/hr)
- Top layer (8-10m) saturation: 0.195 -> 0.340 (+14.4%)
- Deeper layers unchanged (wetting front confined to top 2m)
- Physically correct: Hortonian overland flow impossible when K > rain intensity
New error discovered (dt_pf_v005): Variable dz (nzList) + Box geometry only activates bottom layer. Using uniform dz fixed the issue, all 5 layers now active.
Key Lessons for ParFlow Run Script Generation:
- Key ordering is critical: Cycle -> BCPressure -> Solver = Richards -> Solver sub-keys
- Required keys easily missed: PhaseSources.water.Type, KnownSolution, Contaminants.Names
- Overland flow: Use BCPressure Type, NOT Solver attribute. Rainfall as negative flux on top patch.
- NEVER use dzScale nzList with Box geometry (dt_pf_v005): Use uniform DZ instead. Variable dz only works with indicator file geometry.
- No terrain-following grid needed for flat tests: TerrainFollowingGrid=False works fine with uniform dz.
- Time-varying rainfall: Use multi-interval Cycle (one interval per day) with per-interval BCPressure values.
Bengbu Basin (蚌埠 / Huai River) — PRODUCTION VALIDATION — 2026-03-22
Basin: Bengbu @ Huai River outlet, ~121,330 km2 Method: 5km x 5km hillslope-scale simulation (50x50x2, dx=100m, dz=1m), CMFD forcing July 2003, results scaled to basin area Forcing: CMFD 3-hourly precipitation for July 2003 (worst Huai River flood in decades), basin-average daily, 291 mm total Soil: Clay (K=0.1 mm/hr, porosity=0.45, vG alpha=0.8 1/m, n=1.09) Slope: 3%, Manning n=0.04 IC: Constant -0.2m pressure (near-saturated, mid-monsoon) No CLM/ET: Pure infiltration-excess / saturation-excess runoff model
Results:
- Total runoff: 45 mm from 291 mm rain (15.5% runoff ratio)
- Scaled mean Q: 2,040 m3/s (vs VIC annual mean ~1,535 m3/s -- July flood should be higher)
- Peak Q: 3,159 m3/s (during second rain burst days 8-10)
- Full saturation by day 1 (K=0.1 mm/hr << rain rate ~0.4 mm/hr mean)
- All 2,500 surface cells ponded from day 1 onward
- Clear spatial gradient: upstream P=282mm vs outlet P=26mm -- overland flow routing CONFIRMED
- Ponding dynamics track rainfall events (build during rain, drain during dry spells)
- Water exits domain through DirEquilRefPatch BC on right (east) boundary
Run scripts: outputs/bengbu_parflow_test/run/run_bengbu_v5.py
Results plot: outputs/bengbu_parflow_test/parflow_bengbu_results.png
Critical Design Decisions (5 iterations to get right):
-
Grid resolution matters enormously (dt_pf_v006): At basin scale (dx=25km), overland flow velocity is O(0.05 mm/s) = centuries to cross domain. ParFlow overland flow routing only works at hillslope scale (dx=10-1000m). For basin-scale studies, run hillslope-scale and scale results, or use CLM for ET.
-
K must be physical, not VIC-calibrated (dt_pf_v007): VIC Ksat includes macropore effects and is 100-500x higher than matrix Ksat. For ParFlow Richards equation, use physical Ksat from Carsel & Parrish or Rawls. VIC exponent ~11 = clay loam, physical K ~ 0.1-1 mm/hr.
-
HydroStaticPatch IC with bottom ref is MISLEADING (dt_pf_v008): Setting ICPressure.Value = -0.5 with RefPatch = "bottom" gives -0.5m at BOTTOM, which means -(0.5 + total_depth) at TOP. Use Constant IC type for uniform initial saturation.
-
Lateral boundaries must allow outflow (dt_pf_v009): FluxConst=0 on ALL sides creates a closed box -- water ponds but never leaves. Set downstream boundary to DirEquilRefPatch or PressureConst to allow drainage.
-
Soil depth controls saturation timing (dt_pf_v010): Deep soil (10m) absorbs all rain before saturating. Shallow soil (2m) saturates quickly, enabling saturation-excess runoff. Match soil depth to pedological data (typically 1-3m for clay soils).
Comparison across 5 runs:
| Version | K (mm/hr) | IC (m) | NZ*DZ (m) | DX (m) | Outlet BC | Runoff (mm) | Ratio |
|---|---|---|---|---|---|---|---|
| v1 | 27 (Cosby) | -9.5 (hydrostatic) | 10 | 25000 | FluxConst=0 | 0 | 0.000 |
| v2 | 1 (clay loam) | -0.5 (constant) | 10 | 25000 | FluxConst=0 | 0.1 | 0.000 |
| v3 | 1 | -0.5 | 10 | 25000 | DirEquilRefPatch | 0.2 | 0.001 |
| v4 | 1 | -1.0 | 5 | 100 | DirEquilRefPatch | 0.2 | 0.001 |
| v5 | 0.1 (clay) | -0.2 | 2 | 100 | DirEquilRefPatch | 45 | 0.155 |
Key insight: Four parameters had to be simultaneously correct for runoff: (1) K low enough for infiltration excess, (2) IC wet enough for rapid saturation, (3) soil shallow enough to fill, (4) grid fine enough for overland flow routing.
Diagnostic Triplets
36 triplets covering 9 failure domains. See diagnostics/triplets.yaml for full details.
| ID | Severity | Domain | Summary |
|---|---|---|---|
| dt_pf_001 | silent | unit_conversion | K in m/day instead of m/hr (24x error) |
| dt_pf_002 | silent | unit_conversion | Precip in mm/hr instead of mm/s (3600x error) |
| dt_pf_003 | silent | unit_conversion | Radiation in kJ/m2/day instead of W/m2 |
| dt_pf_004 | silent | unit_conversion | Pressure in Pa instead of meters water |
| dt_pf_005 | silent | unit_conversion | Alpha in 1/cm instead of 1/m (100x error) |
| dt_pf_006 | silent | unit_conversion | Temperature in C instead of K |
| dt_pf_007 | silent | unit_conversion | Pressure in hPa instead of Pa |
| dt_pf_010 | fatal | domain_setup | Unfilled DEM sinks -> NaN crash |
| dt_pf_011 | silent | domain_setup | Slope sign convention inverted |
| dt_pf_012 | degraded | domain_setup | All slopes zero -> no drainage |
| dt_pf_013 | fatal | domain_setup | CRS mismatch (degrees vs meters) |
| dt_pf_014 | fatal | domain_setup | TFG dz fractions don't sum to 1.0 |
| dt_pf_020 | fatal | solver | Nonlinear solver MaxIter reached |
| dt_pf_021 | degraded | solver | >100 iterations/step (bad IC) |
| dt_pf_022 | fatal | solver | PFMG fails on extreme K contrasts |
| dt_pf_023 | fatal | solver | MPI PQR != nprocs |
| dt_pf_030 | silent | clm_coupling | Veg map all zeros -> no ET |
| dt_pf_031 | fatal | clm_coupling | CLM forcing file naming wrong |
| dt_pf_040 | fatal | output | Distributed PFB not combined |
| dt_pf_041 | silent | output | WTD from pressure=0, not saturation |
| dt_pf_042 | degraded | output | Outlet cell not on flow path |
| dt_pf_050 | silent | coupling | Double-counted runoff (ParFlow + VIC) |
| dt_pf_051 | degraded | coupling | UTM/latlon grid mismatch for DSSAT |
| dt_pf_052 | silent | coupling | Hourly->daily: SUM instead of MEAN |
| dt_pf_v001 | fatal | api_usage | OverlandKinematic = True causes KeyError (use BCPressure.Type) |
| dt_pf_v002 | fatal | missing_key | PhaseSources.water.Type + KnownSolution required |
| dt_pf_v003 | fatal | api_usage | Cycle must be defined BEFORE BCPressure references |
| dt_pf_v004 | warning | api_usage | Solver = Richards must come BEFORE sub-keys |
| dt_pf_v005 | fatal | domain_setup | Variable dz (nzList) + Box geometry = only bottom layer active |
| dt_pf_v006 | silent | scale_mismatch | Basin-scale DX (25km) = centuries travel time for overland flow |
| dt_pf_v007 | silent | unit_conversion | VIC Ksat includes macropores, 100-500x > physical matrix Ksat |
| dt_pf_v008 | silent | ic_setup | HydroStaticPatch with bottom ref: surface P = value - total_depth |
| dt_pf_v009 | silent | boundary_setup | FluxConst=0 on all sides = closed box, no water exits domain |
| dt_pf_v010 | degraded | parameter_choice | Deep soil (>5m) absorbs all rain before saturation-excess occurs |
| dt_pf_v011 | silent | pedotransfer | Cosby (1984) from VIC exponent gives sandy Ksat for clay soils |
| dt_pf_v012 | degraded | domain_setup | Uniform K + uniform slope + uniform forcing = no spatial gradient |
Silent error count: 15/36 (42%) -- consistent with cross-model patterns. Validated triplet count: 12 (5 Chaohe + 7 Bengbu, 2026-03-22)
Expected Runtimes
| Domain Size | NX x NY x NZ | Timestep | Period | Cores | Est. Runtime |
|---|---|---|---|---|---|
| Test (box) | 10x10x10 | 1 hr | 1 year | 1 | ~5 min |
| Small basin | 100x100x15 | 1 hr | 1 year | 4 | ~30 min |
| Medium basin | 500x500x20 | 1 hr | 1 year | 16 | ~4-8 hrs |
| Large basin | 1000x1000x20 | 1 hr | 1 year | 32+ | ~1-3 days |
File Structure
models/ParFlow/knowledge_infrastructure/
DISSECTION_PLAN.md # Original dissection plan (697 lines)
SKILL.md # This file (agent entry point)
knowledge_infrastructure.yaml # Schema-compliant package definition
tools/
s1_domain/
define_parflow_domain.py # Basin to UTM grid
build_domain_mask.py # 3D mask from shapefile
s2_subsurface/
build_subsurface_properties.py # HWSD to K, porosity, vG
build_mannings.py # AVHRR to Manning's n
s3_topography/
build_slopes.py # DEM to slope_x, slope_y
s4_clm/
setup_clm_driver.py # CLM driver + veg maps
s5_forcing/
convert_forcing_to_pfb.py # CMFD/MSWX to CLM PFB
s6_ic_bc/
generate_initial_conditions.py # Pressure head IC
s7_solver/
generate_parflow_script.py # Run script generator
s8_execution/
run_parflow.py # MPI execution wrapper
s9_output/
parse_parflow_output.py # PFB to timeseries/maps
s10_coupling/
parflow_to_cama.py # ParFlow -> CaMa-Flood
docs/
s1_domain_skill.md ... s9_output_skill.md
diagnostics/
triplets.yaml # 25 diagnostic triplets
error_log.yaml # Errors from real runs
model/parflow/
source/ # ParFlow source (git clone)
deps/hypre-install/ # HYPRE dependency
install/bin/parflow # ParFlow binary