Skip to content

feat(water-balance): select consumptive water by CFT band (#107) - #681

Merged
lbm364dl merged 1 commit into
mainfrom
lbm364dl/water-balance-bands
Aug 7, 2026
Merged

feat(water-balance): select consumptive water by CFT band (#107)#681
lbm364dl merged 1 commit into
mainfrom
lbm364dl/water-balance-bands

Conversation

@lbm364dl

@lbm364dl lbm364dl commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Prepares the ground for #116 (the water footprint extension, #107) by making
main's build_water_balance() able to answer the question that extension asks.

The problem

build_water_balance() reads the per-CFT consumptive-water cubes and
immediately sums them over every crop band, so the only figure it can report is
the whole-cell total. A grazing water footprint needs the grassland band
alone
— charging grazing the whole-cell total adds every wheat field and rice
paddy in the cell to the pasture.

The change

bands = restricts the consumptive-water and cft_nir terms to named bands.
NULL (default) totals every band, so existing callers are unaffected.

build_water_balance(bands = "rainfed grassland", data = ...)

Selection is by the band_name the file carries in NamePFT, never by
index
: which crop a given index denotes is a property of how the run was
configured, so a positional filter would keep charging band 14 in a run whose
band 14 is a different crop. An unknown name — or an input with no band names
to match on — aborts rather than silently returning the whole-cell total.

Three reader bugs found on the way

Each would have produced wrong numbers rather than an error:

cft_nir mapped to mcft_nir.nc holding monthly cft_nir No WHEP run has ever written that file. All nine runs under LPJmL_runs/, 5.9.7 and 6.1.1 alike, write cft_nir.nc holding annual nir, 32 bands. Never surfaced because nothing calls it yet.
Reader assumed 12 time steps/year for every variable The per-CFT consumptive cubes are annual (nstep 1, mm/yr). Their time axis was decoded as months, so years= sliced the wrong range out of the file.
ncvar_get() drops length-1 dimensions A single-year slice of an annual per-CFT cube came back 3-D and its band axis was decoded as time, scrambling crops into years. Now collapse_degen = FALSE. Monthly cubes never hit this — a one-year slice is still 12 steps.

Verification

Facts checked against the 6.1.1 production run
(global_1901-2023_spinup_300_our_inputs_lpjml611), not guessed: vars
consump_water_b / consump_water_g, nstep 1, mm/yr, 32 bands, band 14
rainfed grassland, band 30 irrigated grassland. The mcft_nir.nc claim was
checked against every run in the archive.

  • 8 new tests (annual decode, band naming from file, annual years= slicing,
    band filtering, both abort paths).
  • Full suite [ FAIL 0 | PASS 6632 ] · air format clean · lint clean ·
    rcmdcheck 0 errors / 0 warnings / 0 notes.

Only bands = NULL paths run today, so nothing in main changes numerically.

Follow-up

#116 rebases onto this, drops its water-grazing-green pin (a country-aggregated
copy of exactly this LPJmL layer) and reads the grassland band from here
instead, per [[reuse what main already has]]. Note for @eduaguilera: that
extension charges grazing the full managed-grassland ET, consistent with
build_grassland_land_extension(grassland_metric = "occupation"). The
alternative — only the area needed to feed the animals, as in Schyns et al.
(2019) — is ~6x smaller and drives ~72% of the green water total. Proceeding on
the occupation basis for consistency with land, but it is your call to confirm.

🤖 Generated with Claude Code

`build_water_balance()` summed the per-CFT consumptive-water cubes over
every crop band, so the only figure it could report was the whole-cell
total. A grazing water footprint needs the grassland band alone; charging
grazing the whole-cell total would add every wheat field and rice paddy in
the cell to the pasture.

Adds `bands =`, which restricts the consumptive-water and `cft_nir` terms
to named bands and defaults to NULL (every band, unchanged behaviour).
Selection is by the `band_name` the file carries in NamePFT, never by
index: which crop a given index denotes is a property of how the run was
configured, so a positional filter would keep charging band 14 in a run
whose band 14 is a different crop. An unknown name, or an input with no
band names to match on, aborts rather than silently returning the
whole-cell total.

Getting there needed three reader fixes, all of which would have produced
wrong numbers rather than an error:

- The cft_nir map entry named `mcft_nir.nc` holding monthly `cft_nir`.
  No WHEP run has ever written that file -- all nine runs under
  LPJmL_runs/, 5.9.7 and 6.1.1 alike, write `cft_nir.nc` holding annual
  `nir` with 32 bands. Never surfaced because nothing calls it yet.
- The reader assumed 12 time steps per year for every variable, but the
  per-CFT consumptive-water cubes are annual (nstep 1, mm/yr), so their
  time axis was decoded as months and `years=` sliced the wrong range.
- ncvar_get() drops length-1 dimensions, so a single-year slice of an
  annual per-CFT cube came back 3-D and its band axis was decoded as
  time, scrambling crops into years. Now collapse_degen = FALSE.

Facts verified against the 6.1.1 production run
(global_1901-2023_spinup_300_our_inputs_lpjml611): vars consump_water_b /
consump_water_g, nstep 1, mm/yr, 32 bands, band 14 "rainfed grassland"
and band 30 "irrigated grassland".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lbm364dl
lbm364dl merged commit f8e55e5 into main Aug 7, 2026
10 checks passed
lbm364dl added a commit that referenced this pull request Aug 10, 2026
Regenerates water-grazing-green from the 6.1.1 production run, per
country AND year (1901-2023, 21,365 rows, 175 countries), replacing one
2000-2009 climatological mean applied to every year since 1850.

Two changes, both of which the old pin got wrong:

1. ANNUAL. The underlying cube is annual, so freezing it at one decade
   threw information away for no reason -- and made the coefficient join
   year-free, which is exactly what whep#669's audit exists to surface.
   The join now keys on (year, area_code), so `.grazing_green_water`
   leaves the territorial-join baseline instead of being added to it:
   the enumerated list shrinks rather than growing, which is what that
   gate asks for.

2. CORRECTED GREEN = green + blue. LPJmL 6.x books infiltrating rain as
   blue (whep#710, fixed in lbm364dl/LPJmL#3). Band "rainfed grassland"
   cannot be irrigated, so its blue is exactly that misassignment.
   Taking green+blue from the existing run is sound because the fix is a
   PURE REPARTITION: rebuilding the parent commit and running both
   binaries on one config moves total consumptive water by +0.0011% and
   gives pre(green+blue) == post(green) cell by cell. No 12-hour rerun
   was needed.

Global total on the new basis: 15,735 km3/yr, against 13,683 for the
5.9.7 pin -- +15%, alongside the +13.5% rise in global ET the run's
README documents. Grassland area unchanged at 3,253 Mha.

Cross-checked against the decade computation: 172 of 175 countries agree
to 0.01%. The three that do not have grassland in only 1, 4 and 10 of
those years, and the old decade mean diluted their intensity by
averaging zero-grassland years into the millimetres -- understating them
by 10x and 2.5x. The annual values do not.

Note for review: at 15,735 km3/yr the occupation-basis total sits ABOVE
the published 8,258-12,960 km3/yr range for total grazing-land ET. The
5.9.7 value already did. That is the occupation-basis choice, not a
defect, and is the open question on #681/#116.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant