Skip to content

pwnlayers3: physical-correctness fixes (botm/thickness/offshore)#34

Open
bdestombe wants to merge 10 commits intomainfrom
pwnlayers3/physical-correctness-fixes
Open

pwnlayers3: physical-correctness fixes (botm/thickness/offshore)#34
bdestombe wants to merge 10 commits intomainfrom
pwnlayers3/physical-correctness-fixes

Conversation

@bdestombe
Copy link
Copy Markdown
Member

@bdestombe bdestombe commented Apr 29, 2026

Summary

Four narrow correctness fixes to pwnlayers3/layers.py (and one to pwnlayers/utils.py). No refactors, no new abstractions; each commit is a single targeted change.

  • Fix 1 (ebd2979) — Replace out.values = np.minimum.accumulate(...) with xr.apply_ufunc(..., dask=\"parallelized\") in fix_missings_botms_and_min_layer_thickness (both pwnlayers3/layers.py and pwnlayers/utils.py). The out.values = ... form silently no-ops on dask-backed arrays; the apply_ufunc form preserves the monotonic-decrease invariant under both numpy and dask.
  • Fix 2 (ae48346) — Add a post-merge non-negative-thickness assertion in get_pwn_layer_model mirroring the pre-merge check; the merge between PWN and REGIS can produce layer crossings that were previously smoothed silently.
  • Fix 3 (8b0b56c) — Make _guard_zero_thickness NaN-aware: zero_d = np.isclose(t, 0.0) | ~np.isfinite(t), log message updated to "zero or undefined thickness". Stops NaN thickness leaking through kh = KD/d into kh/kv inside a layer's own mask.
  • Fix 4 (9670188) — In get_botm, clip botm.where(isin_bounds) before fix_missings_... (rather than after); re-mask the post-fix result with any_layer_mask = isin_bounds.any(axis=0) and a final per-layer mask. Removes the structural cause of the NaN-thickness problem that Fix 3 defends against.

Reverted

  • Fix 7 (31a3550 + 50a7521 revert)noordzee_clip offshore mask in get_botm was reverted at the user's request; PWN data is preferred everywhere mask_pwn is True, including offshore.

Notes on overlap

  • Fixes 3 and 4 are partially redundant: Fix 4 removes the structural cause and Fix 3 defends downstream code defensively. Both are included; you may choose to drop Fix 3 (8b0b56c) before merge if you want only the structural fix. Keeping both is safe.
  • Fixes 5 and 6 from the original plan (log-space KD interpolation, log-space transition) were intentionally dropped — they added a complexity layer (extra branching, kwarg dispatch, non-positive guards) without an obvious complexity budget here.

Test plan

  • Re-run an existing pwnlayers3 model build script (e.g. 09pwnmodel2/03_pwnlayers2026.py) and confirm no ValueError(\"Merged layer thickness should be non-negative\") regression.
  • Spot-check that kh/kv arrays no longer contain NaN inside a layer's isin_bounds[k] mask (was previously occurring at e.g. the S21/W21 boundary per Edinsi 4.1).
  • If Fix 3 is dropped, re-run kh/kv smoke check to confirm Fix 4 alone is sufficient.

bdestombe and others added 10 commits April 29, 2026 14:08
Replace direct .values assignment with xr.apply_ufunc(dask="parallelized")
in fix_missings_botms_and_min_layer_thickness so the monotonic-decrease
invariant survives a future dask-backed array (the .values assignment
silently no-ops on dask arrays). Applied identically in both pwnlayers
and pwnlayers3 copies.
The pre-merge PWN dataset is validated for non-negative thickness, but
the merge with REGIS can introduce layer crossings. Validate the merged
layer model after fix_missings_botms_and_min_layer_thickness to catch
any remaining negative thicknesses.
Treat NaN thickness the same as zero thickness so kh/kv values do not
silently become NaN inside a layer's own mask when adjacent layer
boundaries differ (e.g. S21 vs W21). The mask now combines
np.isclose(thickness, 0.0) with ~np.isfinite(thickness), and the log
message reflects "zero or undefined thickness".
Move botm.where(isin_bounds) to before fix_missings_botms_and_min_layer_thickness
so the downward ffill only operates on cells inside at least one layer's mask.
After the fix, re-mask with any_layer_mask = isin_bounds.any(axis=0) to avoid
synthetic fill-from-top outside all boundaries, then re-apply the per-layer
isin_bounds mask to preserve the original NaN pattern.
Per Edinsi 3.1 (p.12), botm.geojson points extend west of the coastline,
and the nearest-neighbor fallback in get_botm could extrapolate layer
elevations into the seabed. The noordzee_clip polygon is now read inside
get_botm and used to drop offshore cells from the per-layer boundary
mask both before griddata interpolation and in the final clip, so
offshore cells return NaN.
Each split sublayer is now decided from its own mask_model_other and
transition_model only. Removes three cross-layer leakage points so
lower-layer transition rings track the actual per-layer extent rather
than being inflated to upper-layer extents.

- Drop fix_missings_botms_and_min_layer_thickness on the inputs and on
  the merged result in combine_two_layer_models. Validation already
  guarantees no NaN inside mask_model_other and a finite REGIS, so the
  cross-layer ffill+accumulate is no longer needed.
- Replace the post-merge fixer with a per-column np.minimum.accumulate
  along the layer axis only — no ffill, no cross-cell movement.
- Refactor _compute_thickness_ratios to take fallback="equal" and assign
  1/N where mask_valid is False; remove the cross-layer nearest-neighbor
  spread inside _adjust_botm_with_nearest_ratios.
- Rename _interpolate_ds/_interpolate_da to _interpolate_ds_inplace/
  _interpolate_da_inplace, assert per-layer that isvalid and ismissing
  are disjoint and complementary on icell2d, and guard against an empty
  isvalid set.
- Strengthen the no-NaN-inside-mask assertion to name the offending
  variable and point at upstream builders.
- Drop the redundant post-merge fix_missings_botms_and_min_layer_thickness
  call in pwnlayers3/layers.py — the merge function now guarantees
  per-column monotonicity itself.
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