Skip to content

fix(hype): score the calibration objective on the calibration period - #393

Merged
DarriEy merged 1 commit into
developfrom
fix/hype-calibration-period
Jul 28, 2026
Merged

DarriEy merged 1 commit into
developfrom
fix/hype-calibration-period

Conversation

@DarriEy

@DarriEy DarriEy commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Problem

HYPEWorker.calculate_metrics restricted scoring to the calibration period like this:

calib_period = None
try:
    calib_period = config.optimization.calibration_period
except (AttributeError, TypeError):
    pass
if calib_period:
    ...   # filter sim/obs to the window

calibration_period lives on the domain section (CALIBRATION_PERIOD) — OptimizationConfig has no such field. So the AttributeError fired on every call, was caught and passed over, calib_period stayed None, the filter was skipped, and nothing was logged.

Every HYPE calibration therefore scored the entire overlapping record, evaluation period included.

Two consequences — the same pair recently fixed across the JAX model packages, reached by a different route:

  1. Evaluation-period leakage. The optimizer trained on held-out data, so the split-sample test was not honest.
  2. Mismatched reporting. The score the optimizer reported covered a different window than the Calib_* metrics written at final evaluation.

On the P3 Bow-at-Banff calibration ensemble, all 14 HYPE runs disagreed with their own final evaluation, by 3e-3 to 5.9e-2 — across every algorithm, not just the gradient-based ones. That breadth is what distinguishes this from the gradient-path bugs.

Fix

Resolve through the canonical CALIBRATION_PERIOD key, exactly as _calculate_multi_gauge_metrics in this same class already does (line 637), with a typed-config fallback.

When no period resolves, warn instead of silently scoring everything. The silence is what hid this for so long: a try/except AttributeError: pass around a config read gives identical behaviour whether the key is absent, misspelled, or moved.

Verification

HYPE/abc on Bow-at-Banff, before and after:

best_score Calib_KGE |diff| Eval_KGE
before 0.824691 0.819599 5.1e-03 0.805
after 0.832525 0.832523 2.0e-06 0.550

The residual now matches the other models (HBV 5.4e-07, TOPMODEL 3.6e-09, FUSE 9.6e-07).

The Eval_KGE drop from 0.805 to 0.550 is the point, not a regression. The evaluation period was previously part of what the optimizer trained on, so held-out performance was overstated. Any previously published HYPE evaluation-period number is affected.

This is 1 of 15 HYPE configs; the remaining 14 are re-running and I'll follow up if any disagrees.

Related

Companion to the calibration-window work in #392 and the seven model-package PRs (jHBV, jTOPMODEL, jSAC-SMA, jXAJ, jHEC-HMS, jFUSE, cFUSE), all merged.

🤖 Generated with Claude Code

calculate_metrics read config.optimization.calibration_period, but the
field lives on the domain section (CALIBRATION_PERIOD) — OptimizationConfig
has no such attribute. The AttributeError was caught and passed over, so
calib_period stayed None, the filtering block was skipped, and no warning
was emitted. Every HYPE calibration therefore scored the entire overlapping
record, evaluation period included.

Two consequences: the optimizer trained on held-out data, and the score it
reported covered a different window than the Calib_* metrics written at
final evaluation. On the P3 Bow-at-Banff ensemble all 14 HYPE runs
disagreed with their final evaluation by 3e-3 to 5.9e-2 — every algorithm,
not just gradient-based ones.

Resolve through the canonical CALIBRATION_PERIOD key, exactly as
_calculate_multi_gauge_metrics in this same class already does, with a
typed-config fallback. When no period resolves, warn instead of silently
scoring everything — the silence is what hid this.

Verified on Bow-at-Banff: HYPE/abc goes from best 0.8247 vs Calib_KGE
0.8196 (5.1e-3) to best 0.8325 vs 0.8325 (2.0e-6). Its held-out Eval_KGE
drops from 0.805 to 0.550, which is the point — the evaluation period was
previously part of what the optimizer trained on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DarriEy
DarriEy merged commit e08d70e into develop Jul 28, 2026
7 checks passed
@DarriEy
DarriEy deleted the fix/hype-calibration-period branch July 28, 2026 21:09
DarriEy pushed a commit that referenced this pull request Jul 30, 2026
…d area fixes

468 runs, 196 rows changed. 137 are on the corrected basis — regenerated
after the catchment-area fix landed at 2026-07-28T18:03 — and 77 predate it.

The corrected rows carry the five fixes from this cycle: the shared
calibration window (#392), HYPE reading its period from a field that does
not exist (#393), catchment area resolved from two different shapefiles
(#394), point-domain GRU_area written in square degrees (#396), and the
seven model-package leakage fixes now on PyPI.

Every affected model's calibration objective now agrees with its own final
evaluation. Before this cycle the two disagreed by up to 0.35 KGE; across
118 verified runs the worst residual is 5.8e-05 and most sit near 1e-06:

    HBV        17/17   2.6e-07      HYPE       15/15   3.9e-06
    FUSE       15/15   1.4e-06      SACSMA     17/17   5.9e-06
    TOPMODEL   17/17   4.2e-06      HECHMS     17/17   5.8e-05
    SUMMA       3/15   0.0e+00

The 77 pre-fix rows are identifiable by run_completed < 2026-07-28T18:03
and are dominated by SUMMA (32), which needs ~4.8 h per config and could
not be regenerated before this machine was returned. GR (21), GSFLOW (8),
JFUSE (6) and RAVEN (4) were outside this cycle's scope. Re-running any of
them refreshes the rows in place, as the collector is designed for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DarriEy added a commit that referenced this pull request Jul 30, 2026
…d area fixes (#397)

* repro(macos_p3repro): refresh metrics after the calibration-window and area fixes

468 runs, 196 rows changed. 137 are on the corrected basis — regenerated
after the catchment-area fix landed at 2026-07-28T18:03 — and 77 predate it.

The corrected rows carry the five fixes from this cycle: the shared
calibration window (#392), HYPE reading its period from a field that does
not exist (#393), catchment area resolved from two different shapefiles
(#394), point-domain GRU_area written in square degrees (#396), and the
seven model-package leakage fixes now on PyPI.

Every affected model's calibration objective now agrees with its own final
evaluation. Before this cycle the two disagreed by up to 0.35 KGE; across
118 verified runs the worst residual is 5.8e-05 and most sit near 1e-06:

    HBV        17/17   2.6e-07      HYPE       15/15   3.9e-06
    FUSE       15/15   1.4e-06      SACSMA     17/17   5.9e-06
    TOPMODEL   17/17   4.2e-06      HECHMS     17/17   5.8e-05
    SUMMA       3/15   0.0e+00

The 77 pre-fix rows are identifiable by run_completed < 2026-07-28T18:03
and are dominated by SUMMA (32), which needs ~4.8 h per config and could
not be regenerated before this machine was returned. GR (21), GSFLOW (8),
JFUSE (6) and RAVEN (4) were outside this cycle's scope. Re-running any of
them refreshes the rows in place, as the collector is designed for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* repro(macos_p3repro): final collection before the machine was returned

Refreshes 7 rows completed after the previous collection. The corrected /
pre-fix split is unchanged at 137 / 77.

This is the last collection from this box: the reproduction environment was
torn down while SUMMA was still working through its re-run. SUMMA's cma-es
config was ~40% through generation 20 of 50 and did not complete, so 32
SUMMA rows remain on the pre-fix basis, identifiable as before by
run_completed < 2026-07-28T18:03Z.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* repro(macos_p3repro): add the last runs before the machine was returned

469 runs. Adds gr4j, gsflow and a re-run of rhessys, plus the benchmark
config that finished during the final window.

The rhessys row is the end-to-end confirmation of the basin-area fix: it
now reports best_score 0.851883 against Calib_KGE 0.851883 (0.00e+00),
where before the fix the same run reported 0.851883 against 0.835738
(1.61e-02) — the same simulation measured against the worldfile's stale
2248.0606 km2 rather than the delineation's 2207.5038 km2.

Corrected / pre-fix now 138 / 77. The 77 remain identifiable by
run_completed < 2026-07-28T18:03Z and are dominated by SUMMA (32), which
needs ~4.8 h per config; its cma-es run reached generation 40 of 50 before
the machine went back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: DarriEy <darri.eythorsson@ucalgary.ca>
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