Skip to content

check_lu() converts empty stands to kill_stand, preventing crop establishment on setaside #76

Description

@lbm364dl

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions