DSSAT · git:20261002.79c2625 · 2026-10-02 · sha256 84897b2f3d58c6d4

DSSAT git:20261002.79c2625C

Immutable. This exact content never changes and is served at /api/v1/blob/84897b2f3d58c6d4.

> **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/` (19 tools) | the executable pipeline. Read each tool's argparse (`--help`) before composing a command; SKILL.md's stage table says which tool serves which stage. |
| before running a stage | `docs/s*_*.md` (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` (56 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` (27 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/dssat_workdir_setup.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/dssat_workdir_setup.py --help` |
| `tools/parse_plantgro.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/parse_plantgro.py --help` |
| `tools/s10_calibrate_rice_nsga2.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s10_calibrate_rice_nsga2.py --help` |
| `tools/s1_experiment_design/validate_filex_structure.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s1_experiment_design/validate_filex_structure.py --help` |
| `tools/s2_weather_prep/calculate_tav_amp.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_weather_prep/calculate_tav_amp.py --help` |
| `tools/s2_weather_prep/convert_cmfd_to_wth.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_weather_prep/convert_cmfd_to_wth.py --help` |
| `tools/s2_weather_prep/extract_cmfd_points.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_weather_prep/extract_cmfd_points.py --help` |
| `tools/s2_weather_prep/validate_weather_file.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s2_weather_prep/validate_weather_file.py --help` |
| `tools/s3_soil_setup/convert_hwsd_to_sol.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s3_soil_setup/convert_hwsd_to_sol.py --help` |
| `tools/s3_soil_setup/convert_soilgrids_to_sol.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s3_soil_setup/convert_soilgrids_to_sol.py --help` |
| `tools/s3_soil_setup/validate_soil_profile.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s3_soil_setup/validate_soil_profile.py --help` |
| `tools/s8_batch_execution/create_batch_file.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s8_batch_execution/create_batch_file.py --help` |
| `tools/s8_batch_execution/run_grid_ensemble.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s8_batch_execution/run_grid_ensemble.py --help` |
| `tools/s9_output_parsing/aggregate_grid_to_pixel.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s9_output_parsing/aggregate_grid_to_pixel.py --help` |
| `tools/s9_output_parsing/parse_summary_out.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s9_output_parsing/parse_summary_out.py --help` |
| `tools/s9_output_parsing/validate_yield_timeseries.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/s9_output_parsing/validate_yield_timeseries.py --help` |
| `tools/setup_rice_experiment.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/setup_rice_experiment.py --help` |
| `tools/setup_soybean_experiment.py` | `KISSPATH_PYTHON_ENV/bin/python {KI}/tools/setup_soybean_experiment.py --help` |

*19 public tools; `_`-prefixed helpers and packaging files excluded.*
<!-- KI-TOOL-INDEX:END -->

---

## 3. Input Requirements

Exact machine-readable input and output shapes live in `docs/format_spec.yaml`.
That file is projected from `dag.yaml` and `diagnostics/triplets.yaml`; regenerate
it after changing either source, and do not hand-edit the projected spec.

## Data Preparation

### Forcing data

**Data Sources**: Use `from ki_tools_common.load_forcing import load_daily_forcing` for CMFD/MSWX/NASA POWER.
Then convert to DSSAT .WTH format using this KI's tool: `tools/s2_weather_prep/convert_cmfd_to_wth.py`

### Soil properties

**Data Sources**: Use `from ki_tools_common.soil_utils import lookup_hwsd` for soil properties.

**Data Validation Reference**: See `data_ki/CMFD/SKILL.md` for CMFD unit documentation and known traps.
See `data_ki/HWSD/SKILL.md` for soil property documentation.

---

# DSSAT-CSM v4.8.5 — Knowledge Infrastructure

**Model**: DSSAT-CSM (Decision Support System for Agrotechnology Transfer, Cropping System Model)
**Version**: 4.8.5, Build 41
**Domain**: Crop simulation — multi-crop growth, development, and yield
**Language**: Fortran 90
**Binary**: `KISSPATH_HOME/DSSAT/build/bin/dscsm048`
**License**: Research/non-commercial
**Repository**: https://github.com/DSSAT/dssat-csm-os

---

## 1. Model Identity

| Property | Value |
|----------|-------|
| Full name | DSSAT-CSM (Decision Support System for Agrotechnology Transfer, Cropping System Model) |
| Version | 4.8.5, Build 41 |
| Language | Fortran 90 |
| License | Research/non-commercial |
| Repository | https://github.com/DSSAT/dssat-csm-os |
| Primary domain | Crop simulation — multi-crop growth, development, and yield |
| Spatial mode | Point / site experiment; gridded applications use wrapper scripts |

## 2. What This Model Does

## Overview

DSSAT simulates crop growth, development, and yield for 24+ species (wheat, maize, rice, soybean,
sorghum, millet, cotton, etc.) under varying weather, soil, and management conditions. It uses
process-based physiology (photosynthesis, phenology, soil water balance, N/P dynamics) driven by
daily weather data and management schedules.

**Key advantage**: Most detailed crop model in HydroCraft — 50+ diagnostic triplets, 9 pipeline
stages, Chinese cultivar library with 42 calibrated varieties.

## 6. Output Description

**Source of truth**: `dag.yaml`. The dag is the model identity for outputs:
each observable output's `var`, unit, description, validation rank and
observability live there. This section restates the dag facts already extracted
for the reader; if this section and `dag.yaml` ever disagree, `dag.yaml` wins.

**Headline output** (the dag's `validation_rank: 1` variable — the one this model
is judged by):

> `HWAM (harvested grain yield at maturity, dry)` — Dry grain/seed yield at physiological maturity per treatment - the canonical end-of-season yield variable. (`kg/ha`)

| Output variable (dag `var`) | rank | Unit | Description |
|-----------------------------|------|------|-------------|
| HWAM (harvested grain yield at maturity, dry) | 1 | kg/ha | Dry grain/seed yield at physiological maturity per treatment - the canonical end-of-season yield variable. |

Other dag outputs named for this KI:

| Output variable (dag `var`) |
|-----------------------------|
| HWAH (harvested yield, dry, at harvest date) |
| CWAM (aboveground biomass at maturity) |
| HIAM (harvest index at maturity) |
| ADAT (anthesis date) |
| MDAT (physiological maturity date) |
| EDAT (emergence date) |
| LAIX (maximum LAI during season) |
| LAID / XLAI (daily leaf area index) |
| CWAD (daily aboveground dry weight) |
| GWAD (daily grain dry weight) |
| ETCM (cumulative evapotranspiration) |
| ET (daily actual evapotranspiration) |
| SW (daily volumetric soil water by layer) |
| ROCM (cumulative runoff) |
| DRCM (cumulative deep drainage) |
| NLCM (cumulative N leached) |
| NUCM (cumulative crop N uptake) |
| NFXM (cumulative N fixed) |
| GNAM (grain N content at maturity) |
| N2OEM (cumulative N2O-N emissions) |
| CH4EM (cumulative CH4-C emissions, paddy) |

---

## 4. Build Instructions

DSSAT-CSM is already built for this KI. Verify the local binary/package/data
before any run:

```bash
python preflight_check.py
```

If the binary must be invoked directly, use the existing local build:
`KISSPATH_HOME/DSSAT/build/bin/dscsm048`.

## 5. Execution

## Binary Location & Execution

**ALWAYS use `dssat_workdir_setup.py` — NEVER set up DSSAT files manually.**

```python
import sys
sys.path.insert(0, "KISSPATH_KI_ROOT/DSSAT/knowledge_infrastructure/tools")
from dssat_workdir_setup import create_workdir, run_dssat, parse_summary

# Step 1: Create workdir (handles all 6 Fortran pitfalls automatically)
workdir = create_workdir(
    crop="MZ",                    # MZ=maize, WH=wheat, SB=soybean, RI=rice
    cultivar="IB0001",            # From genotype library (see Chinese Cultivar Library below)
    weather_file="path/to.WTH",   # DSSAT .WTH format weather file
    soil_id="IBMZ910014",         # DSSAT soil profile ID
    lat=32.625, lon=116.375,
    planting_date=80150,           # YYDDD format (year 80, DOY 150) or "YYYY-MM-DD"
    start_year=1980, end_year=1985,
    output_dir="/tmp/dssat_run",
)

# Step 2: Run DSSAT
result = run_dssat(workdir)
print(f"Success: {result['success']}")

# Step 3: Parse results
summary = parse_summary(workdir)
for rec in summary:
    print(f"Yield: {rec['HWAM']} kg/ha, Biomass: {rec['CWAM']} kg/ha")
```

### What `dssat_workdir_setup.py` handles (6 known pitfalls):

| Pitfall | What happens without it | The utility's fix |
|---------|------------------------|-------------------|
| Fortran path truncation | Paths >72 chars cause FOTEFX error | Creates workdir under /tmp/ with short paths |
| Missing .CDE files | MODEL.ERR / crash | Symlinks all CDE+SDA files from DSSAT/Data/ |
| Weather station mismatch | Simulation uses wrong weather | Derives 4-char WSTA code from filename |
| CUL column alignment | Wrong cultivar coefficients | Fixed-width format with exact column positions |
| DSSATPRO paths | Old paths point to wrong dirs | Regenerates with workdir paths |
| Summary.OUT parsing | Manual parsing misses edge cases | Structured dict with date conversion |

**Raw binary** (only if you know what you're doing):
```bash
cd /path/to/working_directory
KISSPATH_HOME/DSSAT/build/bin/dscsm048 B DSSBatch.v48
```

---

## 7. Tool Inventory

## Pipeline Stages

| Order | Stage | Skill Document | Key Tools |
|-------|-------|---------------|-----------|
| 1 | Experiment Design | [s1_experiment_design_skill.md](docs/s1_experiment_design_skill.md) | FileX template generation |
| 2 | Weather Prep | [s2_weather_prep_skill.md](docs/s2_weather_prep_skill.md) | CMFD/MSWX → .WTH conversion |
| 3 | Soil Setup | [s3_soil_setup_skill.md](docs/s3_soil_setup_skill.md) | HWSD → SOIL.SOL conversion; SoilGrids → SOIL.SOL (6-layer, global) |
| 4 | Genotype Config | [s4_genotype_config_skill.md](docs/s4_genotype_config_skill.md) | Cultivar selection from library |
| 5 | Simulation Controls | [s5_simulation_controls_skill.md](docs/s5_simulation_controls_skill.md) | DSSAT48.INP, DSSBATCH.v48 |
| 6 | Initial Conditions | [s6_initial_conditions_skill.md](docs/s6_initial_conditions_skill.md) | Soil water/N initialization |
| 7 | Management Spec | [s7_management_spec_skill.md](docs/s7_management_spec_skill.md) | Planting, fertilizer, irrigation |
| 8 | Batch Execution | [s8_batch_execution_skill.md](docs/s8_batch_execution_skill.md) | Run dscsm048 |
| 9 | Output Parsing | [s9_output_parsing_skill.md](docs/s9_output_parsing_skill.md) | Parse Summary.OUT, PlantGro.OUT |

**Additional tools**: `convert_cmfd_to_wth.py` (CMFD → .WTH conversion), `convert_hwsd_to_sol.py` (HWSD → SOIL.SOL, 2-layer), and `convert_soilgrids_to_sol.py` (SoilGrids → SOIL.SOL, 6-layer, global coverage — **preferred for new simulations**).

**Soil tool selection guide:**
| Situation | Tool |
|---|---|
| China, quick setup | `convert_hwsd_to_sol.py` |
| Any region, best accuracy | `convert_soilgrids_to_sol.py` ← preferred |
| NE China (Mollisol zone) | `convert_soilgrids_to_sol.py` — includes BD correction |

`convert_soilgrids_to_sol.py` advantages: 6 depth layers (vs 2), Saxton-Rawls 2006 PTF (vs 1986), direct bdod from SoilGrids, depth-varying SOC/pH, Mollisol BD correction for NE China black soils. Uses `KISSPATH_DATA/soilgrids_global/` (global) with China-optimised fallback to `KISSPATH_DATA/soilgrids/`.

**Stages 1-7 can run in parallel** — they each write a section of the FileX or prepare external files.
Stage 8 depends on all of 1-7. Stage 9 depends on 8.

**Stage validators are now CLI-invocable** (they used to require editing a module-level
global, which is why running them straight from the shell exited 1):
```bash
python tools/s1_experiment_design/validate_filex_structure.py  /path/EXP0001.MZX
python tools/s2_weather_prep/validate_weather_file.py          /path/SITE0001.WTH
python tools/s3_soil_setup/validate_soil_profile.py            /path/SOIL.SOL
```

### Stage 7 — management is a `create_workdir(**management_kwargs)` dict (NO tools/s7_* files)

`tools/s4..s7` are EMPTY directories; every management decision is a keyword to
`create_workdir()`. Source them from data, not from the defaults:

| Key | Meaning | Where to get it |
|---|---|---|
| `fert_n`, `fert_date_offset` | total N kg/ha, split 40% at planting / 60% at planting + offset days; **offset 0 = everything at planting in one row** (wheat and soybean defaults are 0) | rate: `ki_tools_common.fertilizer.get_fertilizer_rates` (NPKGRIDS v1.08); timing: `ki_tools_common.fertilizer.get_schedule` (declared rule, weather-driven) and pass the result as `fertilizer_events` |
| `fertilizer_events` | `[{'yyddd'\|'date'\|'dap': ..., 'n_kgha': ..., 'fmcd'?, 'facd'?, 'depth_cm'?, 'name'?}, ...]` or `[(yyddd, n_kgha), ...]` — the user's own dated N rows (kg N/ha, not product; kept to 0.1 kg), one FERTILIZERS row each, sorted by calendar date; dates must be valid, on/after the simulation start and within the simulated years (pre-plant is fine); an EMPTY list = no application; overrides `fert_n`/`fert_date_offset`; the writer reads its own rows back and refuses if they do not sum to the request | field records, or `ki_tools_common.fertilizer.get_schedule(...)` (needs daily weather for the weather-driven timing, otherwise it labels a calendar fallback): pass each event's `days_after_planting` (accepted as `dap`) and `n_kgha`, and map `material` to an FMCD code (`urea` -> `FE005`). A row dated outside the simulated window is skipped by DSSAT without any error, so check `NICM` in Summary.OUT against the request after the run |
| `planting_date` (YYDDD) | sowing | `ki_tools_common.crop_calendar.get_planting_harvest` — but in the Huang-Huai belt (32–35N) the GGCMI *global* maize calendar returns late May; maize there is the SUMMER crop after winter wheat, so clamp to DOY ≈ 165 (see China crop calendar below) |
| `ppop`, `plrs` | plants/m², row spacing cm | China maize: ~6.0 plants/m² at **60 cm** rows, not the 76 cm US default |
| `irrigation` | `"rainfed"` (default, non-rice) / `"auto"` / `"reported"` | set `"auto"` for any IRRIGATED site |
| `irr_ithrl`, `irr_amt`, `irr_imdep`, `irr_eff` | automatic-irrigation trigger %, mm applied, depth cm, efficiency | NCP supplemental: `irr_ithrl=40, irr_amt=40` (rice paddy default is 80/50) |
| `irrigation_events` | `[(yyddd, mm), ...]` explicit dated events → `IRRIG=R`, `MI=1` | single-season runs only — a multi-season (`NYERS>1`) run needs `irrigation="auto"`, since fixed calendar dates exist for the first season only |

> **Trap (fixed 2026-09-24):** the writer wrote the sidedress row only when `fert_date_offset > 0`. Wheat's
> default offset is 0, so every wheat run that relied on the defaults (or passed `fert_n` without an offset)
> silently received **40 % of the requested N**.
> Now offset 0 writes one row with the whole amount, `fertilizer_events` is the door for real schedules, the
> constants are declared at the top of the writer (`FERT_SPLIT_AT_PLANTING`, `FERT_CODE`, `FERT_APPL`,
> `FERT_DEPTH_CM`, `FERT_MAX_EVENTS`), and the writer refuses to continue if the rows it wrote do not sum to the
> requested N. The writer now REFUSES (ValueError) instead of silently under-applying: a negative
> `fert_date_offset`; a sidedress date after the last simulated year; more than 9,000 events (DSSAT's NAPPL,
> the rest were dropped silently); an invalid or fractional date; a positive N amount on a fertiliser code that
> carries no nitrogen in `FERCH048.SDA` (e.g. FE014 triple superphosphate: DSSAT applies nothing); an amount that
> does not fit the 5-character FAMN field; a line break in an event's text; an application method DSSAT does not
> place (`FERT_SUPPORTED_METHODS`, from `Fert_Place.for`); an incorporated event deeper than the soil profile, or any
> incorporated event when the profile depth cannot be read from the `.SOL` for this `soil_id` (DSSAT loses N below the
> profile, `Fert_Place.for:761`). Positive-offset output is
> byte-identical to the old writer for amounts below 1000 kg.
> Note: DSSAT's automatic fertiliser mode (`IFERI='A'`, AUTOMATIC MANAGEMENT NITROGEN row) is dead code in
> this build (`Fert_Place.for:568-612` commented out): it passes validation and applies nothing. Do not use it.

> **Trap (fixed 2026-08-10):** before this, `irrigation_events` was accepted, documented
> and then **silently discarded**, and `IRRIG` was hard-wired to `N` for every non-rice
> crop — an irrigated site ran as dryland with no error. Also, `fert_n` from NPKGRIDS is a
> **float** and used to crash FileX generation; and `convert_cmfd_to_wth.py` threw away a
> completed multi-minute extraction if `--output`'s directory did not exist yet.

### GDHY (`--obs gdhy`) = `point_time_series` — expect a large POSITIVE PBIAS

`validate_yield_timeseries.py --obs gdhy` tags `point_time_series`
(valid families `[temporal_pattern_match, magnitude_accuracy, ranking_quality]`,
determining metric `pbias`), so NSE/KGE/r/PBIAS are ALL gate-valid — unlike the FAOSTAT
case below. It also writes `scored_series.csv` (`date,obs,sim`) and passes `dates=` to
`all_metrics`, so the metric is re-derivable from the evidence.

**Interpret the magnitude before you retune anything:** DSSAT HWAM is one well-managed,
pest- and weed-free field at **attainable** yield; a GDHY 0.5° pixel is an **area-average
actual** yield over ~2 500 km² including marginal land, pests, weeds and harvest losses.
The Global Yield Gap Atlas puts Chinese maize actual/attainable at ≈0.6, so a
**+40…60 % PBIAS at a Chinese maize cell is the structural yield gap, not a chain bug** —
no cultivar/forcing/soil switch removes it (the same gap produced the +297 % São Paulo
row in the 3×3 table). The DSSAT-sanctioned way to represent an area average is
`SLPF < 1`; this KI ships it as `convert_soilgrids_to_sol.py --soc_slpf` (≈0.80 for a
20 g/kg-SOC topsoil), but **`SLPF = 1.00` is the declared baseline and `--soc_slpf` is a
named sensitivity test** — do not flip it just to move PBIAS, and say which you used.
Also match the *period* to the cultivar: GDHY runs 1981–2016, but the dag's
`point_time_series` caveat requires **consistent cultivar/management**, so a 2000-released
hybrid (e.g. CN0001 Zhengdan958) + present-day NPKGRIDS N rates should be scored from
2000 onward, not against 1981–99 farming.

---

## 8. Unit Conversion Table

| Variable | Source unit (CMFD) | DSSAT expected | Conversion | Trap if wrong |
|----------|--------------------|----------------|------------|---------------|
| Temperature | K | °C | -273.15 | Crop never matures |
| Precipitation | mm/3hr | mm/day | Sum 8 timesteps | Wrong water balance |
| Solar radiation | W/m² | MJ/m²/day | ×0.0864 | Zero or excess growth |
| Wind speed | m/s | km/day | ×86.4 | Wrong ET |
| Precipitation | kg/m²/s (CMFD raw) | mm/day | ×86400 | CMFD 3-hourly files store mm/3hr directly — sum 8 steps |

---

## Chinese Cultivar Library

Location: `KISSPATH_HOME/DSSAT/Data/Genotype/China/`

| Crop | File | Cultivars | Key varieties | Setup Tool |
|------|------|-----------|---------------|------------|
| Wheat | WHCER048_China.CUL | 17 | Zhengmai9023, Jimai22, Yangmai158 | dssat_workdir_setup.py (crop="WH") |
| Maize | MZCER048_China.CUL | 12 | Zhengdan958, Xianyu335, Denghai605 | dssat_workdir_setup.py (crop="MZ") |
| Rice | RICER048_China.CUL | 11 | YZR Hybrid Shanyou, NEC Japonica | **setup_rice_experiment.py** |
| Soybean | SBGRO048_China.CUL | 7 | NEC MG I, HHH MG III | **setup_soybean_experiment.py** |

---

## 9. Diagnostic Triplets (50 entries)

See `diagnostics/triplets.yaml` for the full set. Top 5 most common:

| # | Symptom | Root Cause | Fix |
|---|---------|------------|-----|
| 1 | MODEL.ERR not found | Binary not in workdir | Copy dscsm048 into working directory |
| 2 | Zero yield | Wrong solar radiation unit (W/m² not MJ/m²/d) | Multiply by 0.0864 |
| 3 | Crop never matures | Temperature in K not °C | Subtract 273.15 |
| 4 | FOTEFX error | Path >72 chars (Fortran limit) | Use shorter paths |
| 5 | NL exceeded | FileX has >500 treatments | Split into multiple experiments |

---

## 11. Validated Results

### Performance Metrics — judged against the field's bar, not intuition

State bars from `docs/validation_convention.yaml`; do not substitute remembered
thresholds. For the dag's rank-1 variable, `HWAM (harvested grain yield at
maturity, dry)`, the convention bars extracted for this KI are:

| dag variable | Metric | Direction | Very good | Good | Satisfactory |
|--------------|--------|-----------|-----------|------|--------------|
| HWAM (harvested grain yield at maturity, dry) | pbias | zero_centered | 10 (jamieson1991; chisanga2021; kumar2024) | 20 (jamieson1991; chisanga2021; kumar2024) | 30 (jamieson1991; chisanga2021; kumar2024) |
| HWAM (harvested grain yield at maturity, dry) | nrmse | minimize | 10 (jamieson1991; chisanga2021; kumar2024) | 20 (jamieson1991; chisanga2021; kumar2024) | 30 (jamieson1991; chisanga2021; kumar2024) |
| HWAM (harvested grain yield at maturity, dry) | nrmse | minimize | 10 (jamieson1991; chisanga2021; kumar2024) | 20 (jamieson1991; chisanga2021; kumar2024) | 30 (jamieson1991; chisanga2021; kumar2024) |

The duplicate `nrmse` convention row is intentionally restated as extracted
from the KI facts above. If the convention file is wrong, report it rather than
editing it from this skill document.

| Basin | Crop | Yield (kg/ha) | Reference | Period |
|-------|------|---------------|-----------|--------|
| Bengbu | Maize | 5,324 | SPAM 2020 (-2.3%) | 2010 |
| Bengbu | Wheat | 4,876 | SPAM 2020 (+1.8%) | 2010 |
| Blue Nile (Ethiopia) | Wheat | 1,482 | Uncalibrated | 2005-2015 |
| Blue Nile (Ethiopia) | Maize | 4,810 | Uncalibrated | 2005-2015 |

### Temporal yield validation — Pearson r / NSE / KGE require ≥2 simulation years

The single-year SPAM scalar (`crop_obs.get_observed_yield`) gives ONE (sim,obs) pair,
and `ki_tools_common/metrics.py` returns NaN for <2 pairs, so **r/nse/kge are
structurally undefined** and the orchestrator's `r<0.5` gate retries forever.
To get a DEFINED r, the DSSAT run must span **≥2 simulation years** (set `NYERS`/
`start_year..end_year` so `Summary.OUT` has ≥2 WYEAR rows), then pair each year's
HWAM against the multi-year GDHY series via
`tools/s9_output_parsing/validate_yield_timeseries.py`
(`--workdir <wd> --lat <Y> --lon <X> --crop maize`). It writes
`validation_metrics.json` with `{nse,kge,r,pbias}`. A single-year FileX still yields
one pair → tool exits rc=2 with a warning. Use single-year SPAM only for
%bias/nRMSE point checks (the 3×3 table below).

> **OBS-SCALE RULE for the `r<0.5` gate** — the default `--obs gdhy` is the
> ONLY obs valid for the interannual r/NSE/KGE gate at a single site. GDHY is a
> 0.5° gridded yield product, so its pixel is **co-located and scale-matched**
> to the simulated point. Do **NOT** use `--obs faostat` for the r-gate: FAOSTAT
> is a **national aggregate** dominated by a smooth technology/fertilizer trend
> and by the production-weighted centroid (NE China + Henan/Shandong North China
> Plain). A single non-representative point (e.g. Bengbu, humid monsoon S. Anhui,
> 32.9N/117.4E) has ~4–5× the national interannual variance and can even
> **anti-correlate** with the national series — Bengbu maize 2005–2020 gave
> PBIAS +8.94% (good mean bias) but r = −0.083 / NSE −12.47 against
> `China, mainland`. The same national series tracks at r≈0.80 when the sim sits
> on a *production-representative* point (AquaCrop at Henan 34.0N/113.0E), so the
> failure is **obs location/scale**, not model physics. FAOSTAT national is
> **%bias-only** for a single point; for the r-gate either (a) re-run with
> `--obs gdhy` at the simulated pixel [recommended, runnable today: GDHY maize
> covers 2005–2016 on disk], or (b) build a multi-site ensemble at the national
> production centroid before comparing to the aggregate. FAOSTAT has no
> sub-national `Area`, so provincial (Anhui) is not available as a middle ground.

> **FAOSTAT IS NOT A FAILURE CASE — it has a VALID metric family; do not chase
> the r-gate (verified 2026-06-15).** `validate_yield_timeseries.py --obs faostat`
> classifies the comparison as `obs_shape: regional_aggregate_time_series` (see
> `dag.yaml outputs.HWAM.observability`), whose valid families are
> **`[magnitude_accuracy, trend_match]`** — NOT `temporal_pattern_match`. The tool
> therefore emits `pbias` (magnitude_accuracy) + `sim_slope_kgha_yr/obs_slope_kgha_yr/
> trend_sign_agree` (trend_match) and **nulls `nse/kge/r`** (kept under
> `_raw_invalid_*` only). For Bengbu maize 2005–2020 vs `China, mainland` this is a
> **PASS**: PBIAS +8.94% (good mean bias) and `trend_sign_agree: true` (both rising).
> The dag-driven gate scores on PBIAS + trend-sign, NOT on the raw `_raw_invalid_r`
> (−0.08) / `_raw_invalid_nse` (−12.5) — those are structurally intrinsic to a
> weather-only point process simulating a smooth, technology-trending national
> aggregate, and switching cultivar/forcing/parameters CANNOT rescue them. Do not
> re-run with `--obs gdhy` merely to satisfy an r<0.5 gate that does not apply to
> this obs_shape; only switch to GDHY if the *task* genuinely asks for the
> `point_time_series` interannual-pattern comparison. The slope_ratio (~0.19, sim
> captures ~1/5 of the national trend) reflects fixed cultivar+management across all
> years — it is a documented limitation, not a tool bug.

### 3×3 Validation Results (2026-04-10)

| Site | Crop | Simulated | SPAM Ref | Bias | Cultivar |
|------|------|-----------|----------|------|----------|
| Bengbu | Maize | 5,937 ± 911 | 5,652 | +5.0% | CN0001 (Zhengdan958) |
| Bengbu | Wheat | 3,987 ± 606 | 6,144 | -35.1% | CN0102 (Jimai22) — 1980s vs 2020 obs |
| Harbin | Maize | 6,027 ± 3,080 | 8,219 | -26.7% | CN0018 (NEC Spring Medium) |
| Harbin | Wheat | 321 ± 353 | 4,170 | -92.3% | FAILED — no spring wheat cultivar |
| São Paulo | Maize | 14,690 ± 1,520 | 3,701 | +297% | IB0035 (US default, uncalibrated) |
| São Paulo | Soybean | 1,977 ± 256 | 3,400 | -41.9% | IB0011 (MG0, needs MG VII-VIII) |

### Known Issues (from validation)

| Issue | Detail | Fix needed |
|-------|--------|-----------|
| **CMFD precip unit** | `convert_cmfd_to_wth.py` assumed mm/3hr but CMFD is kg/m²/s | **Fixed** — now ×10800 per timestep |
| **No spring wheat** | Chinese cultivar library lacks P1V=0 spring wheat | Calibrate NEC spring wheat (P1V=0, P1D~20) |
| **No Brazil cultivars** | US maize default +297% bias in tropics | Need Brazil-calibrated maize/soy library |
| **China CUL not auto-loaded** | `create_workdir()` doesn't merge China/ CUL files | Manually append after workdir creation |

---

## 10. Coupling Interfaces

DSSAT can run as a standalone site model or be coupled through prepared forcing,
soil and management files. Upstream weather and soil data are converted into
DSSAT-ready `.WTH` and `SOIL.SOL` files; downstream consumers use parsed
`Summary.OUT` and `PlantGro.OUT` variables such as `HWAM`, `CWAM`, `LAID`,
`CWAD`, `GWAD`, `ET`, `SW`, runoff and drainage.

| Upstream component | Variable exchanged | Unit | Temporal resolution |
|--------------------|-------------------|------|---------------------|
| CMFD/MSWX/NASA POWER forcing | SRAD, TMAX, TMIN, RAIN, WIND | DSSAT weather units after conversion | Daily |
| HWSD/SoilGrids soil data | Soil profile properties | DSSAT SOIL.SOL units | Static profile |
| Crop calendar / management data | Planting, fertilizer, irrigation, population, row spacing | DSSAT FileX controls | Per season / event |

| Downstream component | Variable exchanged | Unit | Temporal resolution |
|----------------------|-------------------|------|---------------------|
| Validation tools | HWAM (harvested grain yield at maturity, dry) | kg/ha | Per treatment / season |
| Growth analysis | LAID, CWAD, GWAD and related PlantGro variables | DSSAT output units | Daily |
| Hydrology / water-balance post-processing | ET, SW, ROCM, DRCM and related outputs | DSSAT output units | Daily or cumulative |

## Standalone Workflow (WITHOUT VIC — for single-site or farmer queries)

DSSAT can run **independently** without any hydrological model. Use this workflow when the user
asks about crop yield, planting decisions, or cultivar comparisons — NOT the VIC 10-step workflow.

### Step 1: Get weather data

```python
# Use NASA POWER for any location worldwide (no local data needed)
python skills/vic-auto-run/s2_forcing/forcing_nasa_power.py \
  --lat 45.5 --lon -73.6 --start_year 2005 --end_year 2023 \
  --output_dir outputs/<run>/weather/
# Then convert to DSSAT .WTH format using the forcing-converter skill
```

Or, for China sites, convert CMFD directly to DSSAT `.WTH` with this KI's
validated S2 tool (the canonical weather recipe — there is **no**
`tools/fetch_weather.py`):
```bash
python tools/s2_weather_prep/convert_cmfd_to_wth.py \
  --forcing_dir KISSPATH_FORCING/Data_forcing_03hr_010deg/ \
  --lat 32.9 --lon 117.4 --start_year 2005 --end_year 2020 \
  --output /tmp/run/weather/SITE.WTH --station_name SITE
```
Notes: (1) run this in a conda env with a working NetCDF backend (e.g.
`KISSPATH_HOME/miniconda3/envs/ohq/bin/python`) — the tool now falls back
h5netcdf→netcdf4→scipy automatically, but the default `lisflood` env has a
broken numpy/netCDF4 binary. (2) Point extraction over the 3-hourly store is
I/O-bound (~8–10 min for 16 years). For global sites, load via
`ki_tools_common.load_forcing.load_daily_forcing` and write the same `.WTH`
columns (DATE/SRAD/TMAX/TMIN/RAIN/WIND).

### Step 2: Set up and run using dssat_workdir_setup.py (MANDATORY)

```python
import sys
sys.path.insert(0, "KISSPATH_KI_ROOT/DSSAT/knowledge_infrastructure/tools")
from dssat_workdir_setup import create_workdir, run_dssat, parse_summary

workdir = create_workdir(
    crop="MZ",                    # MZ=maize, WH=wheat, SB=soybean
    cultivar="IB0001",            # From cultivar library (see below)
    weather_file="path/to/MTRL0501.WTH",
    soil_id="IBMZ910014",
    lat=45.5, lon=-73.6,
    planting_date=5150,           # YYDDD: year 05, DOY 150 (or "2005-05-30")
    start_year=2005, end_year=2015,
    output_dir="outputs/<run>/dssat",
)

result = run_dssat(workdir)
summary = parse_summary(workdir)
for rec in summary:
    print(f"Yield: {rec['HWAM']} kg/ha")
```

### Step 3: Visualize results

```bash
python skills/plot/plot_crop_yield_map.py \
  --csv outputs/<run>/dssat/gridded_yield.csv \
  --value_col hwam_kgha \
  --title "Maize Yield" --output outputs/<run>/yield_map.png
```

---

## Known Limitations

- Chinese cultivar library primarily covers winter wheat zones; spring wheat needs calibration
- FileX format is fixed-width columns — whitespace errors cause silent failures
- Fortran path length limit (~72-256 chars depending on compiler)
- No built-in spatial mode — grid simulations use wrapper scripts

---

*Generated by the Knowledge Dissection Toolkit v4.0.*

---

## Rice Simulation (CERES-Rice / RICER048)

**Model**: CERES-Rice (RICER048 in DSSAT)
**Status**: Validated tool ready, Chinese cultivar library with 11 varieties
**Tool**: `tools/setup_rice_experiment.py`

### Quick Start (Rice)

```python
import sys
sys.path.insert(0, "KISSPATH_KI_ROOT/DSSAT/knowledge_infrastructure/tools")
from setup_rice_experiment import setup_rice

# Auto-selects cultivar and planting date by latitude
result = setup_rice(
    lat=30.0, lon=114.0,         # Wuhan, Yangtze region
    year=2010,
    weather_file="path/to.WTH",
    soil_id="IB00000001",
    output_dir="/tmp/dssat_rice",
)
# Result contains: workdir, cultivar, summary (if run succeeded)
```

### Chinese Rice Cultivar Table

| Code | Name | Region | System | Plant DOY | Expected Yield (kg/ha) |
|------|------|--------|--------|-----------|----------------------|
| CN0201 | YZR Indica Medium | Yangtze (28-33N) | Single crop | ~120 (May 1) | 6,000-8,000 |
| CN0202 | YZR Indica Early | Yangtze | Single crop | ~110 (Apr 20) | 5,000-7,000 |
| CN0203 | YZR Indica Late | Yangtze | Single crop | ~130 (May 10) | 6,500-8,500 |
| CN0204 | YZR Hybrid (Shanyou) | Yangtze | Single crop | ~125 (May 5) | 7,000-9,500 |
| CN0211 | SC Early Indica | South (<28N) | Double early | ~75 (Mar 16) | 4,000-6,000 |
| CN0212 | SC Early Hybrid | South | Double early | ~80 (Mar 21) | 4,500-6,500 |
| CN0221 | SC Late Indica | South | Double late | ~195 (Jul 14) | 4,000-5,500 |
| CN0222 | SC Late Hybrid | South | Double late | ~190 (Jul 9) | 4,500-6,500 |
| CN0231 | NEC Japonica Calib | Northeast (>40N) | Single crop | ~130 (May 10) | 6,000-8,500 |
| CN0232 | NEC Japonica Early | Northeast | Single crop | ~125 (May 5) | 5,000-7,000 |
| CN0233 | NEC Japonica Late | Northeast | Single crop | ~135 (May 15) | 6,500-9,000 |

### Key CERES-Rice Parameters

| Parameter | Description | Range | Calibration |
|-----------|-------------|-------|-------------|
| P1 | Juvenile phase duration (GDD, base 9C) | 150-800 | Flexible, match panicle initiation |
| P2O | Critical photoperiod (hours) | 11-13 | Do not go below 11 without data |
| P2R | Photoperiod sensitivity (GDD/h) | 5-300 | Modern varieties: lower range |
| P5 | Grain filling duration (GDD, base 9C) | 150-850 | Match maturity date |
| G1 | Spikelet number coeff (#/g) | 50-75 | Typical: 55 |
| G2 | Single grain weight (g) | 0.015-0.030 | Low flexibility |
| G3 | Tillering coefficient | 0.7-1.3 | Relative to IR64 |
| PHINT | Phyllochron interval (GDD) | 55-90 | Default 83, need leaf data to change |

### Rice-Specific Notes
- Rice uses transplanting (PLME=T, PAGE=23 for seedling age) in the FileX
- Paddy rice needs flood irrigation management (IR011=bund height, IR009=flood depth)
- N fertilizer: typically 120-180 kg/ha in China (use NPKGRIDS for site-specific rates)
- The model does NOT have a separate .ECO file — ecotype params are in .SPE
- For double-cropping in South China, run two separate experiments (early + late)

---

## Soybean Simulation (CROPGRO / SBGRO048)

**Model**: CROPGRO-Soybean (CRGRO048 engine, SBGRO048 species files)
**Status**: Validated tool ready, Chinese cultivar library with 7 varieties
**Tool**: `tools/setup_soybean_experiment.py`

### Quick Start (Soybean)

```python
import sys
sys.path.insert(0, "KISSPATH_KI_ROOT/DSSAT/knowledge_infrastructure/tools")
from setup_soybean_experiment import setup_soybean

# Auto-selects cultivar by latitude and maturity group
result = setup_soybean(
    lat=45.0, lon=126.0,         # Harbin, Northeast
    year=2010,
    weather_file="path/to.WTH",
    soil_id="IB00000001",
    output_dir="/tmp/dssat_soybean",
)
```

### Chinese Soybean Cultivar Table

| Code | Name | Region | MG | Plant DOY | Expected Yield (kg/ha) |
|------|------|--------|-----|-----------|----------------------|
| CN0301 | NEC MG 0 Early | Northeast (>40N) | 0 | ~130 (May 10) | 1,800-2,800 |
| CN0302 | NEC MG I Medium | Northeast | I | ~135 (May 15) | 2,000-3,000 |
| CN0303 | NEC MG II Late | Northeast | II | ~128 (May 8) | 2,200-3,200 |
| CN0311 | HHH MG III | Huang-Huai-Hai (32-40N) | III | ~165 (Jun 14) | 2,000-3,000 |
| CN0312 | HHH MG IV | Huang-Huai-Hai | IV | ~160 (Jun 9) | 2,200-3,200 |
| CN0321 | SC MG V | South (<32N) | V | ~165 (Jun 14) | 1,800-2,600 |
| CN0322 | SC MG VI | South | VI | ~170 (Jun 19) | 1,600-2,400 |

### Key CROPGRO-Soybean Parameters

| Parameter | Description | Range | Calibration |
|-----------|-------------|-------|-------------|
| CSDL | Critical short day length (h) | 11.8-14.6 | Phenology |
| PPSEN | Photoperiod sensitivity slope (1/h) | 0.13-0.39 | Phenology |
| EM-FL | Emergence to flowering (photothermal days) | 9-29 | Phenology |
| FL-SD | Flowering to first seed (photothermal days) | 11-22 | Phenology |
| SD-PM | First seed to maturity (photothermal days) | 22-38 | Phenology |
| LFMAX | Max leaf photosynthesis rate (mg CO2/m2/s) | 1.0-1.4 | Growth |
| SLAVR | Specific leaf area (cm2/g) | 300-400 | Growth |
| WTPSD | Max weight per seed (g) | 0.15-0.19 | Growth |

### Soybean-Specific Notes
- CROPGRO is a different model engine than CERES — uses photothermal time, not GDD
- Soybean is a legume: SYMBI=Y is set automatically (N fixation enabled)
- Minimal N fertilizer needed (0-30 kg/ha starter), soybean fixes its own N
- Maturity Group (MG) is the key cultivar parameter — higher MG = longer season
- HHH soybean is relay-planted after winter wheat harvest (~mid June)
- Ecotype (.ECO) file exists for soybean (SBGRO048.ECO) and must be present

---

## PlantGro.OUT Parser (Daily Growth Analysis)

**Tool**: `tools/parse_plantgro.py`
**Status**: Operational, tested on maize 10-year PlantGro.OUT

### Quick Start (PlantGro Parser)

```python
import sys
sys.path.insert(0, "KISSPATH_KI_ROOT/DSSAT/knowledge_infrastructure/tools")
from parse_plantgro import parse_plantgro, extract_growth_summary, get_timeseries

# Parse all daily records
records = parse_plantgro("/path/to/PlantGro.OUT")

# Get summary metrics (peak LAI, phenology, stress days)
summary = extract_growth_summary(records)
for run in summary["runs"]:
    print(f"Run {run['run']}: LAI={run['peak_lai']}, "
          f"Grain={run['final_grain_kgha']} kg/ha, "
          f"Water stress days={run['total_stress_days_water']}")

# Extract time series for specific variables
ts = get_timeseries(records, ["LAID", "CWAD", "GWAD"], run_number=1)
```

### CLI Usage

```bash
# Growth summary (peak LAI, phenology dates, stress)
python tools/parse_plantgro.py /path/to/PlantGro.OUT --summary

# Export daily data to CSV
python tools/parse_plantgro.py /path/to/PlantGro.OUT --format csv --output growth.csv

# Extract specific variables
python tools/parse_plantgro.py /path/to/PlantGro.OUT --variables LAID CWAD GWAD --format json
```

### Key Output Variables

| Variable | Description | Unit | Available For |
|----------|-------------|------|---------------|
| LAID | Leaf area index | m2/m2 | All crops |
| CWAD | Total aboveground biomass | kg/ha | All crops |
| GWAD | Grain/fruit weight | kg/ha | All crops |
| HIAD | Harvest index | fraction | All crops |
| WSPD | Water stress (photosynthesis) | 0-1 | All crops |
| NSTD | Nitrogen stress | 0-1 | All crops |
| RDPD | Root depth | m | All crops |
| GSTD | Growth stage | Zadoks | CERES crops |
| DTTD | Daily thermal time | C-day | All crops |
| G#AD | Grain number | #/m2 | Cereals |
| GWGD | Individual grain weight | mg | Cereals |

### Growth Summary Output (extract_growth_summary)

Returns per run:
- `peak_lai` / `peak_lai_date` / `peak_lai_dap`
- `max_biomass_kgha`
- `final_grain_kgha` / `final_harvest_index`
- `emergence_dap` / `emergence_date`
- `anthesis_dap` / `anthesis_date`
- `maturity_dap` / `maturity_date`
- `total_stress_days_water` / `total_stress_days_nitrogen`
- `max_root_depth_m`
- `total_thermal_time`

---

## 12. Parameter Selection by Region

## Crop Calendar Reference (China)

| Region | Latitude | Winter Wheat | Summer Maize | Rice | Soybean |
|--------|----------|-------------|-------------|------|---------|
| Northeast | >40°N | — | May-Sep | May-Oct (japonica) | May-Sep (MG 0-II) |
| North China | 35-40°N | Oct-Jun | Jun-Sep | — | Jun-Sep (MG II-III, after wheat) |
| Huang-Huai | 32-35°N | Oct-Jun | Jun-Oct | — | Jun-Oct (MG III-IV, after wheat) |
| Yangtze | 28-32°N | Nov-May | — | Apr-Oct (single indica) | Jun-Oct (MG IV-V) |
| South | <28°N | — | — | Mar-Jul, Jul-Nov (double) | Jun-Nov (MG V-VI) |

**Data sources on server:**
- GGCMI Crop Calendar: `KISSPATH_HOME/Crop_model_dataset/GGCMI_phase3_crop_calendar/`
- China Phenology GeoTIFF: `KISSPATH_HOME/Crop_model_dataset/8313530/`
- SPAM crop distribution: `KISSPATH_HOME/Crop_model_dataset/dataverse_files/`