Skip to content

Build leaderboard/calibration MODIS LAI obs as a monthly mean#1806

Open
AlexisRenchon wants to merge 2 commits into
mainfrom
ar/fix_leaderboard_lai_monthly_mean
Open

Build leaderboard/calibration MODIS LAI obs as a monthly mean#1806
AlexisRenchon wants to merge 2 commits into
mainfrom
ar/fix_leaderboard_lai_monthly_mean

Conversation

@AlexisRenchon

@AlexisRenchon AlexisRenchon commented Jul 14, 2026

Copy link
Copy Markdown
Member

Now (this PR)

FlagshipCarbonMetrics_sim_annual_time_avg_over_all_years

Before this PR

image

Problem

LAI is prescribed from MODIS, so the leaderboard's SIM and OBS LAI should be ~1:1 — but the MON lines were visibly offset.

Root cause (temporal convention mismatch): the model's lai diagnostic is a calendar-monthly mean dated at the month start, which effectively represents mid-month. get_modis_lai_obs_var built the obs as an instantaneous month-start snapshot, which the mean leads by ~half a month — so SIM ran high on the spring green-up and low on the autumn senescence, even though sim and obs come from the same MODIS files. It is not a mask issue.

The other leaderboard variables already date their obs at the month start (transform_dates!(firstdayofmonth)); LAI was the only snapshot.

Fix

Sample the linearly-interpolated MODIS at the middle of each fully observed calendar month and label it at the month start. For this near-piecewise-linear product the mid-month value equals the true monthly mean to < 0.002 m² m⁻², while the month-start snapshot differs by 0.218 m² m⁻².

The MON lines now sit on top of each other, and global bias is −0.0055 m² m⁻² (≈0, unchanged — the fix is a seasonal-phase correction that cancels in the annual mean).

Per @ph-kev's review, the relabel goes through ClimaAnalysis.transform_dates like the other loaders, with the month selection and midpoint split into _fully_observed_month_starts and _mid_month. That refactor is behavior-preserving (bitwise identical), so the numbers above are unaffected.

Residual RMSE (~0.34 m² m⁻²) — expected, not reducible

Model and MODIS share the 1°×1° resolution, so there is no resolution loss. But the grids are registered a half-cell apart (model at integer degrees, MODIS centers at half-integer), and the model ingests MODIS nearest-neighbor while the leaderboard resamples obs bilinear. The residual is a zero-mean dipole hugging sharp LAI boundaries (Amazon/Congo rims, forest–savanna edges, coasts): interior cells agree to ~0.08, top-quartile-gradient cells carry ~0.64, corr(|SIM−OBS|, |∇LAI|) = 0.76. Matching the obs to nearest-neighbor makes it worse (0.56); reaching ~0 would require passing obs through the model's exact regrid chain, defeating the point of an independent obs.

The same floor exists for other variables, but their RMSE is dominated by genuine physical model-vs-obs differences (ER 1.29, NEE 1.27, GPP 0.83 g m⁻² day⁻¹) — the signal the leaderboard is meant to show. LAI is the only prescribed variable, hence the only one where a non-physical floor matters.

Note on scope

get_modis_lai_obs_var also backs the LAI calibration target, so this shifts that target by up to ~0.2 m² m⁻² in some cells/seasons. Correct direction, but flagging it for in-flight LAI calibrations.

🤖 Generated with Claude Code

@AlexisRenchon
AlexisRenchon requested review from kmdeck and ph-kev July 14, 2026 18:39
(seconds_since_start(d) + seconds_since_start(d + Dates.Month(1))) / 2 for d in month_starts
]
obs_var = ClimaAnalysis.resampled_as(obs_var; time = target_seconds)
obs_var = ClimaAnalysis.resampled_as(obs_var; time = mid_month_seconds)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this very difficult to read. If the problem is that the times are misaligned (e.g. the dates are on the 15th of the month instead of the start), why don't you use ClimaAnalysis.transform_dates! with Dates.firstdayofmonth instead? Is there a reason why we can't do that?

@AlexisRenchon AlexisRenchon Jul 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — the refactor now uses transform_dates, just not as a replacement.

It works for ERA5/ILAMB/inversion (L198/L300/L521) because those are monthly series that happen to be dated mid-month. MODIS isn't: the native samples run on a 30-day cadence that drifts against the calendar, so a month can hold two or none. In 2008, Jan and Mar hold two, Feb and Dec none:

2008-01-01, 2008-01-31, 2008-03-01, 2008-03-31, 2008-04-30, 2008-05-30, ...

Over 2000–2020 firstdayofmonth collapses 252 samples onto 210 labels, so it raises "Dates are not unique after transforming dates" (I ran it against the artifact). And there's no February sample to relabel — interpolation is what fills those months.

But you're right that it's the tool for the relabel, just after the resampling: each sample then sits in its own month and maps one-to-one onto the month start. That replaces the hand-rolled OutputVar reconstruction.

On readability — agreed. Month selection and midpoint are now _fully_observed_month_starts and _mid_month, so the body reads as: pick the fully observed months → resample to midpoints → relabel to starts. Output is bitwise identical to before.

@AlexisRenchon
AlexisRenchon force-pushed the ar/fix_leaderboard_lai_monthly_mean branch from b8f5c49 to 1dbd536 Compare July 15, 2026 17:03
@AlexisRenchon
AlexisRenchon requested a review from ph-kev July 15, 2026 17:04
@AlexisRenchon
AlexisRenchon force-pushed the ar/fix_leaderboard_lai_monthly_mean branch from 1dbd536 to 90488dd Compare July 15, 2026 17:07
`get_modis_lai_obs_var` resampled MODIS LAI onto an instantaneous
month-start snapshot, but the model's `lai` diagnostic is a calendar-
monthly *mean* dated at the month start. A month-mean labeled at the
month start effectively represents mid-month, so the snapshot lagged it
by ~half a month: a prescribed-MODIS-LAI run then disagreed with the LAI
obs on the seasonal cycle (SIM high on the spring green-up, low on the
autumn senescence) and reported a spurious monthly RMSE even though sim
and obs share the same source.

Sample the linearly-interpolated MODIS at the middle of each fully
observed calendar month and label the sample at the month start. For
this near-piecewise-linear product the mid-month value equals the
monthly mean to < 0.002 m^2 m^-2, so the seasonal cycle now matches the
diagnostic. The residual annual-mean RMSE (~0.34 m^2 m^-2, zero global
bias) is regridding at sharp LAI gradients, not a data mismatch.

The relabel goes through `ClimaAnalysis.transform_dates`, matching the
ERA5, ILAMB and inversion loaders. It is valid only after the resampling,
once each sample sits inside its own calendar month: the native samples
drift against the calendar, so a month may hold two or none.

This function also backs the LAI calibration target, so that target
shifts by up to ~0.2 m^2 m^-2 in some cells/seasons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AlexisRenchon
AlexisRenchon force-pushed the ar/fix_leaderboard_lai_monthly_mean branch from 90488dd to 258cf70 Compare July 15, 2026 17:14
Comment on lines +688 to +692
The model's `lai` diagnostic is a calendar-monthly mean dated at the month start,
so it represents mid-month. Sampling MODIS at each month's midpoint reproduces
that convention (to < 0.002 m^2 m^-2 for this near-piecewise-linear product); a
month-start snapshot would instead lag the mean by ~half a month, biasing SIM
high on the spring green-up and low on the autumn senescence.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this confusing to read because I don't know what the dates of the LAI observational data. It would be easier to just list them and describe the transformations we need to do to fix them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the docstring now lists the actual native dates (2008: 01-01, 01-31, 03-01, 03-31, 04-30, 05-30, 06-29, …, so Feb/Dec hold none and Jan/Mar hold two) and walks through the three transforms against them.

Comment on lines +711 to +713
obs_var = ClimaAnalysis.resampled_as(
obs_var;
time = seconds_since_start.(_mid_month.(month_starts)),

@ph-kev ph-kev Jul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to pass Dates.DateTime for the time keyword argument. I am not sure if this simplifies things.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resampled_as uses the time kwarg directly as the coordinate, which is stored as seconds since the reference date (no DateTime conversion), so passing DateTime would not work without ClimaAnalysis support. Kept the seconds conversion, now with a comment noting why.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. I was mistaken. I thought it was implemented for this function, but it isn't.

List the concrete native sample cadence and walk through the three
transforms; note why the resample time coordinate is seconds, not DateTime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AlexisRenchon
AlexisRenchon requested a review from ph-kev July 16, 2026 17:25
Comment on lines +688 to +690
The model prescribes MODIS LAI by linear-in-time interpolation and reports `lai`
as a calendar-monthly mean dated at the month start. This obs reproduces that
convention so a prescribed-LAI run matches it one-to-one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels strange to mention what the model does and the diagonstic is only valid when it is monthly mean. You can just state that this dataset is postprocessed, so that it match the convention that they are monthly mean with dates being on the first day of each month.

Comment on lines +701 to +712
1. `_fully_observed_month_starts` keeps the calendar months whose start and next
start both lie within the native span, so the midpoint never extrapolates.
2. `resampled_as` linearly interpolates the native samples to each month's
`_mid_month` midpoint. For this near-piecewise-linear product the midpoint
equals the monthly mean to < 0.002 m^2 m^-2; a month-start snapshot would
instead lag the mean by ~half a month, biasing SIM high on the spring
green-up and low on the autumn senescence.
3. `transform_dates(_, firstdayofmonth)` relabels each midpoint to its month
start, aligning with the model diagnostic and the leaderboard's
`resampled_as(obs, sim)`. Step 1 leaves exactly one sample per month, so the
relabel is one-to-one; applied to the drifting native samples it would
instead collide the two-per-month months onto a single label.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is verbose and seems overly complicated to what is being done. It is also easier to follow if you use the example above to show what each transformation is supposed to do.

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.

2 participants