EPIC · git:20261025.e9f339e · 2026-10-25 · sha256 ad2e1744239bf64a
EPIC git:20261025.e9f339eC
Immutable. This exact content never changes and is served at /api/v1/blob/ad2e1744239bf64a.
> **MANDATORY EXECUTION POLICY** — READ BEFORE PROCEEDING
>
> You MUST run the **actual EPIC1102 binary** (`epic1102-official_release.exe`)
> for any run described in this document. EPIC is closed-source Fortran; a
> Python reimplementation would not reproduce its crop growth, CENTURY soil
> organic matter, MUSLE erosion, or denitrification modules.
>
> If the model fails to 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 EPIC binary, Wine, and required data are available.
>
> **DEBUGGING PROTOCOL** — When something goes wrong (model crashes, wrong output,
> unexpected values), follow this order. Do NOT skip steps or write debug scripts:
> 1. **Check triplets** — `diagnostics/triplets.yaml` may already cover this error
> 2. **Read official docs** — Check the model's own documentation (PDF manual, README,
> official examples) for expected input formats, variable names, and units
> 3. **Find working examples** — Look in `templates/` for the shipped
> `epic1102_example_files_20221002` workspace, or in `outputs/` for previous
> successful runs
> 4. **Fix the tool** — Now that you know what "correct" looks like, make targeted fixes
>
> Resist the urge to write diagnostic/debug Python scripts. The answers are almost
> always in the official examples, not in reverse-engineering the binary.
<!-- 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/` (17 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. |
| on ANY error, before debugging | `diagnostics/triplets.yaml` (32 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` (20 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 — 9 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/build_control_files.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/build_control_files.py --help` |
| `tools/build_opc_file.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/build_opc_file.py --help` |
| `tools/build_site_file.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/build_site_file.py --help` |
| `tools/build_soil_file.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/build_soil_file.py --help` |
| `tools/configure_control.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/configure_control.py --help` |
| `tools/configure_irrigation.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/configure_irrigation.py --help` |
| `tools/convert_forcing_to_epic.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/convert_forcing_to_epic.py --help` |
| `tools/edit_opc.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/edit_opc.py --help` |
| `tools/parse_outputs.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/parse_outputs.py --help` |
| `tools/quickstart.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/quickstart.py --help` |
| `tools/resolve_obs_shape.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/resolve_obs_shape.py --help` |
| `tools/run_epic.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/run_epic.py --help` |
| `tools/select_crop.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/select_crop.py --help` |
| `tools/set_output_types.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/set_output_types.py --help` |
| `tools/tune_parm.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/tune_parm.py --help` |
*15 public tools; `_`-prefixed helpers and packaging files excluded.*
<!-- KI-TOOL-INDEX:END -->
---
# EPIC 1102 — Environmental Policy Integrated Climate
EPIC (Environmental Policy Integrated Climate; formerly Erosion-Productivity
Impact Calculator) is a field-scale agro-ecosystem model developed by the
USDA/Texas A&M Blackland Research Center. It simulates, on a daily time step,
the complete land-atmosphere interaction of a cropped (or natural-vegetation)
field: weather, hydrology, soil erosion, nutrient cycling, pesticide fate,
plant growth, tillage, and management economics.
This knowledge infrastructure wraps the **EPIC 1102 official release** (Win32
console executable, v2025-05-25) with copy-first Python tools that preserve
Fortran fixed-format files, a CMFD/NASA-POWER forcing adapter, an HWSD soil
adapter, and output parsers for the model's annual/daily text files.
## Model overview
| Attribute | Value |
|------------------------|---------------------------------------------------|
| Name | EPIC 1102 |
| Version | v2025-05-25 (official release) |
| Domain | agro-ecosystem / soil-crop-water/nutrient |
| Scale | field (point); up to 10 soil layers |
| Time step | daily (sub-daily sediment routing) |
| Language | Fortran (compiled to PE32 Windows console EXE) |
| Execution on Linux | via Wine |
| Source | closed binary; example workspace included |
| Primary reference | Sharpley & Williams (1990); Williams (1995) |
| Binary | `epic1102-official_release.exe` |
Working workspace used for this KI:
`KISSPATH_INTERNAL_NOT_SHIPPED/auto_dissect_multi_agent/_work_v2/EPIC/epic_workspace`
## Installation / environment
EPIC 1102 is distributed as a precompiled Windows console executable. On this
HydroCraft server it is launched under Wine.
```bash
which wine # must resolve
ls KISSPATH_INTERNAL_NOT_SHIPPED/auto_dissect_multi_agent/_work_v2/EPIC/epic_workspace/epic1102-official_release.exe
wine /path/to/epic1102-official_release.exe # runs in CWD
```
All input files must live **in the current working directory** when the
executable is launched — EPIC reads by hard-coded filename from CWD.
## Pipeline stages
```
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ 1. SITE PREPARATION │───▶│ 2. FORCING │───▶│ 3. SOIL + LANDUSE │
│ lat/lon/elev/slope │ │ CMFD→.DLY,.WP1,.WND │ │ HWSD → .SOL │
│ → umstead.SIT │ │ │ │ │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ 4. OPERATIONS (.OPC) │───▶│ 5. CONTROL FILES │───▶│ 6. RUN EPIC (wine) │
│ plant/fert/harvest │ │ RUN/CONT/list-COMs │ │ epic1102-...exe │
│ crop code from DB │ │ │ │ │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
│
▼
┌──────────────────────────┐
│ 7. PARSE OUTPUTS │
│ .ACY yield, .ANN wbal │
│ → CSV + validation │
└──────────────────────────┘
```
## Input files (copy-first — NEVER regenerate from scratch)
EPIC uses **fixed-width Fortran-formatted** files where column position is
load-bearing. A single misaligned digit causes silent wrong-result failures.
| File | Purpose | Template in |
|-------------------|----------------------------------------------|-------------|
| `EPICRUN.DAT` | Run list: site_id, indexes into each list | templates/ |
| `EPICCONT.DAT` | Simulation control (years, flags, params) | templates/ |
| `EPICFILE.DAT` | Names of database files | templates/ |
| `SITECOM.DAT` | List of `.SIT` files (index → filename) | templates/ |
| `SOILCOM.DAT` | List of `.SOL` files | templates/ |
| `OPSCCOM.DAT` | List of `.OPC` operation schedules | templates/ |
| `WPM1USEL.DAT` | List of `.WP1` monthly weather stats | templates/ |
| `WINDUSEL.DAT` | List of `.WND` wind stats | templates/ |
| `WDLSTCOM.DAT` | List of `.DLY` daily weather files | templates/ |
| `CROPCOM.DAT` | Crop parameter database (58 cols, 150+ crops)| templates/ |
| `FERT2012.DAT` | Fertilizer database | templates/ |
| `TILLCOM.DAT` | Tillage implement database | templates/ |
| `PESTCOM.DAT` | Pesticide database | templates/ |
| `PARM1102.DAT` | Global model parameters | templates/ |
| `MLRN1102.DAT` | MLRN machine-learning-N parameters | templates/ |
| `PRNT1102.DAT` | Output variable selection / print flags | templates/ |
| `<site>.SIT` | Site: lat, lon, elev, slope, CO2 | per run |
| `<site>.SOL` | Soil profile (up to 10 layers, 40+ props) | per run |
| `<site>.OPC` | Operation schedule (plant/fert/till/harvest) | per run |
| `<stn>.DLY` | Daily weather: yr mo dy RAD TMX TMN PRCP RHUM WSPD | per run|
| `<stn>.WP1` | Monthly mean weather (for generator backfill)| per run |
| `<stn>.WND` | Monthly wind | per run |
## Output files
| File | Content |
|--------------|--------------------------------------------------------------|
| `*.OUT` | Main human-readable run log (echo + variable names + summaries)|
| `*.ACY` | Annual crop yield: YR RT# CPNM YLDG YLDF WCYD HI BIOM RW YLN YLP YLC …|
| `*.ANN` | Annual summary: YR AP15 PMTE TMX TMN PRCP IRGA PET ET Q DN LIME DN2O FULU|
| `*.DWC` | Daily water cycle |
| `*.DCN` | Daily C/N cycle |
| `*.DGN` | Daily general |
| `*.ACN` | Annual C and N |
| `*.ASL` | Annual soil layer |
| `*.MFS` | Monthly flood sediment |
## Output Description
This section restates `dag.yaml`; if it ever disagrees with the dag, the dag
wins. The dag's rank-1 output is:
> var='YLDG' unit='t/ha' description='Annual grain / economic crop yield (reported kg/ha internally, t/ha in .ACY)'
| Output variable (dag `var`) | Rank | File | Unit | Description |
|---|---:|---|---|---|
| `YLDG` | 1 | `.ACY` | `t/ha` | Annual grain / economic crop yield (reported kg/ha internally, t/ha in .ACY) |
| `ET` | 2 | `.ANN` | `mm/yr` | Actual evapotranspiration: water flux from the modeled field to the atmosphere |
| `BIOM` | 3 | `.ACY` | `t/ha` | Total above-ground biomass |
| `Q` | 4 | `.ANN` | `mm/yr` | Surface runoff only; excludes deep percolation and lateral subsurface flow |
| `MUSS/MUST/USLE (water erosion sediment yield)` | 5 | `.ANN` | `Mg/ha` | Soil loss by water erosion via the selected MUSLE/USLE/MUSS/MUST/RUSLE equation |
| `DN2O / VN2O` | 6 | `.ANN` | `kg/ha` | Nitrous-oxide gas emission to the atmosphere from soil denitrification / nitrification |
| `QNO3 / SNO3 (nitrate loss / soil nitrate)` | 7 | `.ANN` | `kg/ha` | Mineral N lost with runoff/leaching and residual soil nitrate |
| `YOC / TOC (soil organic carbon)` | 8 | `.ANN / .ACN` | `kg/ha` | Organic carbon in sediment / total soil-profile organic carbon (CENTURY) |
| `NPPC` | 9 | `.ANN` | `kg/ha` | Crop/vegetation net primary productivity (carbon); VAR(99) = dry matter x 420.0 |
Other dag outputs, in dag order, are:
`['BIOM', 'ET', 'Q', 'MUSS/MUST/USLE (water erosion sediment yield)', 'DN2O / VN2O', 'QNO3 / SNO3 (nitrate loss / soil nitrate)', 'YOC / TOC (soil organic carbon)', 'NPPC']`.
## Unit trap table (verified empirically)
| Variable | Model unit | CMFD/NASA unit | Conversion |
|---------------|----------------|-------------------|------------------|
| Precipitation | mm/day | kg m-2 s-1 (CMFD) | × 86400 |
| Tmax / Tmin | degC | K (CMFD), degC (POWER) | − 273.15 (CMFD only)|
| Radiation | MJ m-2 day-1 | W m-2 (CMFD srad) | × 0.0864 |
| Rel. humidity | fraction 0-1 | % (MERRA2) | × 0.01 |
| Wind speed | m/s | m/s (CMFD,MSWX) | identity |
| Elevation | m | m (HWSD DEM) | identity |
| Slope | m/m | m/m (SRTM) | identity |
| Soil bulk density | t/m3 | g/cm3 (HWSD) | identity (1 g/cm3 = 1 t/m3)|
| Sand / clay / silt | % | fraction | × 100 |
| Yield YLDG | t/ha | kg/ha | ÷ 1000 |
**Critical trap**: the `.DLY` file is `(3I4,6F6.*)` fixed-width with
**no column labels**. Column order is: YEAR MON DAY SRAD TMX TMN PRCP RH WSPD.
Radiation is MJ m-2 day-1 (NOT W m-2). Temps in degC (NOT Kelvin).
Precipitation in mm (NOT kg m-2 s-1).
## Unit Table
This unit table restates the active model/output units used by the KI. Exact
I/O shapes live in `docs/format_spec.yaml`; output identity lives in `dag.yaml`.
| Variable | Source or internal unit | Model/output unit | Conversion or rule |
|---|---|---|---|
| Precipitation | `kg m-2 s-1` in CMFD | `mm/day` in `.DLY` | multiply by `86400` |
| Tmax / Tmin | `K` in CMFD; `degC` in NASA POWER | `degC` in `.DLY` | subtract `273.15` for CMFD only; NASA POWER is identity |
| Radiation | `W m-2` in CMFD `srad` | `MJ m-2 day-1` in `.DLY` | multiply by `0.0864` |
| Relative humidity | `%` in MERRA2 | fraction `0-1` in `.DLY` | multiply by `0.01` |
| Wind speed | `m/s` in forcing | `m/s` in `.DLY` | identity |
| Elevation | `m` in DEM | `m` in `.SIT` | identity |
| Slope | `m/m` in terrain source | `m/m` in `.SIT` | identity |
| Soil bulk density | `g/cm3` in HWSD | `t/m3` in `.SOL` | identity because `1 g/cm3 = 1 t/m3` |
| Sand / clay / silt | fraction in source | `%` in `.SOL` | multiply by `100` |
| Yield `YLDG` | `kg/ha` internally | `t/ha` in `.ACY` and dag | divide by `1000` |
| Biomass `BIOM` | EPIC annual crop output | `t/ha` in `.ACY` and dag | read as reported |
| Actual ET `ET` | EPIC annual water-balance output | `mm/yr` in `.ANN` and dag | read as reported |
| Surface runoff `Q` | EPIC annual water-balance output | `mm/yr` in `.ANN` and dag | read as surface runoff only |
| Water erosion sediment yield `MUSS/MUST/USLE` | EPIC annual erosion output | `Mg/ha` in `.ANN` and dag | read selected erosion equation output |
| `DN2O / VN2O` | EPIC annual N gas output | `kg/ha` in `.ANN` and dag | read as reported |
| `QNO3 / SNO3` | EPIC annual nitrate output | `kg/ha` in `.ANN` and dag | read as reported |
| `YOC / TOC` | EPIC annual carbon output | `kg/ha` in `.ANN / .ACN` and dag | read as reported |
| `NPPC` | EPIC annual productivity output | `kg/ha` in `.ANN` and dag | dry matter x `420.0` |
## Binding an observed yield product to a dag obs_shape (MANDATORY before scoring)
EPIC is a 0-D **point field** model. Most yield observations on this server are
not points -- GDHY (0.5deg gridded), SPAM, FAOSTAT and statistical yearbooks are
**area averages**. `dag.yaml` offers `outputs[YLDG].observability` BOTH
`point_time_series` and `regional_aggregate_time_series`; picking the wrong one
silently changes which metric is the verdict.
Run this BEFORE scoring anything, and record its output in the run notes:
```bash
python KISSPATH_KI_ROOT/EPIC/knowledge_infrastructure/tools/resolve_obs_shape.py \
--var YLDG --granularity grid --geometry "0.5deg grid cell" \
--dataset-id gdhy_v1_2_v1_3 --n-steps 20
```
Decision rule (the tool reads the contract out of `dag.yaml`, nothing hard-coded):
| obs characteristic | obs_shape | VERDICT metric (the only one) | diagnostic metrics (report, never a verdict) |
|---|---|---|---|
| station / experiment plot / single field, >=2 seasons | `point_time_series` | `pbias` | `r`, `NSE`, `KGE`, `RMSE` |
| gridded cell, region, province, nation, or a census-anchored product (GDHY, SPAM, FAOSTAT, yearbook) | `regional_aggregate_time_series` | `pbias` | `r_detr`, `r_firstdiff`, `slope_ratio`, `slope_obs`, `slope_sim`, `RMSE` |
| one season only | `point_snapshot` | `pbias` | `RMSE` |
The verdict is the dag's `determining_metric` and **nothing else**. The tool
emits `verdict_metrics` (always exactly that one metric), `diagnostic_metrics`
(pattern evidence for the chosen shape) and `non_verdict_metrics` (metrics whose
dag `metric_families` belong to a *different* obs_shape of the same var). The
detrended trend metrics are **evidence that the weather signal exists**, not a
second verdict -- `docs/validation_convention.yaml` gives
`yield / regional_aggregate_time_series` a single headline metric, `pbias`.
`dag.yaml` names metric *families* (`trend_match`), not metric *keys*
(`r_detr`). The family -> key binding lives in one data table in the tool,
`METRIC_FAMILY_SOURCE`, which records for each family **which
`ki_tools_common.metrics` scorer computes it** and which of that scorer's
emitted keys it covers (`trend_match` -> `trend_metrics`, and that includes
`PBIAS`, which `trend_metrics` returns alongside the trend keys). Verify the
binding against the live scorers before trusting a resolution:
```bash
python .../tools/resolve_obs_shape.py --self-check # exit 0 = registry faithful
```
`--self-check` calls `all_metrics` / `trend_metrics` / `spatial_metrics` and
fails if any declared key is not actually emitted. Every `resolve()` also
carries a static version of the same check in `metric_registry_check`. A dag
family with no row in the registry is never dropped -- it is reported in
`unmapped_metric_families`, whether it came from the chosen obs_shape or from
one of the var's other shapes.
**Raw inter-annual `r` / `NSE` / `KGE` are NOT a verdict against an area-average
yield obs.** Two independent structural reasons:
1. *Technology trend.* An area-average yield obs often rises year on year from
cultivar turnover, input intensification and mechanisation -- a
fixed-management, fixed-cultivar, fixed-PPOP EPIC run has no mechanism for it.
Raw `r` can collapse; detrended and first-difference `r` show whether the
weather signal is there once the trend is removed.
2. *Variance damping.* A 0.5deg cell mean averages hundreds of heterogeneous
fields, so its inter-annual variance is smaller than any single field's.
`NSE`/`KGE` penalise that scale mismatch as if it were model error.
This is exactly the dag caveat on the `regional_aggregate_time_series` block
("compare detrended trend, not inter-annual r") and `docs/validation_convention.yaml`
(`yield` / `regional_aggregate_time_series`: headline metric is `pbias` only).
See triplet **EPIC_025**.
### Yield moisture basis -- state it, never assume it
The unit table below gives only the kg/ha -> t/ha conversion. It is **not** the
whole convention: **EPIC `.ACY` YLDG/YLDF/BIOM are DRY MATTER**. The obs side's
basis is **not** something a run may assume -- but it is also no longer something
each run re-decides. `resolve_obs_shape.py` now carries an **obs moisture-basis
registry** (`OBS_MOISTURE_BASIS`), one row per obs product, each row carrying the
documentary chain that ESTABLISHES it. Ask the tool:
```bash
python tools/resolve_obs_shape.py --var YLDG --granularity grid \
--geometry "0.5deg grid cell" --dataset-id gdhy_v1_2_v1_3 --crop maize \
--n-steps 20 # -> .moisture_basis.obs_native / .conversion
```
**GDHY / FAOSTAT / SPAM cereals are MARKET (commercial) moisture, established**
(registered 2026-07-29, closing the open item of triplet EPIC_025):
- GDHY grid yields are *"estimated using the satellite-derived crop-specific
vegetation index and **FAO-reported country yield statistics**"* (Iizumi &
Sakai 2020, *Sci Data* 7:97, doi:10.1038/s41597-020-0433-7), so GDHY inherits
FAOSTAT's reporting basis. The GDHY `.nc4` files themselves carry **no
attributes at all** (verified: empty global attrs, unlabelled var `var`), so
the basis cannot be read off the file — it must come from this chain.
- FAO crop-production definitions: *"Production data for cereals are reported in
terms of clean, dry weight of grains with **12-14 percent moisture in the form
usually marketed**"*, and *"Area and production data on cereals relate to crops
harvested for dry grain only. Rice, however, is reported in terms of paddy."*
- => `w = 0.14` for maize in China (top of FAO's 12-14% band; also China's
GB 1353 maize trade-moisture ceiling). `w_range = 0.12-0.14` is the
sensitivity. The old `w = 0.155` is the **US grain-trade** convention, not
FAO's — keep it only as a sensitivity, never as the headline.
Before scoring:
1. **Ask the registry.** A product with no row returns
`basis: "undetermined", established: false` — then establish it from that
product's own documentation, cite it, and **add the row** so the next run
cannot silently re-decide.
2. Declare the basis of both sides in the run notes.
3. Convert once, either way — the PBIAS is identical:
`sim_market = sim_dry / (1 - w)` == `obs_dry = obs_market * (1 - w)`.
4. Report PBIAS on **both** bases regardless.
This is not cosmetic -- the undeclared choice (a dry-vs-as-published basis
MISMATCH, or `w=0.155`) can move the dag's *determining* metric (PBIAS) a lot
(EPIC_025). A determining metric must never be free to change on an
undocumented per-run judgement call — which is exactly why the basis now lives
in a cited registry rather than in the agent's head.
### Known residual magnitude bias (do NOT tune it away)
After the above, EPIC still reads high against a 0.5deg cell mean. The two known
causes are (a) `OPV5` plant population still at the umstead-NC legacy 10.0
plants/m2 vs ~6.0-7.5 for NE-China rainfed spring maize, and (b) a well-managed
single HLU compared against a cell mean that averages sub-optimal fields. Per
triplet **EPIC_022**, do NOT move PPOP to shift PBIAS -- there is no gridded
plant-density product on this server, and tuning it against the scored
observation would make the verdict circular.
## Capability inventory
EPIC supports a wide range of features beyond single-crop yield. Each MUST
have a tool:
| # | Capability | Tool |
|----|-----------------------------------------|-----------------------------------|
| 1 | Crop growth (150+ crop types in CROPCOM)| `tools/select_crop.py` |
| 2 | Weather forcing (CMFD/NASA POWER → EPIC)| `tools/convert_forcing_to_epic.py`|
| 3 | Soil profile (HWSD → .SOL) | `tools/build_soil_file.py` |
| 4 | Site topography (.SIT) | `tools/build_site_file.py` |
| 5 | Operations scheduling (.OPC) | `tools/build_opc_file.py` |
| 6 | Fertilization (FERT2012 + OPC ops) | `tools/build_opc_file.py` (fert op)|
| 7 | Tillage (TILLCOM + OPC ops) | `tools/build_opc_file.py` (till op)|
| 8 | Irrigation (auto or scheduled) | `tools/configure_irrigation.py` (`.SIT` IRR + `.OPC` OPV3 — **not** EPICCONT) |
| 9 | Pesticide fate (PESTCOM + OPC ops) | `tools/build_opc_file.py` (pest op)|
| 10 | Crop rotation (multi-year OPC) | `tools/build_opc_file.py` |
| 11 | Run control & list-COM files | `tools/build_control_files.py` |
| 12 | Execution wrapper (copy+run under Wine) | `tools/run_epic.py` |
| 13 | Output parsing (.ACY, .ANN → CSV) | `tools/parse_outputs.py` |
| 14 | Water-balance validation | `tools/parse_outputs.py` (validate)|
| 15 | Erosion / MUSLE / wind erosion outputs | `tools/parse_outputs.py` (columns)|
| 16 | GHG (N2O, CO2, denitrification) | `tools/parse_outputs.py` (columns)|
All tools follow validate → process → validate and import from
`ki_tools_common` where appropriate.
## Tool reference (short)
| Tool | Purpose |
|------------------------------|-------------------------------------------------|
| `convert_forcing_to_epic.py` | CMFD/NASA POWER → .DLY/.WP1/.WND |
| `build_site_file.py` | copy umstead.SIT + overwrite lat/lon/elev/slope |
| `build_soil_file.py` | copy umstead.SOL + overlay HWSD layer props |
| `build_opc_file.py` | copy umstead.OPC + rewrite plant/fert/harvest |
| `select_crop.py` | browse CROPCOM.DAT → crop code |
| `configure_irrigation.py` | set `.SIT` IRR method + `.OPC` OPV3 auto trigger |
| `build_control_files.py` | stage templates + rewrite EPICRUN.DAT |
| `run_epic.py` | copy-first Wine execution wrapper |
| `parse_outputs.py` | parse .ANN/.ACY → CSV + water balance check |
| `quickstart.py` | **one-command whole pipeline** from lat/lon/crop/years |
| `configure_control.py` | edit EPICCONT.DAT simulation-control flags |
| `edit_opc.py` | low-level .OPC row editor (add/replace single ops) |
| `set_output_types.py` | choose which daily/annual output files EPIC writes |
| `tune_parm.py` | edit PARM1102.DAT global model parameters |
### Start here for a NEW site: `quickstart.py`
`quickstart.py` chains every stage below (crop calendar → NPKGRIDS fertiliser
→ SPAM yield target → .SIT/.SOL/.OPC → forcing → control files → Wine run →
parse) from nothing but lat/lon/crop/years. Use it instead of driving the nine
stage tools by hand:
```bash
cd KISSPATH_KI_ROOT/EPIC/knowledge_infrastructure/tools
python3 quickstart.py --lat 44.25 --lon 125.25 --crop corn \
--start 1995 --end 2016 --source cmfd --irrigation dryland
```
Forcing extraction from the CMFD 3-hourly store costs **~80 s per simulated
year** at one point (7 variables × 12 monthly NetCDFs). `convert_forcing_to_epic.py`
memoises each year under `<workspace>/.forcing_cache`, so a re-run or a resumed
detached run skips years already extracted (override the location with
`$EPIC_FORCING_CACHE`). A multi-decade setup should therefore be launched
detached, not inline.
**CMFD store**: point the forcing at `Data_forcing_03hr_010deg` (3-hourly), NOT
`Data_forcing_01dy_010deg`. The daily store carries only a daily MEAN
temperature, so `TMX == TMN` reaches EPIC as a zero diurnal range — no error,
just wrong heat units and PET (triplet EPIC_023). `convert_forcing_to_epic.py`
now resolves the 3-hourly store (hc_ssd first, disk1 fallback) and raises if a
degenerate diurnal range is detected.
### Crop maturity — set PHU from the SITE, never from the template
`quickstart.py` now does this for you: it builds the forcing BEFORE the `.OPC`
and passes `phu=compute_phu(...)` plus the `n_cap` of triplet EPIC_018. Drive
the stage tools by hand only if you need to override those.
The PLANT operation's `OPV1` is the crop's **PHU** (Potential Heat Units) and
EPIC reports `HUSC = accumulated HU / PHU` on every operation line of the
`.OUT` log. `dag.yaml` requires **HUSC 0.9–1.2 at harvest** or the yields are
suspect. The shipped umstead value (1550, North Carolina) is wrong at most
sites, so derive PHU from the run's own weather **after** the forcing stage:
```python
from build_opc_file import build as build_opc, compute_phu, crop_base_temp
phu = compute_phu(f"{ws}/STN01.DLY", "05-04", "09-13", crop_base_temp("CORN"))
build_opc(name, "CORN", "05-04", "09-13", "09-13", "05-04", n, ws, phu=phu)
```
Check it afterwards: `grep -E "YLD=.*HUSC=" <run>.OUT`. See triplet EPIC_021.
### MANDATORY post-run check — the heat-unit index must reset every year
A multi-year rotation only behaves if EPIC **terminates** the crop at the end
of each season. Two greps, always, before you trust a yield series:
```bash
grep " HUI " <run>.OUT | head # January column MUST be 0.00 every year
grep "CSP2WD" <run>.OUT # HUSC at harvest must be 0.9-1.2
```
If January HUI drifts up year on year (`0.00, 0.11, 0.21, 0.29 …`) the crop is
never being killed and its heat units carry across the rotation boundary, so
HUSC at harvest reads `1 + residual` and yields are inflated. The cause is a
`.OPC` KILL row (equip 451) written with **crop_id = 0** instead of the crop's
CROPCOM code — the shipped `templates/umstead.OPC` kill row carries `2` (CORN).
`build_opc_file.py` had this bug and now writes `crop_code`; see triplet
EPIC_024. The failure is completely silent: EPIC runs, the `.ACY` looks
plausible, and the over-shoot masquerades as a PHU error — which invites the
wrong "raise the PHU" fix.
### Irrigation is TWO fields in TWO files — and neither is EPICCONT.DAT
An "irrigated" run that leaves `.ACY IRGA = 0.000` in every year is still a
DRYLAND run. EPIC's switch is:
| field | file | meaning |
|---|---|---|
| `IRR` | first integer of the flag line at the end of `<run>.SIT` | 0 dryland, 1 sprinkler, 2 furrow, 3 irrigation-with-fert, 4 automatic big-gun/lagoon, 5 drip (codes read off the `.OUT` MANAGEMENT DATA echo) |
| `OPV3` | the PLANTING row of `<run>.OPC` | automatic-irrigation trigger (plant water-stress factor, e.g. 0.85); **0 means "wait for scheduled ops"** |
`configure_irrigation.py` sets both, and `quickstart.py --irrigation furrow
--irrigation-trigger 0.85` forwards them. It must run AFTER `build_opc_file`,
which rewrites the `.OPC`. Verify every irrigated run:
```bash
grep -E "DRYLAND AGRICULTURE|IRRIGATION" <run>.OUT # method echo
awk 'NR>11{print $1, $4, $16}' <run>.ACY # YR YLDG IRGA
```
Until 2026-08-10 the tool wrote the flag into `EPICCONT.DAT` row 2, which holds
no irrigation field at all (verified by sweeping all 21 of its integers through
the binary), so **every irrigated run this KI ever produced was dryland** —
triplet **EPIC_032**, which supersedes item 3 of EPIC_019. EPIC_019's "at a
monsoon site irrigation cannot help" still holds *for monsoon sites*: check the
`.ACY WS` column first. Where WS shows many water-stress days, irrigation can
move the yield a lot (EPIC_032).
### Non-China sites: terrain and forcing
- `_lookup_terrain` resolves elevation/slope from the China DEM, then the
**global MERIT DEM** (`KISSPATH_DATA/MERIT_DEM`, 5°×5° tiles). Before
2026-08-10 the MERIT step did not exist and every non-China `.SIT` silently
carried 100 m / 1% slope (triplet **EPIC_031**).
- `--source nasa_power` is the practical global forcing (~3 s/simulated year vs
2-6 min for CMFD 3-hourly and >6 min for MSWX). Its **radiation record starts
1984-01-01** (temperature/precipitation start 1981), so a GDHY comparison
forced by NASA POWER is a 1984-2016 window (triplet **EPIC_030**).
- The per-year forcing memo used to drop every numpy-array variable, which
killed the whole NASA POWER path silently; delete a `.forcing_cache` written
before 2026-08-10 (triplet **EPIC_029**).
### Plant population (OPV5) is a site decision, not a default
`build_opc_file.build(..., ppop=)` sets the plant row's OPV5 (plants m-2,
echoed as the `.ACY` PPOP column). The default 10.0 is the umstead
North-Carolina legacy — the same site-blind class of value as `phu=1550` — and
it drives LAI, biomass and yield. Real densities are typically 6.0-7.5
plants m-2 for NE-China rainfed spring maize. No gridded planting-density
product exists on this server, so pass a sourced value when you have one;
never tune it against the observation you are scoring.
`quickstart.py` now forwards it too — `--ppop` / `quickstart(..., ppop=)`.
Before that it had no such parameter at all, so **every** quickstart run
silently inherited 10.0 plants m-2 (100,000 plants/ha) no matter what this
section said — a documented requirement with no way to satisfy it from the
recommended entry point. Omit the flag to keep the documented default; pass it
ONLY with a recorded source. Published NE-China spring-maize density treatments
sit at 30,000 / 52,500 / 75,000 / 97,500 plants ha-1 with an optimum near
76,000 plants ha-1, so 100,000 plants ha-1 is above every published treatment —
but the *average farmer density for a given cell-year* is not established on
this server, and any pick inside that range moves PBIAS a lot. Until a source
pins it, report a `--ppop` run as a labelled **sensitivity**, never as the
verdict (triplet EPIC_022).
### EPIC binary location
The executable is resolved by `tools/_common.resolve_binary()`, in order:
`$EPIC_BINARY` → `models/EPIC/bin/epic1102-official_release.exe` (the durable
install) → `models/EPIC/source/repo/` → the legacy `_work_v2` dissection
scratch path. Do not hard-code the scratch path — it is transient.
## Quick validation (fastest path — use this for KI testing)
```bash
cd KISSPATH_KI_ROOT/EPIC/knowledge_infrastructure
python3 quick_validate.py # uses existing umstead outputs
python3 quick_validate.py --rerun # force a fresh Wine execution
```
This runs the shipped umstead example (NC corn, 1930-1944, 15 years),
parses `.ANN` and `.ACY` outputs, checks water balance and yield ranges,
and prints JSON in the KI test format. Takes ~5 seconds with existing
outputs or ~30 seconds with `--rerun`.
## Quick-start example (reproducing shipped umstead run)
```bash
# copy the shipped template workspace somewhere writable, then:
cp KISSPATH_KI_ROOT/EPIC/bin/epic1102-official_release.exe .
wine ./epic1102-official_release.exe
ls umstead_0.* # .OUT .ACY .ANN .DGN .DCN ...
```
(The old `_work_v2/EPIC/epic_workspace` path in earlier revisions of this file
is a transient dissection scratch dir and no longer exists.)
## Water balance sanity check
After every run, parse `<name>_0.ANN` and confirm:
P (mm/yr) = ET (mm/yr) + Q (mm/yr) + ΔS
Note: Q in the `.ANN` file is **surface runoff only**. Deep percolation and
lateral subsurface flow are not included, so the residual P − ET − Q is
typically 200–600 mm/yr for humid sites. This is normal. An error is
indicated when ET exceeds P + IRGA (energy violation) or when values
are negative or zero.
## Reference KIs
- `KISSPATH_KI_ROOT/GLM/knowledge_infrastructure/` —
similar "closed-binary + fixed-format input" pattern
- Data KIs (unit verification):
(dataset KI dir names are UPPER-CASE on disk; the lower-case paths in
earlier revisions of this file do not resolve)
- `KISSPATH_DATA_KI/CMFD/SKILL.md`
- `KISSPATH_DATA_KI/NASA_POWER/SKILL.md`
- `KISSPATH_DATA_KI/HWSD/SKILL.md`
Forcing/soil/terrain LOADERS live in `ki_tools_common`
(`load_forcing`, `soil_utils`, `terrain`), not under `data_ki/*/tools/`.
## Validation Status
Run results (site, period, scores) are not kept here. They live in the run records
(`test_runs` in hydrocraft.db and each run's own result files).
This section restates `docs/validation_convention.yaml` for the dag rank-1
variable. The convention wins over remembered thresholds: all YLDG bands below
are null in the convention and therefore must be written as `no cited threshold`.
### Rank-1 Convention Bar
| dag variable | obs shape | Metric | Direction | Very good | Good | Satisfactory | Citation key from convention |
|---|---|---|---|---|---|---|---|
| `YLDG` | `point_time_series` | `pbias` | `zero_centered` | no cited threshold | no cited threshold | no cited threshold | none recorded |
| `YLDG` | `point_time_series` | `nse` | `maximize` | no cited threshold | no cited threshold | no cited threshold | none recorded |
| `YLDG` | `regional_aggregate_time_series` | `pbias` | `zero_centered` | no cited threshold | no cited threshold | no cited threshold | none recorded |
| `YLDG` | `point_snapshot` | `pbias` | `zero_centered` | no cited threshold | no cited threshold | no cited threshold | none recorded |
For `YLDG`, a metric value without an obs-shape-specific convention band is not
a pass/fail verdict. Do not transfer hydrology or sediment thresholds onto crop
yield; the convention records no cited threshold for these YLDG bands.
### KI Validation Result
| Check | Result recorded in this KI | Notes |
|---|---|---|
| Binary execution path | Analytic validation available through `quick_validate.py` | Runs the shipped `umstead` example with the actual `epic1102-official_release.exe` binary |
| Test case | `umstead` corn, NC, `1930-1944`, `15 years` | Existing example workspace/output parser path |
| Parsed outputs | `.ANN` and `.ACY` | Water-balance and yield-range checks are applied |
| Independent observed yield scoring | Pending | The YLDG convention has no cited pass-band thresholds, so the KI must not auto-validate yield against guessed bands |
## Validation tier
This KI has been validated at **tier = analytic**: the shipped
`umstead` example (corn, NC, 1930-1944, 15 years) is run unmodified
through the binary, annual `*.ANN` is parsed, and a water-balance check
is applied. Independent observed yield/ET/discharge comparisons would
upgrade this to `real` tier.
## Files in this KI
```
SKILL.md # this file
knowledge_infrastructure.yaml # machine-readable index
preflight_check.py # environment check
quick_validate.py # one-command KI validation
templates/ # shipped example workspace (copy-first!)
tools/ # copy-first Python tools (count: KI map)
docs/ # stage guides
diagnostics/triplets.yaml # diagnostic triplets (count: KI map)
```