Found while migrating a setup from 5.9.7 to 6.1.1. Flagging it in case it isn't already covered by the revision work mentioned in #75.
check_lu() in src/landuse/check_lu.c is documented as a predicate and takes a const Standlist:
Bool check_lu(const Standlist standlist, ...)
) /** \return TRUE if crop stand can be established */
but it now modifies the stands it inspects, and the local const Stand *stand was changed to Stand *stand to allow it:
foreachstand(stand,s,standlist)
{
if(getlandusetype(stand)!=URBAN && stand->pftlist.n==0)
stand->type=&kill_stand;
else if(stand->type->landusetype==landusetype)
{
pft=getpft(&stand->pftlist,0);
The guard looks intentional and sensible — getpft(&stand->pftlist,0) on the following line would read past the end of an empty list, which 5.9.7 didn't protect against. But the empty stand is destroyed rather than skipped.
Why that breaks sowing. check_lu() is called from cultcftstand() as the if condition, immediately before cultivate() is called on a setaside stand. Setaside stands are legitimately empty at the moment a crop is about to be sown into them — especially with intercrop = false, where they carry no vegetation at all. So evaluating the condition marks the very stand being planted into for deletion.
Effect. 500-cell subsets, 250-year spinup, 6.1.1 vs 5.9.7 with identical inputs and forcing:
| latitude |
cropland fraction |
harvested C |
| 25 °N |
−35% |
−64% |
| 17 °N |
−27% |
−72% |
| 10 °N |
−19% |
−50% |
The loss is concentrated in 10–25 °N and splits by calcmethod_sdate: precipitation-sown CFTs (rice, tropical cereals, tropical roots, groundnut, sugarcane) drop toward zero and every irrigated CFT goes to exactly zero, while temperature-sown CFTs and managed grass are bit-identical to 5.9.7. That follows from the mechanism — whether a setaside is empty depends on when in the year sowing is attempted.
Suggested fix — skip instead of destroy, and restore const:
const Stand *stand;
...
if(stand->pftlist.n==0)
continue;
if(stand->type->landusetype==landusetype)
This restores cropland to within 0.3% of 5.9.7 at all three latitudes while leaving 6.1.1's other changes intact.
One note that might help others. The symptom is quiet: no error, no warning. What makes it detectable is that the model's own outputs disagree — fpc band 1 (natural stand fraction) and the globalflux area column both report managed land unchanged, while cftfrac accounts for only ~64% of it. Reconciling those two is what located it, after a fair few dead ends chasing crop parameters.
Disclosure: this report was written by Claude (Anthropic's AI assistant) during an assisted debugging session, and reviewed by a human before posting.
Found while migrating a setup from 5.9.7 to 6.1.1. Flagging it in case it isn't already covered by the revision work mentioned in #75.
check_lu()insrc/landuse/check_lu.cis documented as a predicate and takes aconst Standlist:but it now modifies the stands it inspects, and the local
const Stand *standwas changed toStand *standto allow it:The guard looks intentional and sensible —
getpft(&stand->pftlist,0)on the following line would read past the end of an empty list, which 5.9.7 didn't protect against. But the empty stand is destroyed rather than skipped.Why that breaks sowing.
check_lu()is called fromcultcftstand()as theifcondition, immediately beforecultivate()is called on a setaside stand. Setaside stands are legitimately empty at the moment a crop is about to be sown into them — especially withintercrop = false, where they carry no vegetation at all. So evaluating the condition marks the very stand being planted into for deletion.Effect. 500-cell subsets, 250-year spinup, 6.1.1 vs 5.9.7 with identical inputs and forcing:
The loss is concentrated in 10–25 °N and splits by
calcmethod_sdate: precipitation-sown CFTs (rice, tropical cereals, tropical roots, groundnut, sugarcane) drop toward zero and every irrigated CFT goes to exactly zero, while temperature-sown CFTs and managed grass are bit-identical to 5.9.7. That follows from the mechanism — whether a setaside is empty depends on when in the year sowing is attempted.Suggested fix — skip instead of destroy, and restore const:
This restores cropland to within 0.3% of 5.9.7 at all three latitudes while leaving 6.1.1's other changes intact.
One note that might help others. The symptom is quiet: no error, no warning. What makes it detectable is that the model's own outputs disagree —
fpcband 1 (natural stand fraction) and the globalfluxareacolumn both report managed land unchanged, whilecftfracaccounts for only ~64% of it. Reconciling those two is what located it, after a fair few dead ends chasing crop parameters.Disclosure: this report was written by Claude (Anthropic's AI assistant) during an assisted debugging session, and reviewed by a human before posting.