Skip to content

[PROBE: mass] add groundwater vertical-datum invariance - #151

Merged
chrimerss merged 7 commits into
Flood-Lab:mainfrom
JustinLoiii:probe/131-groundwater-datum-invariance
Oct 1, 2026
Merged

chrimerss merged 7 commits into
Flood-Lab:mainfrom
JustinLoiii:probe/131-groundwater-datum-invariance

Conversation

@JustinLoiii

Copy link
Copy Markdown
Contributor

Summary

Implements the accepted groundwater vertical-datum invariance probe from #131.

The probe runs identical single-aquifer and river cases under:

  • control
  • datum_up
  • datum_down

All absolute heads and elevations are shifted together while hydraulic differences, storage properties, conductance, recharge, timestep, and seed remain unchanged.

The new datum_flux_invariance criterion:

  • compares the non-cancelling gw_sw_exchange trajectory;
  • uses the control gross exchange as the denominator;
  • applies the 1e-6 relative tolerance and 1e-5 mm floor;
  • rejects a zero-exchange implementation with a 0.1 mm activity safeguard.

The PR includes exact, datum-dependent, and zero-exchange reference models, documentation, site metadata, and archive synchronization.

Validation

  • ht validate passes
  • ht gate --probe mass/groundwater-datum-invariance passes
  • documentation synchronization tests pass

Closes #131

@chrimerss

Copy link
Copy Markdown
Contributor

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated review by Claude (needs-discussion), not a maintainer approval.

The PR adds mass/groundwater-datum-invariance: three variants (control, datum_up, datum_down) shift every absolute head and elevation by ±250 m while holding hydraulic differences fixed, and a new paired criterion datum_flux_invariance requires the gw_sw_exchange trajectory to be unchanged, with a 0.1 mm activity guard. Three subprocess reference models, docs, site and archive rows come with it.

The physics is right and the gate separates the three baselines, but the probe is currently only testable against code written in this PR, and two things would give real models wrong verdicts:

  • requires.static names none of the keys the shift actually moves (aquifer_initial_head_m, aquifer_top_m, aquifer_bottom_m). A groundwater model that sets its own initial head or grid elevations — modflow6 does both, with a hard-coded TOP=20/BOTM=0 and a .get(..., 10.0) default — is scored as VIOLATION on a datum it was never required to read, instead of N/A (INCOMPATIBLE).
  • generate.py puts datum_offset_m in static.json, which the harness hands to the model verbatim. That is the exact transform the probe asks the model to be blind to, and reference_datum_exact only passes state_bounds because it reads it.

Separately, the ten models/result.csv rows are not what ht run writes: the detail string does not exist in the harness, and modflow6's reason should be INCOMPATIBLE, not INCOMPLETE. Per docs/writing-a-probe.md, gating on an exact reference alone also needs a README justification and a physical model passing in the archive; neither is present.

Posted by the pr-review workflow. Run

Comment thread probes/mass/groundwater-datum-invariance/probe.yaml Outdated
Comment thread probes/mass/groundwater-datum-invariance/generate.py Outdated
Comment thread models/result.csv Outdated
Comment thread probes/mass/groundwater-datum-invariance/probe.yaml
Comment thread site/index.html Outdated
Comment thread README.md
Comment thread site/index.html
Comment thread tests/test_report_detail.py Outdated
Comment thread ROADMAP.md
Comment thread models/reference_datum_exact/ht_adapter.py Outdated

@Barbhuiya12 Barbhuiya12 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at 5040542, which is current with main. ht validate (34 probes, 79 models), ht gate --probe mass/groundwater-datum-invariance and the full pytest -q (1232 passed) pass here. The probe itself is sound. A pure translation of every absolute elevation can't change a Darcy exchange, the ±250 m shift is large enough to push datum_down to negative elevations where a hard-coded datum would show, and exchange_directions now closes the constant-flux route the automated review raised. MODFLOW 6's archived residual of 2.1e-12 mm against an allowance of about 1e-5 mm also answers the concern I would otherwise have had: an iterative solver doesn't come near the 1e-6 relative tolerance.

One thing has to change before this merges, though.

MODFLOW 6 loses the pass another probe rests on

6.7.0-adapter.2 reads aquifer_top_m and aquifer_bottom_m by subscript and declares them in needs_static, where adapter.1 hard-coded TOP = 20.0 and BOTM = 0.0. mass/gw-sw-exchange-consistency's generator supplies neither key, so this PR's own archive row now reads:

2026-09-29,modflow6,6.7.0-adapter.2,...,mass/gw-sw-exchange-consistency,N/A,INCOMPATIBLE,...,"static does not provide aquifer_top_m...

At adapter.1 that row was PASS. It matters beyond MODFLOW's own standing. docs/writing-a-probe.md:326-333 names exactly that archived pass as the independent check that lets mass/gw-sw-exchange-consistency gate on an exact reference alone ("as modflow6 does there. That archived row is the independent check the rule above asks for"). After this PR, that probe has no physical model passing it. The README's PASS, 1 of 1 probe scored for modflow6 stays true only because the one probe it's scored on has changed.

The smallest fix keeps both probes: declare the two keys under uses_static rather than needs_static, and fall back to the old geometry when a case doesn't supply them.

top = float(static.get("aquifer_top_m", 20.0))
botm = float(static.get("aquifer_bottom_m", 0.0))

That is the documented fallback uses_static exists for, and gw-sw-exchange-consistency then runs exactly the grid it ran at adapter.1. Then re-archive modflow6 on both probes. The other way is to add the two keys to gw-sw-exchange-consistency's generator, but that changes a merged probe's case to suit this one, and I'd avoid it.

Two replies that don't match the head

  • docs/writing-a-probe.md: the reply to the reference-table comment says it now lists the three references. At 5040542 it has the datum_flux_invariance criterion row but none of reference_datum_exact, reference_datum_dependent or reference_zero_exchange. README.md has them.
  • ROADMAP author: the entry at ROADMAP.md:100 still ends without Contributed by Zhenjiang Wu (Xi'an Jiaotong University)., which every other contributed entry carries. That thread has no reply, so it may just have been missed.

Smaller points, not blocking

  1. The criterion reads only gw_sw_exchange. Each variant's gw is held only to its own balance, never to the control. Take a model whose exchange is invariant but whose storage is measured from a fixed level, gw = S_y · (head − z_fixed). It stays self-consistent in every variant, so groundwater_balance passes. Today it's caught only incidentally: with z_fixed = 0, datum_down drives gw negative and state_bounds fires. With z_fixed anywhere below −160 m it passes the probe. Comparing the change in gw between variants, which a datum shift can't move, would close that with one more line.
  2. groundwater_balance still sits at its capped 0.05 mm floor at this case's storage size. The author reply says the conductance change lifts the signal above it, which is true for the exchange. The precondition itself stays loose, though. That's fine as long as groundwater_balance isn't what's pinned here, and it isn't.

Requesting changes for the MODFLOW 6 regression alone. With uses_static and the two rows re-archived, I'd expect to approve.

@Barbhuiya12

Copy link
Copy Markdown
Collaborator

A correction to smaller point 1 in my review. Comparing the change in gw between variants would not catch that model: S_y · (head − z_fixed) changes by the same amount in every variant, and only its level moves. What closes it is comparing gw itself. The aquifer bottom shifts with the datum, so storage above it is identical across variants, which is exactly what reference_datum_exact reports now. Still not blocking.

@JustinLoiii

Copy link
Copy Markdown
Contributor Author

Reviewed at 5040542, which is current with main. ht validate (34 probes, 79 models), ht gate --probe mass/groundwater-datum-invariance and the full pytest -q (1232 passed) pass here. The probe itself is sound. A pure translation of every absolute elevation can't change a Darcy exchange, the ±250 m shift is large enough to push datum_down to negative elevations where a hard-coded datum would show, and exchange_directions now closes the constant-flux route the automated review raised. MODFLOW 6's archived residual of 2.1e-12 mm against an allowance of about 1e-5 mm also answers the concern I would otherwise have had: an iterative solver doesn't come near the 1e-6 relative tolerance.

One thing has to change before this merges, though.

MODFLOW 6 loses the pass another probe rests on

6.7.0-adapter.2 reads aquifer_top_m and aquifer_bottom_m by subscript and declares them in needs_static, where adapter.1 hard-coded TOP = 20.0 and BOTM = 0.0. mass/gw-sw-exchange-consistency's generator supplies neither key, so this PR's own archive row now reads:

2026-09-29,modflow6,6.7.0-adapter.2,...,mass/gw-sw-exchange-consistency,N/A,INCOMPATIBLE,...,"static does not provide aquifer_top_m...

At adapter.1 that row was PASS. It matters beyond MODFLOW's own standing. docs/writing-a-probe.md:326-333 names exactly that archived pass as the independent check that lets mass/gw-sw-exchange-consistency gate on an exact reference alone ("as modflow6 does there. That archived row is the independent check the rule above asks for"). After this PR, that probe has no physical model passing it. The README's PASS, 1 of 1 probe scored for modflow6 stays true only because the one probe it's scored on has changed.

The smallest fix keeps both probes: declare the two keys under uses_static rather than needs_static, and fall back to the old geometry when a case doesn't supply them.

top = float(static.get("aquifer_top_m", 20.0))
botm = float(static.get("aquifer_bottom_m", 0.0))

That is the documented fallback uses_static exists for, and gw-sw-exchange-consistency then runs exactly the grid it ran at adapter.1. Then re-archive modflow6 on both probes. The other way is to add the two keys to gw-sw-exchange-consistency's generator, but that changes a merged probe's case to suit this one, and I'd avoid it.

Two replies that don't match the head

  • docs/writing-a-probe.md: the reply to the reference-table comment says it now lists the three references. At 5040542 it has the datum_flux_invariance criterion row but none of reference_datum_exact, reference_datum_dependent or reference_zero_exchange. README.md has them.
  • ROADMAP author: the entry at ROADMAP.md:100 still ends without Contributed by Zhenjiang Wu (Xi'an Jiaotong University)., which every other contributed entry carries. That thread has no reply, so it may just have been missed.

Smaller points, not blocking

  1. The criterion reads only gw_sw_exchange. Each variant's gw is held only to its own balance, never to the control. Take a model whose exchange is invariant but whose storage is measured from a fixed level, gw = S_y · (head − z_fixed). It stays self-consistent in every variant, so groundwater_balance passes. Today it's caught only incidentally: with z_fixed = 0, datum_down drives gw negative and state_bounds fires. With z_fixed anywhere below −160 m it passes the probe. Comparing the change in gw between variants, which a datum shift can't move, would close that with one more line.
  2. groundwater_balance still sits at its capped 0.05 mm floor at this case's storage size. The author reply says the conductance change lifts the signal above it, which is true for the exchange. The precondition itself stays loose, though. That's fine as long as groundwater_balance isn't what's pinned here, and it isn't.

Requesting changes for the MODFLOW 6 regression alone. With uses_static and the two rows re-archived, I'd expect to approve.

Thanks for the detailed review. I addressed the MODFLOW 6 compatibility regression and the documentation issues.

The MODFLOW 6 adapter now declares aquifer_top_m and aquifer_bottom_m under uses_static and falls back to the previous geometry when older probes do not provide them:

  • TOP = 20.0
  • BOTM = 0.0

I re-ran and re-archived both relevant probes:

  • mass/gw-sw-exchange-consistency: PASS
  • mass/groundwater-datum-invariance: PASS

The archived MODFLOW 6 standing is now 2/2 probes. I also updated the README and site counts, added reference_datum_exact, reference_datum_dependent, and reference_zero_exchange to docs/writing-a-probe.md, and added the contributor attribution to ROADMAP.md.

Validation results:

  • ht validate: passed
  • ht gate --probe mass/groundwater-datum-invariance: passed
  • Full test suite: 1224 passed, 8 skipped
  • Docker MODFLOW 6 runs: both passed

Regarding the smaller point, thanks for the clarification. The probe compares the absolute gw level using storage relative to the datum-shifted aquifer bottom, so no further code change is needed for that point.

Barbhuiya12
Barbhuiya12 previously approved these changes Oct 1, 2026

@Barbhuiya12 Barbhuiya12 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at 44f24f1. The regression is fixed the way I hoped: aquifer_top_m and aquifer_bottom_m are now uses_static with the old 20 m / 0 m geometry as the fallback, so mass/gw-sw-exchange-consistency runs the grid it ran at adapter.1, and modflow6 is archived PASS on both probes. The guide now lists the three references and the ROADMAP entry carries the attribution. ht validate (34 probes, 79 models), both probe gates and the docs-sync, datum and report tests (282) pass here.

Approving on the substance. Three things before it can land, none of them physics:

  1. It conflicts with main. #103 merged at 62e003c and touched the same three tail files: README.md, models/result.csv and site/index.html. A merge of main is needed. The push will dismiss this approval, and I'll re-approve on the merged head.
  2. One stale row. models/result.csv still has 2026-09-29,modflow6,6.7.0-adapter.2,...,mass/gw-sw-exchange-consistency,N/A,INCOMPATIBLE, from the adapter that required the two keys, next to the 2026-09-30 PASS under the same version string. That adapter never merged, so I'd drop the 09-29 row (and the duplicate 09-29 datum row) rather than bump the version. Then adapter.2 means one thing in the archive.
  3. On my smaller point, for the record rather than as a request: reference_datum_exact does report storage above the shifted bottom, but datum_flux_invariance compares only gw_sw_exchange, so nothing in the probe holds another model's gw level to the control's. It's fine to leave that for a later version. It just isn't covered by the criterion yet.

…er-datum-invariance

# Conflicts:
#	README.md
#	models/result.csv
#	site/index.html
@JustinLoiii

Copy link
Copy Markdown
Contributor Author

I merged the latest main (including #103), removed the stale MODFLOW 6 adapter.2 archive rows, and re-archived MODFLOW 6 across all 35 probes. The full test suite passes with 1270 passed and 8 skipped. The branch is now mergeable; please re-review the merged head when convenient.

@Barbhuiya12 Barbhuiya12 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving at 9071746, which is current with main. Since 44f24f1 the only changes outside the archive are main's, from #103. The two stale 09-29 adapter.2 rows are gone, and modflow6 is re-archived on all 35 probes, with PASS on both mass/groundwater-datum-invariance and mass/gw-sw-exchange-consistency. ht validate (35 probes, 80 models), both groundwater gates and the docs-sync, datum and report tests (289) pass here.

@chrimerss
chrimerss merged commit 99f6c1a into Flood-Lab:main Oct 1, 2026
chrimerss added a commit that referenced this pull request Oct 1, 2026
…eeds

Follow-up to #151.

- README: thirty-five probes, twenty-three under mass, and two
  groundwater-exchange probes where it said one.
- models/result.csv: the two modflow6 6.7.0-adapter.2 PASS rows were run
  on non-gate seeds. They are replaced by the rows that
  `ht run --model modflow6 --gate-seeds` writes. The verdict is unchanged,
  and the other 33 rows match byte for byte.
- The probe README says why the probe gates on reference_datum_exact
  alone, names modflow6 as the independent check and how to re-run it,
  states that the criterion compares only the exchange, not `gw`, and
  records that the allowance is set for double precision: an exact
  float32 implementation fails it by 1.7e-4 to 2.7e-2 mm.
- site: the three pillars grow by 12 px and the steps below them move
  down, so the twenty-third mass entry sits inside its pillar. The
  English and Spanish labels are shortened to fit its width.
Barbhuiya12 added a commit to Barbhuiya12/hydroturing that referenced this pull request Oct 2, 2026
…nowpack-ripening

Conflicts in README.md, models/result.csv and site/index.html. Counts are
re-derived for both probes: thirty-six, twenty-three mass, eight energy, five
momentum. The flowchart keeps main's taller pillars with the ripening entry
in the energy one. sacsma_snow17 1.2.0 is archived on
mass/groundwater-datum-invariance (N/A) after main's 1.1.0 row, and
modflow6 6.7.0-adapter.2 on this probe (N/A); the site chart data is
regenerated with scripts/site_standings.py.
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.

[PROBE] Groundwater vertical-datum invariance

3 participants