How to use this module. Read this page for the ideas; every chart comes from the module 4 notebook(), which runs the DSSAT engine and writes the small data files the charts read. The runs on this build were made on .
What you will learn
What the observed data files of an experiment hold, how their variables are coded, and how to write them from your own measurements (the shell’s ATCreate tool).
How to change the environment of an experiment (here: shade the crop for three weeks) and evaluate the simulation against the data.
How nitrogen moves through the soil and the crop, and what limits a crop when water and nitrogen are both simulated.
1. Creating crop measurement files: the 1976 peanut shading study
Field measurements reach DSSAT through two files next to the experiment file. The FileA (.PNA for peanut) has one line per treatment with what was measured at the end of the season; the FileT (.PNT) has one line per treatment and sampling date. Each column is a DSSAT variable code, defined in the engine’s DATA.CDE; the files are fixed-format, 6 characters per column, right-aligned under the code.
The course’s exercise uses a shading study done at Gainesville in 1976 (Hang et al. 1984): Florunner peanut, a control and a treatment shaded during pod filling. Its final harvest and growth analysis data came as two spreadsheets, whose columns already carry the DSSAT codes:
The exercise starts from the 1976 Florunner experiment UFGA7601.PNX and changes only what differs: experiment number 9, planting and simulation start on 22 April 1976, automatic irrigation, and a second treatment with an environmental modification that sets solar radiation to 20% of normal from 4 to 24 August 1976. The notebook edits the file section by section, as XBuild would. The two sections that change most:
In the ERAD column, M .20 multiplies radiation by 0.20 from day 217 (4 August). A modification stays in force until another replaces it, so the second line, M 1 on day 238 (25 August), restores full radiation.
Writing the FileA and FileT
learnlib.write_observed writes both files from the spreadsheets, as ATCreate does:
The alignment is not cosmetic: the engine reads these files by column position. A first version of this writer put every name one character to the left of its values. Read back by whitespace the file looked fine, but the engine’s evaluation reported anthesis about 11,000 days after planting. Checking the engine’s Evaluate output against the spreadsheet is the test that caught it.
Inputs.table(shStats.map(d => ({...d,treatment: d.TRNO===1?"control":"shaded"})), {columns: ["treatment","variable","n","rmse","d"],header: {treatment:"Treatment",variable:"Code",n:"Samples",rmse:"RMSE",d:"Willmott d"},format: {rmse: d => d <10? d.toFixed(2) : fmt.int(d),d: d => d.toFixed(2)},layout:"auto"})
ev = (t, c) => shEval.find(d => d.TRNO=== t && d.code=== c)lossSim =1-ev(2,"HWAM").simulated/ev(1,"HWAM").simulatedlossObs =1-ev(2,"HWAM").measured/ev(1,"HWAM").measured
The field lost of its seed yield to three weeks of shade; the model loses . The control is reproduced well (seed yield kg/ha against ); the shaded treatment has only samples, from the middle of the shade period onwards, which is why its d values say little.
Collecting the data: sampling and the order of calibration
The measurement files are only as good as the samples behind them. The course’s lecture on experimental data collection gave a principle and a procedure:
Take a large sample for biomass, a small one for its parts. Total biomass varies a lot from plant to plant, so it is cut from about 1 m² of land, with a border on all four sides about as wide as the crop is tall (2 m for maize at the end of the season). Fractions and ratios (leaf, stem, pod, seed) vary much less, so a representative subsample is split into parts, oven-dried (about 40 hours at 60 °C) and weighed, and the fractions are scaled to the large sample.
Sample often enough. Weekly during vegetative growth; the key stages (flowering, first seed, maturity) dated as exactly as possible; four or more replications.
Record what the model does not simulate: pests, diseases and weeds, so that a missed yield can be explained.
Calibration then follows a fixed order, on treatments with as little stress as possible (enough water and nitrogen, good pest control):
Development: flowering, then maturity dates (for example CSDL, EM-FL, SD-PM in CROPGRO; P1, P5 in CERES-Maize).
Dry matter accumulation: biomass over time (LFMAX or radiation use efficiency only when cultivars on the same soil differ; the soil fertility factor SLPF when sites differ in fertility).
Leaf area and partitioning, then yield and its components (SLAVR, FL-SH, FL-SD, seed size).
The statistics below (RMSE and Willmott’s d on time series and end-of-season values) judge each step; the course warned that R² alone can mislead, and that a plot of simulated against observed says more than any single number.
How good is the fit? The course’s statistics
The course evaluates a simulation with a handful of statistics computed for each variable: the means of the observed and simulated values and their ratio, the r² of their regression, the RMSE, and Willmott’s d (Willmott 1981). With \(O_i\) observed and \(S_i\) simulated values:
The published experiment UFGA7604.PNX in dssat-csm-data holds the full study: a control and four 21-day periods of 25% radiation, from flowering to late pod filling.
With the exercise’s experiment as a template, the notebook moves a 21-day shade window through the season a week at a time, at two strengths. Pick a window:
shStarts = [...newSet(shSweep.filter(d => d.start!=null).map(d => d.start))].sort(d3.ascending)viewof shStart = Inputs.range([0, shStarts.length-1], {step:1,value:5,label:"Shade from day",format: i =>`${shStarts[i]} (${shStarts[i] -113} days after planting)`})viewof shF = Inputs.radio(newMap([["25% of normal radiation",0.25], ["50%",0.5]]), {value:0.25,label:"Radiation"})
withDownload(Plot.plot({width:Math.min(W,640),height:280,marginLeft:60,x: {label:"First day of the 21-day shade, days after planting",grid:true},y: {label:"Seed yield lost",grid:true,tickFormat:"%"},color: {legend:true,domain: [0.25,0.5],range: ["#b5651d","#f2a33a"],tickFormat: d =>`${d *100}% radiation`},marks: [ Plot.lineY(shSweep.filter(d => d.start!=null), {x:"start_dap",y:"loss",stroke:"radiation",marker:"dot",tip:true}), Plot.dot([shSel], {x:"start_dap",y:"loss",r:9,stroke:"#222",strokeWidth:1.5}) ]}),"module4-shade-loss")
worst = shSweep.filter(d => d.radiation===0.25&& d.start!=null).reduce((a, b) => b.loss> a.loss? b : a)
Shade from day at % radiation costs of the seed yield ( against kg/ha). At 25% radiation the most damaging three weeks start days after planting, a loss of . Shade early in the season costs less, and so does shade late in pod filling.
2. Nitrogen-limited production: UFGA8213.MZX
The course’s nitrogen exercise builds on the water-limited maize experiment of module 3 and adds what the nitrogen balance needs: initial conditions with 1000 kg/ha of maize residue at 0.8% N incorporated to 15 cm, the initial ammonium and nitrate of each layer, nitrogen fertilizer on reported dates, and both balances switched on. Four treatments: rainfed and automatically irrigated, each without N and with 60 kg N/ha on 15 March plus 75 kg N/ha on 12 April (ammonium nitrate, broadcast and incorporated at 10 cm). The sections as the notebook writes them:
The soil N model keeps ammonium and nitrate in every layer; they change with fertilizer, mineralization of organic matter, nitrification, leaching with drainage water and uptake by roots (SOILNI.for).
Where the nitrogen went
flows = ["applied as fertilizer","mineralized (net)","taken up by the crop","leached","left in the soil at maturity"]nLong = nBal.flatMap(d => [ {treatment: d.treatment,flow: flows[0],v: d.NICM}, {treatment: d.treatment,flow: flows[1],v: d.NMINC}, {treatment: d.treatment,flow: flows[2],v: d.NUCM}, {treatment: d.treatment,flow: flows[3],v: d.NLCM}, {treatment: d.treatment,flow: flows[4],v: d.NIAM}])withDownload(Plot.plot({width: W,height:420,marginLeft:150,marginRight:140,x: {label:"kg N/ha",grid:true},y: {label:null,domain: flows,axis:null},fy: {label:null,domain: nBal.map(d => d.treatment)},color: {legend:true,domain: flows,range: ["#4a6fa5","#9bb8ad","#3f7a34","#f2a33a","#c4a35a"]},marks: [Plot.barX(nLong, {y:"flow",x:"v",fill:"flow",fy:"treatment",tip:true}), Plot.ruleX([0])]}),"module4-n-balance")
Inputs.table(nBal, {columns: ["treatment","HWAM","CWAM","NICM","NUCM","NLCM","NIAM","IRCM"],header: {treatment:"Treatment",HWAM:"Grain, kg/ha",CWAM:"Tops, kg/ha",NICM:"N applied",NUCM:"N uptake",NLCM:"N leached",NIAM:"Soil N at maturity",IRCM:"Irrigation, mm"},layout:"auto"})
Neither water nor nitrogen alone does much: irrigation without N gives kg/ha, nitrogen without irrigation kg/ha, and both together kg/ha. With irrigation the crop takes up kg N/ha, without it : water and nitrogen limits interact.
Nitrate in the soil over the season
viewof nTrt = Inputs.select(nBal.map(d => d.TRNO), {value:4,label:"Treatment",format: t => trtName.get(t)})
withDownload(Plot.plot({width: W,height:300,marginLeft:55,x: {label:"Days after planting",grid:true},y: {label:"kg N/ha",grid:true},color: {legend:true,domain: ["applied, cumulative","inorganic N in the soil","taken up, cumulative","leached, cumulative"],range: ["#4a6fa5","#c4a35a","#3f7a34","#f2a33a"]},marks: [ Plot.lineY(soilN.filter(d => d.TRNO=== nTrt), {x:"DAP",y:"NAPC",stroke: () =>"applied, cumulative",tip:true}), Plot.lineY(soilN.filter(d => d.TRNO=== nTrt), {x:"DAP",y:"NIAD",stroke: () =>"inorganic N in the soil",tip:true}), Plot.lineY(soilN.filter(d => d.TRNO=== nTrt), {x:"DAP",y:"NUCM",stroke: () =>"taken up, cumulative"}), Plot.lineY(soilN.filter(d => d.TRNO=== nTrt), {x:"DAP",y:"NLCC",stroke: () =>"leached, cumulative"}) ]}),`module4-soil-n-${nTrt}`)
NO3 by layer in ppm, as the engine writes it (OPSOILNI.for:132); the depths are the engine’s simulation layers (module 3). Totals in kg N/ha.
Nitrogen in the plant
CERES-Maize compares the N concentration of the tops (TANC) with a critical concentration (TCNP, below which growth suffers) and a minimum (TMNC), both falling as the crop develops, and turns the shortfall into a 0-1 factor (MZ_NFACTO.for:61): \[
NFAC = 1 - \frac{TCNP - TANC}{TCNP - TMNC}
\]
nfacText = tmnc >= tcnp ?"TMNC must be below TCNP.": tanc >= tcnp ?"TANC is at or above the critical concentration: NFAC = 1, no N stress.":`NFAC = ${Math.max(0,1- (tcnp - tanc) / (tcnp - tmnc)).toFixed(2)}: the plant is ${fmt.pct((tcnp - tanc) / (tcnp - tmnc))} of the way from the critical to the minimum concentration.`
viewof pnVar = Inputs.radio(newMap([["Tops N, kg/ha","CNAD"], ["Stem and leaf N, %","VN%D"], ["N stress index (0 = none)","NSTD"], ["Water stress index (0 = none)","WSGD"]]), {value:"NSTD",label:"Show"})
withDownload(Plot.plot({width: W,height:320,marginLeft:55,x: {label:"Days after planting",grid:true},y: {label: pnVar,grid:true},color: {legend:true,domain: nBal.map(d => d.treatment),range: ["#c4a35a","#4a6fa5","#f2a33a","#3f7a34"]},marks: [Plot.lineY(plantN, {x:"DAP",y: pnVar,stroke: d => trtName.get(d.TRNO),tip:true})]}),`module4-plant-n-${pnVar.replace(/[^A-Z]/gi,"")}`)
The stress indices are NSTD and WSGD of PlantGro, written as 1 minus the factor (MZ_OPGROW.for:175-176).
Nitrogen fixation in a legume
Grain legumes have a source of nitrogen that cereals lack: fixation by bacteria in root nodules. In CROPGRO the crop grows nodules when its nitrogen demand exceeds what the roots take up from the soil and what it mobilizes from older tissue; the nodules then fix nitrogen at a rate set by their mass and by temperature, the crop’s water status, flooding, nodule age and the carbohydrate they receive (Plant/CROPGRO/NFIX.for). The SYMBI switch of the simulation controls turns fixation on or off. Bragg soybean at Gainesville in 1978 (UFGA7801.SBX, a sandy soil and no nitrogen fertilizer), with the nitrogen balance on and fixation on or off, and with the nitrogen balance off for comparison:
withDownload(Plot.plot({width:Math.min(W,680),height:300,marginLeft:60,x: {label:"Days after planting",grid:true},y: {label:"Cumulative N, kg/ha",grid:true},color: {legend:true,domain: ["fixed","taken up from the soil"],range: ["#3f7a34","#d9822b"]},marks: [Plot.lineY(nFixD.filter(d => d.case==="fixation"&& d.treatment==="irrigated").flatMap(d => [{DAP: d.DAP,k:"fixed",v: d.NFXC}, {DAP: d.DAP,k:"taken up from the soil",v: d.NUPC}]), {x:"DAP",y:"v",stroke:"k",tip:true})]}),"module4-n-fixation")
nfPick = (c, t) => nFix.find(d => d.case=== c && d.treatment=== t)nfOn =nfPick("fixation","irrigated")nfOff =nfPick("no fixation","irrigated")nfPot =nfPick("nitrogen not limiting","irrigated")
Irrigated, the soybean fixes kg N/ha and takes only kg N/ha from the soil. With fixation it yields kg/ha, close to the kg/ha it yields when nitrogen does not limit at all; without fixation, on this soil, it yields kg/ha. In this experiment, fixation supplies almost all the nitrogen the crop needs.
Two soil organic matter models
DSSAT can simulate soil organic matter in two ways, chosen with the MESOM switch of the simulation controls: G, the original CERES approach, and P, a model based on CENTURY. The four treatments of the exercise under both:
Without fertilizer the choice matters most: net mineralization over the season is kg N/ha with the CERES option and with CENTURY, and the irrigated unfertilized maize yields against kg/ha. With 135 kg N/ha of fertilizer the two give similar yields. Where a soil’s own nitrogen supply matters, the organic matter option is a choice to justify, not a default to accept.
Self test 1: the response to nitrogen
0 to 200 kg N/ha in total, in the exercise’s two applications (60:75), rainfed and automatically irrigated:
viewof nrVar = Inputs.select(newMap([["Grain yield, kg/ha","HWAM"], ["N uptake, kg N/ha","NUCM"], ["N leached, kg N/ha","NLCM"], ["Extra grain per kg N (agronomic efficiency)","agronomic_efficiency"], ["Share of the extra N taken up (recovery)","recovery"]]), {value:"HWAM",label:"Show"})
Irrigated, yield climbs from to kg/ha over the five rates. Rainfed it peaks at kg/ha and falls again at the higher rates, to kg/ha at ; at that rate the rainfed crop takes up kg N/ha against irrigated.
Self test 2: three soils
The four treatments on a deep clay, silt loam and sand from the soil file, and on the Millhopper fine sand:
The fertilized treatments with the residue, its N content and the initial nitrate changed one at a time:
Inputs.table(nRes, {columns: ["scenario","treatment","HWAM","NUCM","NLCM","NMINC","NIAM"],header: {scenario:"Initial conditions",treatment:"Treatment",HWAM:"Grain, kg/ha",NUCM:"N uptake",NLCM:"N leached",NMINC:"Net mineralization",NIAM:"Soil N at maturity"},layout:"auto",rows:14})
resRange = d3.extent(nRes.filter(d => d.treatment.startsWith("irrigated")), d => d.HWAM)
In this sandy soil with 135 kg N/ha of fertilizer, the residue and initial nitrogen move the irrigated yield only between and kg/ha.
Self-test
Which codes would you put in a FileT for a rice trial that sampled leaf area, tops and grain weight every two weeks?
Why does the engine’s evaluation of a written FileA catch errors that reading it back with pandas does not?
Why does three weeks of shade at the start of pod filling cost more than the same shade at flowering?
Rainfed, yield falls at the highest N rates. Look at the water stress index of those runs: what could explain it, and how would you test that in the model?
Where this goes next
Module 5 repeats management comparisons over many seasons and adds economics and risk. The plan for all six modules is on the training overview.