**Package**: `modflow6-knowledge-infrastructure` v1.2.0 **Model**: MODFLOW 6 (USGS Modular Hydrologic Model) **Domain**: Groundwater hydrology, subsurface flow, solute transport **Role in HydroCraft**: Fills the groundwater modeling gap — couples with VIC (recharge), CaMa-Flood (river stage), and Lohmann routing (baseflow) ---
Scanned 9/11/2026
Install to Claude Code
npx -y skills add lzwei196/KISS---Knowledge-Infrastructure-for-Scientific-Simulation --skill MODFLOW6 --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of MODFLOW6?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/lzwei196-modflow6)More formats (shields.io, HTML) on the badges page.
> **MANDATORY EXECUTION POLICY** — READ BEFORE PROCEEDING
>
> You MUST run the **actual model binary or package** described in this document.
> If the model fails to import, compile, or execute, you MUST:
> 1. Check `diagnostics/triplets.yaml` for a matching error pattern
> 2. Try the fix suggested in the triplet's `remedy` section
> 3. If still failing, report the error to the user with full details
>
> You MUST NOT substitute a simplified Python formula, regression equation,
> or hand-coded approximation in place of the real model. 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:
> 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/` (30 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` (22 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` (21 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-17 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/calib_run.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/calib_run.py --help` |
| `tools/s1/verify_mf6_installation.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s1/verify_mf6_installation.py --help` |
| `tools/s10_transport/configure_gwt.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s10_transport/configure_gwt.py --help` |
| `tools/s10_transport/parse_gwt_output.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s10_transport/parse_gwt_output.py --help` |
| `tools/s11_sfr/configure_sfr.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s11_sfr/configure_sfr.py --help` |
| `tools/s11_sfr/parse_sfr_output.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s11_sfr/parse_sfr_output.py --help` |
| `tools/s2/build_dis_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2/build_dis_package.py --help` |
| `tools/s2/build_layers_from_global.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2/build_layers_from_global.py --help` |
| `tools/s2/create_grid_from_basin.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2/create_grid_from_basin.py --help` |
| `tools/s3/assign_k_from_glhymps.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s3/assign_k_from_glhymps.py --help` |
| `tools/s3/build_npf_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s3/build_npf_package.py --help` |
| `tools/s3/build_sto_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s3/build_sto_package.py --help` |
| `tools/s4/build_chd_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s4/build_chd_package.py --help` |
| `tools/s4/build_drn_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s4/build_drn_package.py --help` |
| `tools/s4/build_rch_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s4/build_rch_package.py --help` |
| `tools/s4/build_riv_from_cama.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s4/build_riv_from_cama.py --help` |
| `tools/s4/build_riv_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s4/build_riv_package.py --help` |
| `tools/s4/build_wel_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s4/build_wel_package.py --help` |
| `tools/s5/assign_transient_stress.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s5/assign_transient_stress.py --help` |
| `tools/s5/build_ic_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s5/build_ic_package.py --help` |
| `tools/s5/build_tdis_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s5/build_tdis_package.py --help` |
| `tools/s6/build_ims_package.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s6/build_ims_package.py --help` |
| `tools/s7/write_and_run_simulation.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s7/write_and_run_simulation.py --help` |
| `tools/s8/extract_budget.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s8/extract_budget.py --help` |
| `tools/s8/extract_heads.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s8/extract_heads.py --help` |
| `tools/s9/export_to_netcdf.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s9/export_to_netcdf.py --help` |
| `tools/s9/plot_head_map.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s9/plot_head_map.py --help` |
| `tools/s9/plot_water_budget.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s9/plot_water_budget.py --help` |
*28 public tools; `_`-prefixed helpers and packaging files excluded.*
<!-- KI-TOOL-INDEX:END -->
---
## Data Preparation
### Input data
**Input Source**: MODFLOW 6 takes recharge and boundary conditions, not raw met forcing.
- `tools/s4/build_rch_package.py` — Builds recharge package (from VIC drainage or direct estimation)
- `tools/s3/assign_k_from_glhymps.py` — Assigns hydraulic conductivity from GLHYMPS 2.0 dataset
- `tools/s2/build_layers_from_global.py` — Builds layer geometry from DTB China / global datasets
- `tools/s4/build_riv_from_cama.py` — Builds river boundary from CaMa-Flood network
**Data Validation Reference**: See `data_ki/GLHYMPS/SKILL.md` for hydrogeology data documentation.
See `data_ki/FanWTD/SKILL.md` for water table depth documentation.
See `data_ki/GRACE/SKILL.md` for GRACE TWS validation data.
---
# MODFLOW 6 Knowledge Infrastructure — Agent Entry Point
**Package**: `modflow6-knowledge-infrastructure` v1.2.0
**Model**: MODFLOW 6 (USGS Modular Hydrologic Model)
**Domain**: Groundwater hydrology, subsurface flow, solute transport
**Role in HydroCraft**: Fills the groundwater modeling gap — couples with VIC (recharge), CaMa-Flood (river stage), and Lohmann routing (baseflow)
---
## What This Enables
An AI agent can autonomously:
1. Build a MODFLOW 6 groundwater flow model for any basin using FloPy
2. Map VIC deep percolation to MODFLOW recharge (RCH package)
3. Couple CaMa-Flood river stages to MODFLOW river boundaries (RIV package)
4. Feed MODFLOW drain discharge back to routing models as baseflow
5. Extract water table depths for VIC soil moisture feedback
6. Run transient simulations driven by HydroCraft forcing data
7. Add solute transport (GWT) to any existing flow model (nitrate, chloride, etc.)
8. Analyze contaminant plumes, breakthrough curves, and mass budgets
9. Add streamflow routing (SFR) for physics-based stream-aquifer interaction
10. Analyze gaining/losing reaches, stream stage, and downstream flow routing
## Quick Reference
| Component | What | Where |
|-----------|------|-------|
| **Binary** | `mf6` (Fortran CLI) | System PATH or specified path |
| **Python interface** | FloPy (`flopy.mf6`) | `pip install flopy` |
| **Simulation name file** | `mfsim.nam` | Created by FloPy in workspace directory |
| **Input format** | Block/keyword text files (`BEGIN ... END`) | One file per package |
| **Binary output** | `.hds` (heads), `.cbc` (cell budget) | Read with `flopy.utils.HeadFile`, `CellBudgetFile` |
| **Listing file** | `.lst` | Convergence info, budget summary, warnings |
## 6. Output Description
**Source of truth**: `dag.yaml`. The dag is the model identity for observable outputs; if this section and `dag.yaml` ever differ, `dag.yaml` wins.
**Headline output** (the dag's `validation_rank: 1` variable -- the one this model is judged by):
> `hydraulic_head` -- Hydraulic head per active cell; water table is the head in the topmost active non-dry cell per column. (`m`)
| Output variable (dag `var`) | Rank | Unit | Description |
|-----------------------------|------|------|-------------|
| `hydraulic_head` | 1 | `m` | Hydraulic head per active cell; water table is the head in the topmost active non-dry cell per column. |
Other dag outputs currently declared by this KI:
| Output variable (dag `var`) |
|-----------------------------|
| `water_table_elevation` |
| `cell_budget_flux_terms` |
| `solute_concentration` |
| `stream_stage` |
| `stream_aquifer_exchange` |
| `volumetric_budget_discrepancy` |
## 8. Unit Conversion Table
**Source of truth**: model package expectations, the KI's stage tools, and the existing coupling notes in this skill document. MODFLOW 6 does not convert units internally; every row below must be enforced before writing package files or post-processing outputs.
| Variable or exchange | Source unit | MODFLOW/KI unit | Conversion | Notes |
|----------------------|-------------|-----------------|------------|-------|
| `hydraulic_head` | `m` | `m` | `x1` | Dag rank-1 output unit. |
| Recharge from VIC deep percolation / `OUT_BASEFLOW` | `mm/day` | `m/day` | `/1000` | Used by RCH; recharge is a rate, not a volume. |
| Hydraulic conductivity `K` | `m/day` | `m/day` | `x1` | Required when MODFLOW `LENGTH_UNITS` is meters and `TIME_UNITS` is days. |
| Well, drain, and budget flows | `m3/day` | `m3/day` | `x1` | Package rates and cell budget terms use volume per model time. |
| CaMa-Flood river stage `sfcelv` | `m` | `m` | `x1` | Used by RIV stage coupling. |
| MODFLOW DRN flux to routing baseflow | `m3/day` | `m3/day` | `x1` | Downstream routing input in the existing coupling note. |
| CaMa-Flood `outflw` to SFR `INFLOW` | `m3/s` | `m3/day` | `x86400` | SFR period inflow conversion. |
| VIC runoff to SFR `RUNOFF` | `mm/day` | `m3/day` | `runoff * cell_area / 1000` | SFR runoff conversion. |
| GWT molecular diffusion | `m2/s` | `m2/day` | `x86400` | Transport parameter conversion. |
### 8c. Sign Conventions and Output Units
| Variable | Convention in this KI | Impact if wrong |
|----------|-----------------------|-----------------|
| `hydraulic_head` | Elevation head in `m`; dry cells use the HDRY sentinel and must be filtered. | Statistics and maps are corrupted if HDRY is treated as a real head. |
| `water_table_elevation` | Derived from the topmost active non-dry cell per column. | Water table validation binds to the wrong layer if dry cells are not handled first. |
| Recharge | Positive downward input to RCH in `m/day`. | Passing `mm/day` as `m/day` creates a `1000` magnitude error. |
| Cell budget flux terms | Volume rates in `m3/day`; budget percent discrepancy is diagnostic. | Coupling and closure checks fail if rates are interpreted as depths. |
| SFR stream-aquifer exchange | Parsed from SFR budget output in model volume/time units. | Gaining and losing reaches can be classified incorrectly if signs are flipped. |
## 11. Validated Results -- Convention Bars
**Source of truth**: `docs/validation_convention.yaml`. A metric without its convention band is not a verdict. Null convention bands are written as `no cited threshold`.
The dag's rank-1 output is `hydraulic_head`. The currently extracted convention bars provided for this KI are keyed to `water_table_elevation`:
| Dag variable | Metric | Direction | Convention bar (cited) |
|--------------|--------|-----------|-------------------------|
| `water_table_elevation` | `nrmse` | minimize | good: `52.0` (`guillaumot2022`); satisfactory: no cited threshold (`guillaumot2022`) |
| `water_table_elevation` | `pbias` | zero_centered | satisfactory: no cited threshold |
| `water_table_elevation` | `csi` | maximize | satisfactory: no cited threshold |
No cited threshold is stated here for `hydraulic_head` because the extracted convention facts supplied for this update do not include a `hydraulic_head` convention band.
## Pipeline Overview (10 Stages)
```
S1 Installation ──> S2 Grid/DIS ──> S3 NPF/STO ──> S4 Boundary Conditions
│ │
v v
S5 TDIS/IC ──────> S6 Solver/IMS
│
v
S7 Execute mf6
│
v
S8 Extract Heads/Budget
│
v
S9 Postprocess & Visualize
│
v
S10 Transport (GWT) [OPTIONAL]
Add GWT model ──> Run ──> Parse
│
v
S11 Streamflow Routing (SFR) [OPTIONAL]
Add SFR package ──> Run ──> Parse
```
## Skill Documents (Read for Each Stage)
| Stage | Document | Key Decisions |
|-------|----------|---------------|
| S1 | `docs/s1_installation_skill.md` | Binary source (pre-built vs compile), FloPy version |
| S2 | `docs/s2_grid_discretization_skill.md` | Cell size, number of layers, IDOMAIN mask, DIS vs DISV |
| S3 | `docs/s3_layer_properties_skill.md` | K values, confined vs convertible, Ss/Sy, Newton formulation |
| S4 | `docs/s4_boundary_conditions_skill.md` | Which packages (CHD/WEL/RCH/DRN/RIV/EVT), units, VIC/CaMa coupling |
| S5 | `docs/s5_stress_periods_skill.md` | Period lengths, timesteps, steady-state warmup, IC from prior run |
| S6 | `docs/s6_solver_skill.md` | SIMPLE/MODERATE/COMPLEX, DVCLOSE, RCLOSE, under-relaxation |
| S7 | `docs/s7_execution_skill.md` | Write files, run mf6, check listing file, handle failures |
| S8 | `docs/s8_output_extraction_skill.md` | Read .hds/.cbc, HDRY sentinel, budget balance check |
| S9 | `docs/s9_postprocessing_skill.md` | Contour maps, budget plots, NetCDF export for coupling |
## Tools Reference
| Stage | Tool ID | Script | Purpose |
|-------|---------|--------|---------|
| S1 | `verify_mf6_installation` | `tools/s1/verify_mf6_installation.py` | Check mf6 and FloPy availability |
| S2 | `create_grid_from_basin` | `tools/s2/create_grid_from_basin.py` | Basin shapefile to MODFLOW grid |
| S2 | `build_dis_package` | `tools/s2/build_dis_package.py` | Create DIS package via FloPy |
| S3 | `build_npf_package` | `tools/s3/build_npf_package.py` | Create NPF package (K, ICELLTYPE) |
| S3 | `build_sto_package` | `tools/s3/build_sto_package.py` | Create STO package (Ss, Sy) |
| S4 | `build_chd_package` | `tools/s4/build_chd_package.py` | Constant head boundaries |
| S4 | `build_rch_package` | `tools/s4/build_rch_package.py` | Recharge (incl. VIC coupling) |
| S4 | `build_riv_package` | `tools/s4/build_riv_package.py` | River boundaries (incl. CaMa coupling) |
| S4 | `build_drn_package` | `tools/s4/build_drn_package.py` | Drain boundaries (baseflow) |
| S4 | `build_wel_package` | `tools/s4/build_wel_package.py` | Well pumping/injection |
| S5 | `build_tdis_package` | `tools/s5/build_tdis_package.py` | Temporal discretization |
| S5 | `build_ic_package` | `tools/s5/build_ic_package.py` | Initial conditions |
| S5 | `assign_transient_stress` | `tools/s5/assign_transient_stress.py` | Time-varying stress data |
| S6 | `build_ims_package` | `tools/s6/build_ims_package.py` | Solver configuration |
| S7 | `write_and_run_simulation` | `tools/s7/write_and_run_simulation.py` | Write files + execute mf6 |
| S8 | `extract_heads` | `tools/s8/extract_heads.py` | Read binary head file |
| S8 | `extract_budget` | `tools/s8/extract_budget.py` | Read cell budget file |
| S9 | `plot_head_map` | `tools/s9/plot_head_map.py` | Water table contour map |
| S9 | `plot_water_budget` | `tools/s9/plot_water_budget.py` | Budget bar chart |
| S9 | `export_to_netcdf` | `tools/s9/export_to_netcdf.py` | Export for HydroCraft coupling |
| S2 | `build_layers_from_global` | `tools/s2/build_layers_from_global.py` | TOP/BOTM/IDOMAIN/STRT from global rasters |
| S3 | `assign_k_from_glhymps` | `tools/s3/assign_k_from_glhymps.py` | K/Sy from GLHYMPS 2.0 + HWSD pedotransfer |
| S4 | `build_riv_from_cama` | `tools/s4/build_riv_from_cama.py` | RIV package from CaMa-Flood river network |
| S10 | `configure_gwt` | `tools/s10_transport/configure_gwt.py` | Build GWT model (coupled or standalone) |
| S10 | `parse_gwt_output` | `tools/s10_transport/parse_gwt_output.py` | Parse concentration output, plume analysis |
| S11 | `configure_sfr` | `tools/s11_sfr/configure_sfr.py` | Add SFR streamflow routing to GWF model |
| S11 | `parse_sfr_output` | `tools/s11_sfr/parse_sfr_output.py` | Parse SFR stage, exchange, flow budget |
## Global Data Sources
| Dataset | Path | What It Provides |
|---------|------|------------------|
| **GLHYMPS 2.0** | `data/groundwater/glhymps/GLHYMPS.shp` | Hydraulic conductivity (K), porosity — global subsurface hydrogeology |
| **Reinecke WTD** | `data/groundwater/fan_wtd/MeanWaterTableDepth_meter.tif` | Mean water table depth (m) — initial heads for IC package |
| **GLiM lithology** | `data/groundwater/glim/glim_wgs84_0point5deg.txt.asc` | Lithology classes — K validation / cross-check |
| **China DTB** (Yan/Shangguan 2020) | *Manual download* from http://globalchange.bnu.edu.cn/research/cdtb.jsp | Depth-to-bedrock for China — layer bottom elevations |
| **Pelletier regolith** | *Manual download* from https://daac.ornl.gov/cgi-bin/dsviewer.pl?ds_id=1304 | Global regolith thickness — layer bottom elevations outside China |
## Automated Setup Pipeline
Three new tools automate the most error-prone setup steps using the global datasets above:
1. **`build_layers_from_global`** (S2) — Builds TOP/BOTM/IDOMAIN/STRT from DEM + depth-to-bedrock + water table depth rasters. Eliminates manual layer elevation guesswork.
2. **`assign_k_from_glhymps`** (S3) — Assigns K/Sy from GLHYMPS 2.0 for deep layers + HWSD pedotransfer for the shallow soil layer. Handles CRS reprojection and logK decoding automatically.
3. **`build_riv_from_cama`** (S4) — Builds RIV package from CaMa-Flood river network + discharge. Auto-clamps rbot to prevent fatal MODFLOW errors.
## Critical Domain Knowledge (Non-Obvious Facts)
1. **Units are the user's responsibility.** MODFLOW 6 does not convert units. If LENGTH_UNITS is "meters" and TIME_UNITS is "days", then K must be m/day, recharge must be m/day, well rates must be m3/day. There is no error if units are wrong — results will simply be silently wrong.
2. **Recharge is a rate, not a volume.** The RCH package expects recharge in LENGTH/TIME (e.g., m/day), not volume (m3/day). A common error is passing VIC deep percolation as mm/day without converting to m/day (divide by 1000).
3. **HDRY sentinel for dry cells.** When a cell goes dry (head drops below cell bottom), MODFLOW 6 sets head to 1.0e30 (HDRY). Reading this as a real head value corrupts statistics. Always filter HDRY before computing means or plotting.
4. **Layer numbering is 0-indexed in FloPy, 1-indexed in MODFLOW input files.** FloPy layer 0 = MODFLOW layer 1. Cellid tuples in FloPy are (layer, row, col) with 0-based indices. In raw MODFLOW input files, they are 1-based.
5. **ICELLTYPE matters for water table problems.** If the top layer is confined (ICELLTYPE=0) but the water table is in that layer, MODFLOW will not correctly compute saturated thickness — heads can rise above cell top with no error. Set ICELLTYPE=1 for convertible (unconfined) layers.
6. **Newton-Raphson formulation is essential for drying/rewetting.** Standard formulation can oscillate when cells repeatedly wet and dry. Use `NEWTON UNDER_RELAXATION` in the GWF model options for unconfined problems.
7. **Budget percent discrepancy > 1% indicates a problem.** Typical well-posed models have < 0.01% discrepancy. Large discrepancy usually means convergence was not achieved or DVCLOSE/RCLOSE are too loose.
8. **The listing file is the primary diagnostic.** Budget summaries, convergence iterations, dry cell warnings, and error messages are all in `<model_name>.lst`. Always read it after a run.
9. **FloPy arrays must be 3D (nlay x nrow x ncol).** Common error: saving 2D arrays for `strt` or `sy`. FloPy will raise "Unable to set data layer 0. Data is not in a valid format" if you pass a 2D (nrow x ncol) array instead of 3D (nlay x nrow x ncol). Always use `np.broadcast_to()` or `np.stack()` to ensure 3D shape.
10. **Layer 1 K: use VIC/HWSD soil Ksat, NOT GLHYMPS bedrock K.** GLHYMPS represents deep bedrock permeability (log-scale, often 1e-15 to 1e-12 m²). For the shallow water table layer (0-10m), VIC soil Ksat from HWSD pedotransfer is more appropriate (typically 0.1-10 m/day for alluvial basins). Tested on Wangjiaba: VIC K=1.0 m/d + Sy=0.15 gave GRACE r=0.56; GLHYMPS K=0.002 m/d + Sy=0.01 gave r=0.41. Use GLHYMPS for Layer 2-3 (deeper bedrock) only. The `assign_k_from_glhymps.py` tool does this by default — Layer 1 from HWSD, deeper from GLHYMPS — but verify the L1 values are reasonable (>0.1 m/day for alluvial, >0.01 for clay plains).
10. **GLHYMPS CRS is Cylindrical Equal Area (meters), not WGS84.** Use geopandas bbox reprojection when doing spatial joins: reproject your WGS84 grid centroids to GLHYMPS CRS, or reproject GLHYMPS to WGS84. Direct coordinate comparison will fail silently.
11. **GLHYMPS logK values are stored x100.** The `logK_Ferr_` field contains logK(m^2) multiplied by 100. For example, -1180 means logK = -11.80 m^2. Decode: `K_m2 = 10**(logK_field / 100)`, then convert to m/day: `K_m_day = K_m2 * rho * g / mu * 86400`.
12. **RIV rbot must be above cell BOTM.** If river bottom elevation is below the MODFLOW cell bottom, MODFLOW aborts with "RIVER BOTTOM IS LESS THAN CELL BOTTOM". Always clamp: `rbot = max(rbot, botm + 0.5)`.
## Error Handling
When errors occur, look up symptoms in `diagnostics/triplets.yaml` (17 triplets across 7 failure domains). Key triplets:
- **dt_mf6_001**: Convergence failure — solver did not converge within iteration limit
- **dt_mf6_002**: Dry cells in unconfined simulation — cells going dry causing oscillation
- **dt_mf6_003**: Budget imbalance — percent discrepancy > 0.1%
- **dt_mf6_004**: Recharge unit error — mm/day vs m/day mismatch (silent error)
- **dt_mf6_008**: Silent wrong recharge — RCH applied to wrong layer or IRCH not set
## HydroCraft Coupling
See `docs/model_couplings.yaml` for detailed coupling specifications:
- **VIC deep percolation -> MODFLOW RCH**: VIC baseflow_out (mm/day) / 1000 -> m/day recharge
- **MODFLOW DRN flux -> Routing baseflow**: Drain discharge (m3/day) -> routing input
- **CaMa-Flood river stage -> MODFLOW RIV**: CaMa sfcelv (m) -> RIV package stage
- **MODFLOW water table -> VIC feedback**: Water table depth modifies VIC soil moisture capacity
## Minimal Working Example (FloPy)
```python
import flopy
# 1. Create simulation
sim = flopy.mf6.MFSimulation(sim_name="test", sim_ws="./workspace")
# 2. Temporal discretization (1 steady-state period)
tdis = flopy.mf6.ModflowTdis(sim, nper=1, perioddata=[(1.0, 1, 1.0)])
# 3. Solver
ims = flopy.mf6.ModflowIms(sim, complexity="SIMPLE")
# 4. Groundwater flow model
gwf = flopy.mf6.ModflowGwf(sim, modelname="gwf", newtonoptions="NEWTON")
# 5. Discretization (10x10 grid, 1 layer, 100m cells)
dis = flopy.mf6.ModflowGwfdis(gwf, nlay=1, nrow=10, ncol=10,
delr=100.0, delc=100.0,
top=50.0, botm=[0.0])
# 6. Node properties (K=10 m/day, unconfined)
npf = flopy.mf6.ModflowGwfnpf(gwf, icelltype=1, k=10.0)
# 7. Initial conditions
ic = flopy.mf6.ModflowGwfic(gwf, strt=40.0)
# 8. Constant head on left boundary
chd_data = [[(0, i, 0), 45.0] for i in range(10)]
chd = flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_data)
# 9. Recharge
rch = flopy.mf6.ModflowGwfrcha(gwf, recharge=0.001) # 1 mm/day = 0.001 m/day
# 10. Output control
oc = flopy.mf6.ModflowGwfoc(gwf,
head_filerecord="gwf.hds",
budget_filerecord="gwf.cbc",
saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")])
# 11. Write and run
sim.write_simulation()
sim.run_simulation()
# 12. Read results
import flopy.utils.binaryfile as bf
hds = bf.HeadFile("workspace/gwf.hds")
heads = hds.get_data()
```
## Validated Results — Bengbu Basin (Re-validated 2026-03-22)
**Validation Protocol**: 3-step (VALIDATION_PROTOCOL.md)
**Status**: `production_validated`
**FloPy Version**: 3.10.0
### Step 1: Binary Test
- MODFLOW 6.6.1 binary works (compiled Intel Fortran, double precision)
- FloPy 3.10.0: `run_simulation(exe_name=...)` removed, use `subprocess.run()` instead
- Binary output readable with `precision='double'`
- Newton + SIMPLE solver = ERROR (CG incompatible with asymmetric matrix) -> use MODERATE/COMPLEX
### Step 2: Progressive Data Replacement
| Component | Source | Status | Notes |
|-----------|--------|--------|-------|
| Grid | VIC 0.25deg basin_grid.nc | Validated | 16x24 grid, 224 active cells |
| DEM/TOP | China DEM 90m | Validated | Range: 5-1378 m |
| Initial heads | Fan/Reinecke global WTD | Validated | WTD mean: 16.9 m, clamped to 1-5m for IC |
| K (Layer 1) | HWSD via VIC soil params, scaled 100x | Validated | HWSD Ksat underestimates aquifer K by ~100x |
| K (Layers 2-3) | Derived from Layer 1 K/3, K/30 | Validated | Typical depth decay factors |
| Recharge | VIC OUT_BASEFLOW (mm/day -> m/day) | Validated | Mean: 0.578 mm/day = 0.000578 m/day |
| Boundary (CHD) | DEM-derived (lowest 10% + edge cells) | Validated | 71 CHD cells |
| Drain (DRN) | All non-CHD active cells | Validated | Prevents ponding, C = K*1000*W/10 |
| Storage (Ss/Sy) | Literature defaults | Validated | Ss=1e-5, Sy=0.15/0.10/0.05 |
### Step 3: Full HydroCraft Run Results
| Metric | Value | Expected | Status |
|--------|-------|----------|--------|
| WT depth median | 0.9 m | 1-5 m | PASS |
| WT depth mean | 2.0 m | 2-5 m | PASS |
| WT depth range | 0.5 - 5.0 m | 1-10 m | PASS |
| Budget closure | 0.0002% | < 1% | PASS |
| Convergence failures | 0 | 0 | PASS |
| Flow direction | correct (upstream 361m > downstream 30m) | correct | PASS |
| Artesian cells | 0 | < 10% | PASS |
| Layer 1 dry cells | 0% | < 20% | PASS |
| Runtime | 0.4 seconds | < 60s | PASS |
### Key Lessons Learned
1. **HWSD soil Ksat != aquifer K**: HWSD gives soil K (~0.01 m/day), aquifer K is ~1-10 m/day. Scale by ~100x for Layer 1.
2. **Newton requires MODERATE/COMPLEX solver**: SIMPLE uses CG which fails on asymmetric Newton matrices.
3. **DRN conductance must use realistic dimensions**: C = K * drain_width * cell_width / bed_thickness, NOT K * delr * delc.
4. **FloPy 3.10 API changes**: `run_simulation()` no longer accepts `exe_name`, `inner_rclose` -> `rcloserecord`, STO period data format changed.
5. **Deep layer dry cells are normal**: For unconfined aquifer with WT in Layer 1, Layers 2-3 will be dry. Only check Layer 1 dry cells for validation.
6. **Initial heads must be within layer bounds**: Clamping WTD to 1-5m prevents starting heads below cell bottom.
### Validation Files
- Script: `outputs/bengbu_modflow6_revalidation/step2_3_full_bengbu.py`
- Results JSON: `outputs/bengbu_modflow6_revalidation/validation_results.json`
- Plot: `outputs/bengbu_modflow6_revalidation/modflow6_bengbu_validation.png`
- MODFLOW workspace: `outputs/bengbu_modflow6_revalidation/bengbu_workspace/`
---
## Stage S10: Groundwater Transport (GWT)
**Added**: 2026-04-03
**KI Version**: 1.1.0
**Validated**: Minimal 1D advection-dispersion test (coupled GWF-GWT)
### What GWT Does
The GWT Model simulates three-dimensional transport of a single solute species in flowing groundwater. It solves the advection-dispersion equation using numerical methods and can represent:
1. Advective transport with flowing groundwater
2. Hydrodynamic mechanical dispersion and chemical diffusion
3. Sorption (linear, Freundlich, Langmuir isotherms)
4. First-order and zero-order solute decay/production
5. Mass transfer between mobile and immobile zones (dual-domain)
6. Mixing from GWF stress package sources/sinks
### GWT Workflow
```
Existing GWF Model ──> S10a: configure_gwt.py ──> S10b: Run mf6 ──> S10c: parse_gwt_output.py
(flow) (add GWT packages) (coupled run) (concentrations, plumes)
```
**Two coupling modes:**
1. **COUPLED mode (recommended)**: GWF and GWT in same simulation. The GWF-GWT Exchange automatically passes flows. Both models share the same TDIS.
2. **STANDALONE mode**: GWT reads previously saved GWF head (.hds) and budget (.cbc) files via the FMI (Flow Model Interface) package. Useful for testing multiple transport scenarios without re-running flow.
### Required GWT Packages
| Package | Type | Purpose | Key Parameters |
|---------|------|---------|----------------|
| DIS | Discretization | Same grid as GWF | nlay, nrow, ncol, delr, delc, top, botm |
| IC | Initial Conditions | Starting concentration | strt (typically 0.0 for clean aquifer) |
| ADV | Advection | Solute transport by flow | scheme: UPSTREAM (stable), CENTRAL, TVD (accurate) |
| DSP | Dispersion | Mechanical dispersion + diffusion | alh, ath1, atv, diffc |
| MST | Mobile Storage | Porosity, sorption, decay | porosity, sorption, bulk_density, distcoef, decay |
| SSM | Source/Sink Mixing | Concentrations for GWF stress packages | sources: [(pname, srctype, auxname)] |
| OC | Output Control | Save concentration, budget | CONCENTRATION FILEOUT, BUDGET FILEOUT |
### Optional GWT Packages
| Package | Purpose | When to Use |
|---------|---------|-------------|
| CNC | Constant concentration boundary | Fixed concentration sources (e.g., leaking landfill) |
| SRC | Mass source loading | Direct mass injection (not tied to water flow) |
| IST | Immobile zone transfer | Dual-domain (fractured rock, clay lenses) |
| FMI | Flow Model Interface | Standalone mode (reads saved GWF files) |
### Key Transport Parameters
| Parameter | Symbol | Typical Range | Unit | Notes |
|-----------|--------|---------------|------|-------|
| Porosity | n | 0.10-0.40 | - | Mobile domain pore volume fraction |
| Longitudinal dispersivity | ALH | 1-100 | m | Scale-dependent (Gelhar 1992). Start with L/10 where L = plume length |
| Transverse dispersivity | ATH1 | ALH/10 | m | Typically 0.1x longitudinal |
| Vertical dispersivity | ATV | ALH/100 | m | Typically 0.01x longitudinal |
| Molecular diffusion | Dm | 1e-10 to 3e-9 | m2/s | Species-dependent. Convert to m2/day: multiply by 86400 |
| Bulk density | rho_b | 1.4-1.8 | kg/L | For sorption calculations |
| Distribution coefficient | Kd | 0-100 | L/kg | Sorption strength (LINEAR isotherm) |
| First-order decay rate | lambda | 0.0001-0.01 | 1/day | Half-life = ln(2)/lambda |
### Contaminant Presets
The `configure_gwt.py` tool includes documentation-derived parameter presets:
| Preset | Sorption | Decay | Use Case |
|--------|----------|-------|----------|
| conservative | None | None | Tracer tests, chloride, bromide |
| nitrate | LINEAR (weak) | First-order (denitrification) | Agricultural N pollution |
| chloride | None | None | Saltwater intrusion, road salt |
| ammonium | LINEAR (moderate) | First-order (nitrification) | Wastewater, fertilizer |
| phosphate | LINEAR (strong) | None | Eutrophication sources |
| generic_solute | None | None | General transport studies |
### GWF-GWT Exchange (Coupled Mode)
In the simulation name file (`mfsim.nam`), add:
```
BEGIN models
gwf6 gwf.nam gwf
gwt6 gwt.nam gwt
END models
BEGIN exchanges
GWF6-GWT6 gwf-gwt.gwfgwt gwf gwt
END exchanges
BEGIN solutiongroup 1
ims6 gwf.ims gwf
ims6 gwt.ims gwt
END solutiongroup 1
```
The exchange file (`gwf-gwt.gwfgwt`) is empty -- MODFLOW 6 handles the coupling automatically.
### GWT Solver Requirements
- **MUST use BICGSTAB** (not CG) for linear acceleration -- the advection term makes the matrix asymmetric
- Typical convergence: OUTER_DVCLOSE=1e-6, INNER_DVCLOSE=1e-6
- RCLOSE with "strict" option recommended
- Separate IMS for GWT (do not share with GWF IMS)
### SSM (Source/Sink Mixing) -- How Concentrations Enter
Three methods to assign concentrations to GWF stress packages:
1. **Default**: Sources have concentration 0, sinks withdraw at cell concentration
2. **Auxiliary variable**: Add CONCENTRATION auxiliary to GWF package (e.g., WEL with aux CONCENTRATION), reference in SSM SOURCES block
3. **SPC6 file**: Separate file with time-varying concentrations per stress period
Example SSM with WEL concentration:
```python
# In GWF: well with concentration auxiliary
wel = flopy.mf6.ModflowGwfwel(gwf, auxiliary="CONCENTRATION",
stress_period_data=[[(0, 5, 5), -100.0, 50.0]]) # Q=-100, C=50
# In GWT SSM:
ssm = flopy.mf6.ModflowGwtssm(gwt,
sources=[("WEL-1", "AUX", "CONCENTRATION")])
```
### Tools Reference
| Tool | Script | Purpose |
|------|--------|---------|
| `configure_gwt` | `tools/s10_transport/configure_gwt.py` | Build GWT model coupled to or standalone from GWF |
| `parse_gwt_output` | `tools/s10_transport/parse_gwt_output.py` | Parse .ucn concentration files, compute plume extent, breakthrough curves |
### Example Usage
```python
# 1. Coupled mode -- add GWT to existing GWF simulation
from tools.s10_transport.configure_gwt import build_coupled_gwt
result = build_coupled_gwt(
gwf_sim_ws="/path/to/gwf/workspace",
contaminant="nitrate",
source_concentration=50.0, # mg/L
source_cells=[(0, 5, 5)], # layer 0, row 5, col 5
advection_scheme="UPSTREAM",
run_model=True,
)
# 2. Parse results
from tools.s10_transport.parse_gwt_output import GWTOutputParser
gp = GWTOutputParser(
concentration_file=result["concentration_file"],
listing_file=result["listing_file"],
delr=100.0, delc=100.0,
)
summary = gp.summary()
plume = gp.plume_extent(threshold=1.0, layer=0)
btc = gp.breakthrough_curve(layer=0, row=10, col=10)
```
### CLI Usage
```bash
# List contaminant presets
python configure_gwt.py --list-presets
# Build coupled GWT from existing GWF
python configure_gwt.py --gwf-dir /path/to/gwf --contaminant nitrate \
--source-conc 50.0 --source-cell 0,5,5
# Parse results
python parse_gwt_output.py gwt.ucn --summary --listing-file gwt.lst
python parse_gwt_output.py gwt.ucn --plume 1.0 --delr 100 --delc 100
python parse_gwt_output.py gwt.ucn --breakthrough 0,10,10
python parse_gwt_output.py gwt.ucn --plume-evolution 1.0 --json
python parse_gwt_output.py gwt.ucn --export-csv concentrations.csv
```
### Critical Domain Knowledge for GWT
1. **GWT "flows" are mass flows, not water flows.** Budget terms in the GWT listing file are in mass/time units, not volume/time.
2. **GWT concentration files (.ucn) use text="CONCENTRATION"**, not "HEAD". When reading with FloPy's HeadFile, pass `text="CONCENTRATION"`.
3. **The GWT solver MUST use BICGSTAB**, not CG. The advection term produces an asymmetric matrix. CG will fail or produce wrong results.
4. **Dispersion XT3D is expensive.** Use `xt3d_off=True` for 1D problems or when flow aligns with the grid. Remove for 2D/3D problems where diagonal flow is important.
5. **SSM is required if the GWF model has ANY stress packages**, even if all concentrations are zero. Without SSM, MODFLOW 6 will error.
6. **GWT has no steady-state option.** To find steady-state concentrations, run a long transient simulation until concentrations stabilize.
7. **Multiple species require multiple GWT models.** Each GWT model simulates one species. Add multiple GWT entries in mfsim.nam, each with its own exchange.
8. **Dispersivity is scale-dependent.** Field-scale ALH is typically 0.1x to 1x the plume length scale (Gelhar et al., 1992). Lab-scale values are much smaller.
9. **MODFLOW 6 GWT does NOT have particle tracking or MOC.** Only finite-difference methods (upstream, central, TVD). For sharp fronts, use TVD scheme or refine the grid.
10. **HYPE-MODFLOW coupling for nutrients**: HYPE nutrient leachate (N, P) can be mapped to MODFLOW GWT via the SSM package. HYPE deep percolation concentration -> RCH auxiliary CONCENTRATION, or via SPC6 files for time-varying concentrations.
### HydroCraft Coupling for Transport
| Coupling | From | To | Mechanism |
|----------|------|----|-----------|
| HYPE N leachate -> GWT | HYPE deep_perc_N (mg/L) | GWT SSM or CNC | RCH aux CONCENTRATION or SPC6 file |
| HYPE P leachate -> GWT | HYPE deep_perc_P (mg/L) | GWT SSM or CNC | Separate GWT model for P |
| WEL injection -> GWT | Well concentration | GWT SSM | WEL aux CONCENTRATION |
| GWT plume -> HYPE | GW concentration | HYPE return flow quality | Not yet automated |
### Validation Results (Minimal Test)
| Metric | Value | Expected | Status |
|--------|-------|----------|--------|
| mf6 runs coupled GWF-GWT | Yes | Yes | PASS |
| Concentration file readable | Yes | Yes | PASS |
| CNC boundary holds C=1.0 | 1.0000 | 1.0 | PASS |
| Advection-dispersion plume | 9 cells > 0.01 at t=500d | Plume grows over time | PASS |
| Breakthrough curve monotonic | Yes | Yes (continuous source) | PASS |
| parse_gwt_output.py works | All features tested | - | PASS |
---
## Stage S11: Streamflow Routing (SFR)
**Added**: 2026-04-03
**KI Version**: 1.2.0
**Validated**: Self-contained test model (10-reach diagonal stream, 10x10 grid)
### What SFR Does
The SFR Package simulates 1D open-channel flow in stream networks. Unlike the simpler RIV package (which requires user-specified stream stage), SFR **computes stream stage from Manning's equation** based on actual stream geometry and flow. This provides:
1. Physically realistic stream-aquifer interaction based on computed stream depth
2. Downstream flow routing through connected reaches
3. Diversions (irrigation canals, water management)
4. Tracking of gaining/losing reaches along the network
5. Proper handling of dry reaches (no-flow conditions)
### When to Use SFR vs RIV
| Criterion | Use RIV | Use SFR |
|-----------|---------|---------|
| Stream stage is known (from observations or CaMa-Flood) | YES | Not needed |
| Stream stage should be computed from flow | No | YES |
| Need to route flow downstream | No | YES |
| Need diversions / irrigation canals | No | YES |
| Need gaining/losing analysis per reach | Approximate | YES (exact) |
| Simple setup, few river cells | YES | Overkill |
| Complex stream network, many reaches | Possible but simplistic | YES |
| Data available: only river stages | YES | Not enough (need geometry) |
| Data available: stream network + geometry | Possible | YES (preferred) |
**Rule of thumb**: Use RIV when you have CaMa-Flood or observed stream stages. Use SFR when you need to compute stream stage from flow, route streamflow, or analyze stream-aquifer interaction in detail.
### SFR Workflow
```
Existing GWF Model ──> S11a: configure_sfr.py ──> S11b: Run mf6 ──> S11c: parse_sfr_output.py
(flow) (add SFR package) (simulation) (stages, exchange, budget)
```
### SFR Key Concepts
1. **Reach**: A segment of stream within a single model cell. Each reach has static properties (length, width, slope, Manning's n, streambed K, streambed thickness) defined in PACKAGEDATA.
2. **Connection**: Topology defining which reaches connect to which. Upstream connections are positive; downstream connections are negative in the CONNECTIONDATA block.
3. **Period data**: Time-varying settings per reach (inflow, rainfall, evaporation, runoff, diversion, status).
4. **Manning's equation**: Q = (1/n) * A * R^(2/3) * S^(1/2). SFR uses this to compute stream stage from flow. Requires unit conversion factors when using meters + days.
### Required SFR Parameters
| Parameter | Symbol | Typical Range | Unit | Notes |
|-----------|--------|---------------|------|-------|
| Reach length | rlen | Cell dimension | m | Along-stream length within cell |
| Reach width | rwid | 1-100 | m | Top width of channel |
| Stream gradient | rgrd | 1e-4 to 0.05 | - | Bed slope (must be > 0) |
| Streambed top | rtp | DEM - 1 to 5 m | m | Elevation of streambed surface |
| Streambed thickness | rbth | 0.5-3.0 | m | Thickness of low-K streambed |
| Streambed K | rhk | 0.01-10 | m/day | Hydraulic conductivity of streambed |
| Manning's n | man | 0.015-0.06 | s/m^(1/3) | Channel roughness |
### Stream Type Presets
The `configure_sfr.py` tool includes documentation-derived presets:
| Preset | Manning's n | Streambed K | Width | Slope | Use Case |
|--------|-------------|-------------|-------|-------|----------|
| mountain_stream | 0.04 | 1.0 m/day | 5 m | 0.01 | Headwaters, cobble bed |
| lowland_river | 0.03 | 5.0 m/day | 30 m | 0.001 | Alluvial floodplain |
| canal | 0.015 | 0.01 m/day | 10 m | 0.0005 | Lined irrigation channel |
| wetland_channel | 0.06 | 0.5 m/day | 15 m | 0.0001 | Marsh drainage |
| generic | 0.035 | 1.0 m/day | 10 m | 0.001 | General purpose |
### Tools Reference
| Tool | Script | Purpose |
|------|--------|---------|
| `configure_sfr` | `tools/s11_sfr/configure_sfr.py` | Add SFR package to GWF model |
| `parse_sfr_output` | `tools/s11_sfr/parse_sfr_output.py` | Parse stage, exchange, budget |
### Example Usage
```python
# 1. Simple linear stream -- auto-builds reaches from cell list
from tools.s11_sfr.configure_sfr import build_simple_sfr
result = build_simple_sfr(
gwf_sim_ws="/path/to/gwf/workspace",
stream_cells=[(0,0,0), (0,1,1), (0,2,2), (0,3,3), (0,4,4)],
stream_elevations=[100.0, 98.0, 96.0, 94.0, 92.0],
stream_preset="lowland_river",
inflow_rate=1000.0, # m3/day
run_model=True,
)
# 2. Full control -- custom reach data + connections
from tools.s11_sfr.configure_sfr import build_sfr_package
reach_data = [
{"ifno": 1, "cellid": (0,0,5), "rlen": 500, "rtp": 50.0, "ncon": 1},
{"ifno": 2, "cellid": (0,1,5), "rlen": 500, "rtp": 49.5, "ncon": 2},
{"ifno": 3, "cellid": (0,2,5), "rlen": 500, "rtp": 49.0, "ncon": 1},
]
connection_data = [
[1, -2], # reach 1 flows downstream to reach 2
[2, 1, -3], # reach 2 connects to upstream 1 and downstream 3
[3, 2], # reach 3 connects to upstream 2
]
result = build_sfr_package(
gwf_sim_ws="/path/to/gwf",
reach_data=reach_data,
connection_data=connection_data,
stream_preset="generic",
period_data={0: [[1, "INFLOW", 500.0]]},
run_model=True,
)
# 3. Parse results
from tools.s11_sfr.parse_sfr_output import SFROutputParser
sp = SFROutputParser(
stage_file=result["stage_file"],
budget_file=result["budget_file"],
listing_file=result["listing_file"],
)
summary = sp.summary()
exchange = sp.get_stream_aquifer_exchange()
ts = sp.stage_time_series(reach_number=1)
```
### CLI Usage
```bash
# List stream presets
python configure_sfr.py --list-presets
# Build self-contained test model
python configure_sfr.py --test --workspace /tmp/sfr_test
# Add SFR to existing GWF model from JSON
python configure_sfr.py --gwf-dir /path/to/gwf --reach-json reaches.json \
--preset lowland_river
# Parse output
python parse_sfr_output.py --stage gwf.sfr.stg --budget gwf.sfr.cbc \
--listing gwf.lst --summary
python parse_sfr_output.py --stage gwf.sfr.stg --reach-ts 5
python parse_sfr_output.py --budget gwf.sfr.cbc --exchange
python parse_sfr_output.py --stage gwf.sfr.stg --export-csv stages.csv
python parse_sfr_output.py --stage gwf.sfr.stg --summary --json
```
### Critical Domain Knowledge for SFR
1. **SFR uses Manning's equation for stage calculation.** Stream stage is NOT user-specified (unlike RIV). SFR computes depth from flow using Q = (1/n) * A * R^(2/3) * S^(1/2), where A = width * depth, R = A / (width + 2*depth).
2. **Unit conversion is required for Manning's equation.** For meters + days, use `length_conversion=1.0` and `time_conversion=86400.0`. The `unit_conversion` keyword is deprecated since v6.4.2. Without proper conversion, computed stages will be wildly wrong.
3. **Stream gradient (rgrd) must be positive.** MODFLOW 6 will terminate if any reach has rgrd <= 0. When building from DEM, clamp minimum slope to ~1e-6.
4. **Streambed top (rtp) minus streambed thickness (rbth) must be above cell bottom.** Otherwise MODFLOW 6 will abort. Always validate: rtp - rbth > botm[lay, row, col].
5. **Connection data uses signed reach numbers.** Negative values = downstream, positive = upstream. The first entry in each connection list is the reach number itself.
6. **SFR reaches must be connected sequentially.** Unlike RIV (independent cells), SFR requires proper network topology. Incorrect connections cause mass balance errors or convergence failures.
7. **Dry reaches get DNODATA sentinel (-1e30) in stage output.** When a reach goes dry (no flow), MODFLOW sets stage to -1e30. Always filter these before statistics or plotting.
8. **SFR is an "advanced" stress package.** Unlike simple packages (RIV, CHD), SFR period data persists across stress periods unless explicitly changed. An empty PERIOD block does NOT remove SFR stresses.
9. **SFR and RIV should NOT be used in the same cell.** Having both packages in the same cell causes double-counting of stream-aquifer exchange. Use one or the other per cell.
10. **CaMa-Flood integration**: CaMa-Flood can provide river discharge as SFR upstream inflow (INFLOW keyword in period data), or stream widths for reach width. However, CaMa-Flood does not provide cross-sections or Manning's n -- those must come from literature or field data.
### HydroCraft Coupling for SFR
| Coupling | From | To | Mechanism |
|----------|------|----|-----------|
| CaMa-Flood Q -> SFR INFLOW | CaMa outflw (m3/s) | SFR period INFLOW | outflw * 86400 -> m3/day at headwater reaches |
| CaMa-Flood width -> SFR rwid | CaMa rivwth (m) | SFR packagedata rwid | Direct mapping |
| VIC runoff -> SFR RUNOFF | VIC runoff (mm/day) | SFR period RUNOFF | runoff * cell_area / 1000 -> m3/day |
| SFR outflow -> CaMa-Flood | SFR downstream-flow | CaMa lateral inflow | Not yet automated |
### Validation Results (Test Model)
| Metric | Value | Expected | Status |
|--------|-------|----------|--------|
| mf6 runs with SFR | Yes | Yes | PASS |
| Normal termination | Yes | Yes | PASS |
| Stage file readable | Yes | Yes | PASS |
| Budget file readable | Yes | Yes | PASS |
| Stream stage computed (reach 1) | 98.03 m | Near land surface - 2m | PASS |
| Stream-aquifer exchange | -5000 m3/day (all losing) | Inflow goes to aquifer | PASS |
| Budget closure | -0.00% | < 0.01% | PASS |
| Convergence failures | 0 | 0 | PASS |
| parse_sfr_output.py works | All features tested | - | PASS |
---
*This knowledge infrastructure was built using the Knowledge Dissection Toolkit v1.0 (Jianyun Zhang Research Group, Hohai University).*
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!