import {withDownload, adminMarks, cellFeatures, fmt, pearson} from "../assets/js/dl.js"
summary = FileAttachment("data/summary.json").json()
cells = FileAttachment("data/grid_cells.csv").csv({typed: true})
outline = FileAttachment("data/java_outline.geojson").json()
provinces = FileAttachment("data/provinces.geojson").json()
districtsGeo = FileAttachment("data/districts.geojson").json()
soilLayers = FileAttachment("data/soil_layers.csv").csv({typed: true})
sens = FileAttachment("data/sensitivity.csv").csv({typed: true})
tcCells = FileAttachment("data/tcldf_cells.csv").csv({typed: true})
districts = FileAttachment("data/districts.csv").csv({typed: true})
districtYears = FileAttachment("data/district_years.csv").csv({typed: true})
javaYears = FileAttachment("data/java_years.csv").csv({typed: true})
cultivars = FileAttachment("data/cultivars.csv").csv()
fit = FileAttachment("data/fit_slpf.json").json()
fitGrid = FileAttachment("data/fit_slpf_grid.csv").csv({typed: true})
slpfCells = FileAttachment("data/slpf_cells.csv").csv({typed: true})
slpfPointsText = fit.slpf_points.join(", ")
slpfRange = `${d3.min(fit.slpf_points)} to ${d3.max(fit.slpf_points)}`
usesTable = summary.current.slpf_table != null && summary.current.slpf_table !== "None"
currentSlpf = usesTable && fit.temp ? [fit.temp.slpf_irrigated, fit.temp.slpf_rainfed]
: [summary.current.slpf, summary.current.slpf_rainfed].filter(v => v != null && v !== "None").map(Number)
runsAll = FileAttachment("data/runs.json").json()
tcText = tcRun ? `(${fmt.int(tcRun.yield_wet_rainfed)} vs ${fmt.int(tcRun.yield_wet_irrigated)} kg/ha in run ${tcRun.run}),` : ","
// one SLPF for all rice with the TCLDF of the current run, if that run exists
tcRun = runsAll.find(r => r.tcldf === summary.current.tcldf && (r.slpf_rainfed === "None" || r.slpf_rainfed == null) && r.slpf !== "None" && r.yield_wet_rainfed != null && Math.abs(+r.slpf - 0.75) < 0.05)
cur = summary.current
runOptions = new Map(Object.entries(summary.districts).map(([k, v]) => [v.label, k]))
mapW = Math.min(width, 960)Scientific method
Why each step is done the way it is, with the source behind it
The model is CERES-Rice in DSSAT-CSM 4.8 (Jones et al. 2003), built from the DSSAT source at version 4.8.6.4 (DSSAT Foundation 2026). File and line references below (RI_Grosub.for:648) point into that source (dssat-csm-os, commit 25c93b9a).
Soil profiles from SoilGrids
What. For each weather cell, SoilGrids 2.0 (Poggio et al. 2021) properties are averaged over the cell’s rice pixels, weighted by rice area, at the six SoilGrids depths. Stored integers are converted with ISRIC’s factors (ISRIC - World Soil Information, n.d.). Water limits come from the pedotransfer functions of Saxton and Rawls (2006): lower limit (LL, 1500 kPa), drained upper limit (DUL, 33 kPa), saturation (SAT) and saturated conductivity, from sand, clay and organic matter (organic matter = 1.724 x organic carbon, the conventional van Bemmelen factor; an assumption, not from the paper). Code: soils.saxton_rawls.
Why. Saxton and Rawls give all four properties DSSAT needs from texture and organic matter alone, which is what a global grid provides, and the coefficients were checked against USDA-ARS WEPP saxpar.for (src/soils.py docstring).
Why scale organic carbon. At the 15 WISE sites on Java (18 profiles), SoilGrids organic carbon is 1.8 times the measured value at 0-30 cm (median 3.67 against 2.03%, 35 layers; scripts/fit_soc_scale.py --country Indonesia --bbox 105 -9 114.7 -5). xdssat multiplies SoilGrids organic carbon by the median WISE/SoilGrids ratio over 78 Indonesian WISE profiles (Romero et al. 2012): 0.70 (0-30 cm), 0.63 (30-100 cm), 0.47 (100-200 cm), soils.SOC_SCALE. The scaled value also enters the pedotransfer.
Root growth factor (SRGF). DSSAT needs a relative root growth factor per layer. xdssat uses 1 for layers ending at or above 15 cm and exp(-0.02 x mid-depth) below (soils.root_growth_factor). This is not taken from a paper: it is the rule that reproduces 96.8% of the 16,360 layers of DSSAT’s WI.SOL, measured in this repository on 2026-11-06 (src/soils.py docstring). The line is the rule, the dots are the layers of the current profiles.
Planting dates: BMKG rainfall onset
What. In per_cell mode, the wet season starts at the rainfall onset of each cell: the first dasarian (10-day period) with at least 50 mm of rain that is followed by two more dasarian of at least 50 mm, the rule used by Indonesia’s meteorological agency BMKG (Giarno et al. 2012), and in BMKG Stasiun Klimatologi Sumatera Selatan’s Buletin Prakiraan Musim Hujan 2025/2026 (both cited in management.onset). Onset is computed per agricultural year from AgERA5 rain and the median over the years is used (management.onset, management.onset_climatology).
Why. Rain-fed paddies can only be planted once the rain has come, and the onset is not the same everywhere: across Java’s rice cells the median onset spans days between the 10th and 90th percentile of rice area (computed from onset.csv of the current run). A uniform calendar would plant some cells before the rain and others late.
Assumptions. Seedbeds are sown at onset and transplanted 20 days later (the template transplants 23-day-old seedlings); the dry season is planted 120 days after the wet season (planting_from_onset). Both offsets are placeholders, not calibrated.
Level calibration: SLPF
What the factor does. SLPF is the soil fertility factor in the soil profile. In CERES-Rice it multiplies the day’s carbohydrate production after all stresses (Plant/CERES-Rice/RI_Grosub.for:648-649):
CARBO = PCARB*AMIN1(PRFT,SWFAC,NSTRES,TSHOCK,PStres1,KSTRES,PRFO3)
& * SLPFthat is,
\[ \begin{aligned} CARBO = {} & PCARB \cdot \min(f_T,\ SWFAC,\ NSTRES, \\ & \qquad f_{shock},\ f_P,\ f_K,\ f_{O_3}) \cdot SLPF , \end{aligned} \]
so SLPF scales the day’s growth after the most limiting stress, by the same fraction every day.
Why use it. The uncalibrated model simulates the yield allowed by weather, water and nitrogen, and overshoots the district statistics by a factor of . The losses it does not simulate (pests, diseases, weeds, uneven water, management) act on growth, which is what SLPF scales. Nitrogen rate and cultivar alone do not close the gap (see Data, AOI and variety). The soil file’s other factor, SLNF, does not act on growth: in DSSAT 4.8 it reaches the CERES soil organic matter module as DMOD (optempy2k.for:520, SOILDYN.for:276-277), where it scales the mineralisation of stable organic matter (SoilOrg.for:673); the CENTURY module does not use it.
How strongly. Yield falls faster than SLPF; the reason has not been traced in the source here. The sweep on the 40-cell sample:
checks/n_cultivar_sensitivity_soc1_slpf*.csv: IR 58, 140 kg N, 40 cells, season years 1992-2014, area-weighted. Dashed: proportional response. Orange lines: SLPF of the current run for irrigated and rain-fed strata (with a per-cell table: its values at the reference minimum temperature).
One SLPF for all of Java fixes the level, not the pattern; the current run therefore uses one value for irrigated and one for rain-fed rice (below), each changing with the cell’s night temperature (further below). Its Java mean is simulated vs observed ku/ha.
Cold sterility: TCLDF
Status. The value is a calibration choice tested on the district data, not a measured property of the varieties grown in the Javanese highlands.
The problem it should address. Simulated district yields rise with season length, and the long seasons are in the cool highlands; observed yields do not show this (see the Story).
What the coefficient does. TCLDF is a cultivar coefficient: the critical low temperature (°C) for spikelet sterility (the CUL file’s definition is in the table on the Data page). In the phase that ends at heading (CASE (3), Plant/CERES-Rice/RI_Phenol.for:592), once 65% of its thermal time has passed (RI_Phenol.for:600), CERES-Rice estimates canopy temperature from the daily minimum (TCANOPY = 0.87 + 0.92*TMIN, after Heenan and Lewis 1981 per the source comment) and accumulates the degrees below TCLDF (RI_Phenol.for:607-614):
TCANOPY = 0.87 + 0.92*TMIN
IF (TCANOPY .LE. TCLDF) THEN
CUMTMP = CUMTMP + (TCLDF-TCANOPY)
ENDIFThat sum becomes a cold factor STRCOLD = 1.0-(0.01*CUMTMP)**1.667 (RI_Phenol.for:641), which caps the number of grains: GPP = SPIKE*AMIN1(STRCOLD,STRHEAT) (RI_Grosub.for:409). Raising TCLDF makes cool nights cost grains, which lowers yield where the model’s long seasons come from cool nights. The cultivar file gives IR 58 and IR 64 a TCLDF of and °C.
checks/n_cultivar_sensitivity_soc1_slpf0.73_tcldf{0,17,19,21}.csv: 40-cell sample, IR 58, 140 kg N, SLPF 0.73, season years 1992-2014; one dot per stratum (cell and water regime), mean over the seasons.
Full runs with a changed TCLDF, if any have been checked against the districts, show up in the selector below next to the runs without it. The chart is the diagnostic that matters: simulated/observed ratio against simulated season length. A flat cloud would mean the highland bias is gone.
Irrigated and rain-fed SLPF
Why two factors. With one SLPF and TCLDF , simulated rain-fed rice still out-yielded irrigated rice in the wet season while observed district yields rise with the irrigated share. The losses SLPF stands for need not be the same in both systems, so run_aoi.py --slpf-rainfed gives rain-fed strata their own SLPF (their cells get a copy of the soil profile with that value).
How it is fitted (scripts/fit_slpf.py). Four full runs that differ only in SLPF (, all TCLDF 19) give every simulated season its yield at those values; a polynomial per season interpolates between them, which is possible because SLPF changes growth but not the season’s dates. Seasons are then summed to districts and harvest years exactly as in compare_districts.py, and a grid search over (irrigated, rain-fed) pairs minimises the rice-area-weighted mean squared log ratio of simulated to observed district means over districts.
Does the second factor predict, or only fit? Leaving out one province at a time, refitting on the others and scoring the left-out districts:
Refitted without each province, the irrigated SLPF stays within and the rain-fed one within . The pair reduces the error in of left-out provinces. The orange dots belong to the next step.
checks/districts/fit_slpf.json and fit_slpf_grid.csv from scripts/fit_slpf.py; the surface is for the pair without the temperature term.
SLPF that changes with night temperature
Why. With TCLDF 19 and the pair, simulated district yield still rose with season length: the cool cells keep a long-season yield bonus that the district statistics do not show. fit_slpf.py therefore adds one more parameter, a slope of SLPF on the cell’s mean season minimum temperature:
\[ \begin{aligned} SLPF_{c,w} = \min\big(& SLPF_{max}, \\ & \max\big(SLPF_{min},\ s_w + b\,(\bar T_{min,c} - T_{ref})\big)\big) \end{aligned} \]
for cell \(c\) and water regime \(w\) (irrigated or rain-fed), with \(\bar T_{min,c}\) the cell’s mean season minimum temperature, \(T_{ref}\) = °C, and the limits the range of the SLPF runs, .
The fit gives s = for irrigated and for rain-fed strata and b = per °C: a cell 3 °C cooler than the reference gets an SLPF about lower. of the strata hit the clipping range. This is a statistical correction of the model’s long-season bonus in cool cells, not a mechanism: nothing in it says why those districts yield less than the model expects. run_aoi.py --slpf-table takes the resulting per-stratum table.
Fit statistics from fit_slpf.json (interpolated between the SLPF runs, before the confirming run); held-out RMSE is the district-weighted mean of the leave-one-province-out scores. The confirming full run is the current run of this site, ; its own district statistics are on the Story page. The SLPF table is .
Comparing like with like: harvest years
What. Statistics are reported per calendar year and include every season harvested in it. xdssat’s seasons belong to an agricultural year that starts on 1 August (a wet season planted in November and the following dry season share one label). For the comparison each simulated season moves to the calendar year of its harvest date (HDAT, else maturity MDAT); only years fully covered by the run are kept. The district yield of a year is the area-weighted mean over all seasons harvested in it, with the rice ha of each (weather cell, water regime, district) piece from the 100 m rasters, so irrigated rice counts twice a year and rain-fed rice once, as in harvested area (scripts/compare_districts.py).
From dry matter to GKG
What. DSSAT reports grain yield (HWAH) as dry matter. District statistics report gabah kering giling (GKG), milled-dry unhusked grain with some moisture. xdssat divides by (1 - moisture), with moisture 0.14 (--moisture in the comparison scripts).
Why it is flagged. 14% is the moisture usually quoted for GKG, but no BPS definition has been found to cite (scripts/compare_bps.py docstring). The choice moves the level a little, not the pattern: try it.
At % moisture the current run’s Java level is times the observed mean (1.00 at the 14% used in the calibration).
Observations: cleaning before comparing
The district statistics are cleaned by explicit rules before they are used: text with comma decimals, zeros meaning missing, production-sized numbers in the yield column, a lost decimal in 1998, and single outliers against a 7-year running median. Where production and area are both reported, yield is checked against production / area. Every change is written to a flags file with the raw value (scripts/clean_bdsp.py, scripts/clean_moa_yields.py; counts on the Data page).
References
DSSAT Foundation. 2026. DSSAT-CSM Source Code, Version 4.8.6.4 (Commit 25c93b9a). https://github.com/DSSAT/dssat-csm-os.
Giarno, Giarno, Zadrach Ledoufij Dupe, and Musa Ali Mustofa. 2012. “Kajian Awal Musim Hujan Dan Awal Musim Kemarau Di Indonesia.” Jurnal Meteorologi Dan Geofisika 13 (1). https://doi.org/10.31172/jmg.v13i1.113.
ISRIC - World Soil Information. n.d. SoilGrids FAQ: Units and Conversion Factors of the SoilGrids Layers. https://docs.isric.org/globaldata/soilgrids/SoilGrids_faqs_01.html.
Jones, J. W., G. Hoogenboom, C. H. Porter, et al. 2003. “The DSSAT Cropping System Model.” European Journal of Agronomy 18 (3–4): 235–65. https://doi.org/10.1016/S1161-0301(02)00107-7.
Poggio, Laura, Luis M. de Sousa, Niels H. Batjes, et al. 2021. “SoilGrids 2.0: Producing Soil Information for the Globe with Quantified Spatial Uncertainty.” SOIL 7 (1): 217–40. https://doi.org/10.5194/soil-7-217-2021.
Romero, Consuelo C., Gerrit Hoogenboom, Guillermo A. Baigorria, Jawoo Koo, Arjan J. Gijsman, and Stanley Wood. 2012. “Reanalysis of a Global Soil Database for Crop and Environmental Modeling.” Environmental Modelling & Software 35: 163–70. https://doi.org/10.1016/j.envsoft.2012.02.018.
Saxton, K. E., and W. J. Rawls. 2006. “Soil Water Characteristic Estimates by Texture and Organic Matter for Hydrologic Solutions.” Soil Science Society of America Journal 70 (5): 1569–78. https://doi.org/10.2136/sssaj2005.0117.