Imported from lzwei196/KISS-Knowledge-Infrastructure-for-Scientific-Simulation (
models/EPIC/SKILL.md). Install upstream withnpx skills add lzwei196/KISS-Knowledge-Infrastructure-for-Scientific-Simulation --skill EPIC. Copyright stays with the author.
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:
- 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 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:
- Check triplets —
diagnostics/triplets.yamlmay already cover this error- Read official docs — Check the model's own documentation (PDF manual, README, official examples) for expected input formats, variable names, and units
- Find working examples — Look in
templates/for the shippedepic1102_example_files_20221002workspace, or inoutputs/for previous successful runs- 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 — 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>.
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.
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.
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:
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:
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, both verified at Changchun
(GDHY 44.25N 125.25E maize, 1997-2016, n=20):
- Technology trend. The obs rises +0.163 t/ha/yr (2.66%/yr of mean; 1997-2006
mean 5.25 -> 2007-2016 mean 6.97 t/ha). That is cultivar turnover, input
intensification and mechanisation -- a fixed-management, fixed-cultivar,
fixed-PPOP EPIC run has no mechanism for it (sim slope +0.003 t/ha/yr,
slope_ratio 0.02). Raw
rcollapsed to 0.21; detrended r = 0.38 and first-difference r = 0.59 -- the weather signal is there once the trend is removed. - Variance damping. A 0.5deg cell mean averages hundreds of heterogeneous
fields, so its inter-annual variance is smaller than any single field's.
Detrended sd was 0.55 t/ha (obs) vs 1.07 t/ha (sim).
NSE/KGEpenalise 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:
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
.nc4files themselves carry no attributes at all (verified: empty global attrs, unlabelled varvar), 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.14for maize in China (top of FAO's 12-14% band; also China's GB 1353 maize trade-moisture ceiling).w_range = 0.12-0.14is the sensitivity. The oldw = 0.155is the US grain-trade convention, not FAO's — keep it only as a sensitivity, never as the headline.
Before scoring:
- 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. - Declare the basis of both sides in the run notes.
- Convert once, either way — the PBIAS is identical:
sim_market = sim_dry / (1 - w)==obs_dry = obs_market * (1 - w). - Report PBIAS on both bases regardless.
This is not cosmetic -- at Changchun the undeclared choice moved the dag's
determining metric from PBIAS +26.8% (dry-vs-as-published, i.e. a basis
MISMATCH) to +50.1% (w=0.155). A determining metric must never be free to
double 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:
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:
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:
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:
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. At Po Valley (45.25N 9.75E) WS is 25-35 stress days and
furrow irrigation moves 1984 maize from 7.70 to 10.25 t/ha dry (IRGA 200 mm).
Non-China sites: terrain and forcing
_lookup_terrainresolves 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.SITsilently carried 100 m / 1% slope (triplet EPIC_031).--source nasa_poweris 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_cachewritten 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)
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)
# 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.mdKISSPATH_DATA_KI/NASA_POWER/SKILL.mdKISSPATH_DATA_KI/HWSD/SKILL.md
Forcing/soil/terrain LOADERS live in
ki_tools_common(load_forcing,soil_utils,terrain), not underdata_ki/*/tools/.
Validated Results
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/ # 9 copy-first Python tools
docs/ # 7 stage guides
diagnostics/triplets.yaml # 17 symptom/diagnosis/remedy entries