Skip to content

fix(mmpde): finite SPD fallback for fully-degenerate metric evaluations (#352)#368

Merged
lmoresi merged 2 commits into
developmentfrom
bugfix/spd-sanitise-nan
Jul 15, 2026
Merged

fix(mmpde): finite SPD fallback for fully-degenerate metric evaluations (#352)#368
lmoresi merged 2 commits into
developmentfrom
bugfix/spd-sanitise-nan

Conversation

@lmoresi

@lmoresi lmoresi commented Jul 15, 2026

Copy link
Copy Markdown
Member

Closes #352.

The bug

_spd_sanitise — the guard that keeps the MMPDE energy's fractional powers finite — failed exactly when it was most needed: if EVERY eigenvalue on a rank was NaN/inf (fully-degenerate metric evaluation, e.g. a starved or badly-deformed partition under live convection), np.nanmax returned NaN, max(nan, 1.0) propagated it (Python max compares with >, false for NaN), and the "sanitised" output was NaN instead of the intended benign SPD fallback. Confirmed by direct probe on the installed build before any change.

The fix (two guards — the issue's sketch alone was insufficient)

  1. Floor anchor: computed from the finite eigenvalues only; unit scale when none exist.
  2. Basis fallback: the eigenvector basis of a fully-degenerate tensor is itself NaN, so even a finite floor rebuilt NaN through the einsum; such tensors now rebuild in the identity basis — the isotropic floor metric ("no information here, stay isotropic").

The helper is hoisted to module level (it captured nothing) for unit-testability; the mover's behaviour for any input with at least one finite eigenvalue is unchanged, and a genuine SPD batch passes through bit-identical (asserted).

Verification

  • New TestSPDSanitise in test_0850 (level_1/tier_a): all-NaN and all-inf batches → finite SPD; valid SPD → bit-identical passthrough; one degenerate tensor cannot perturb neighbours. Shown failing against the unmodified build first.
  • Gates: level_1 and tier_a 380 passed (376 baseline + the 4 new tests); np2 test_0855_mesh_smoothing_parallel 4 passed; style gate green (86 allowlisted).
  • No dependency/lockfile changes.

This is the live-mmpde robustness path (the surviving mover; see #346 discussion) — relevant to the planned MMPDE+NVB capstone.

Underworld development team with AI support from Claude Code

…enerate metric input (#352)

Two guards, because the issue's wmax sketch alone is insufficient:

1. The relative floor is anchored from the FINITE eigenvalues only,
   falling back to the unit scale when none exist - previously an
   all-NaN eigenvalue set made nanmax (and max(nan, 1.0)) NaN, so the
   floor itself was NaN.
2. The eigenvector basis of a fully-degenerate tensor is also NaN, so
   rebuilding through it re-introduces NaN even with a finite floor;
   such tensors fall back to the identity basis, i.e. the isotropic
   floor metric - the honest 'no information here' answer.

The helper is hoisted to module level (it captured nothing) so it is
unit-testable; the mover's call site is unchanged. Regression tests:
all-NaN and all-inf batches produce finite SPD output; a genuine SPD
batch passes through bit-identical; one degenerate tensor cannot
perturb its neighbours.

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings July 15, 2026 18:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…ACK-path dependent (adversarial review of #368)

Independent review probing found the 2x2 kernel returns quiet NaNs while
the general (3x3) kernel raises LinAlgError from the batched eigh BEFORE
the degeneracy guards run - and a batched eigh raises for the whole
batch if any one tensor fails. Unreachable via the 2D-only mover today,
but the 3D MMPDE capstone lands exactly here, and the 2x2 tests were
silently LAPACK-build dependent.

The eigh call now retries per tensor on LinAlgError: valid neighbours
keep their exact decomposition, tensors that still fail are marked
non-finite and rebuilt as the isotropic fallback. Docstring also states
the floor's absolute 1e-8 minimum honestly (pre-existing semantics: a
far-below-unit-scale valid batch would be floored).

Tests: 3x3 all-NaN batch; 3x3 mixed batch where one inf tensor makes
batched eigh raise - neighbours preserved bit-identical.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

An independent adversarial review pass (standing in for the quota-exhausted Copilot) probed this fix and produced two accepted findings, now addressed in the second commit:

  1. Latent 3D gap (confirmed by probe): degenerate-input eigh is LAPACK-path dependent — the 2×2 kernel returns quiet NaNs but the general kernel raises LinAlgError from the batched call before any guard runs, and one bad tensor takes down the whole batch. Unreachable via the 2D-only mover today, but the 3D MMPDE capstone lands exactly on this path. Fixed with a per-tensor retry; two new 3×3 tests (all-NaN, and mixed-batch-survives-batched-failure with neighbours preserved bit-identical).
  2. Docstring honesty: the floor has an absolute 1e-8 minimum (pre-existing semantics), so the pass-through guarantee is scoped to the O(1)-normalised metrics the mover uses — now stated.

One PR-body correction from the review: the invariance claim holds per tensor (any tensor with finite eigenvalues is unchanged); a fully-degenerate tensor in a mixed batch previously produced NaN and now gets the finite fallback — a strict improvement, and exactly what the tests assert.

Review verdicts on the remaining attack surfaces: the feared nanmax-inf divergence is unreachable (symmetrisation bounds eigenvalues below inf); finite-eigenvalue/NaN-basis never occurs (10k-tensor hunt); hoist is behaviour-identical elsewhere; both metric-eval paths remain sanitised; no new cross-rank dependence.

Gates after hardening: level_1 and tier_a 382 passed (376 baseline + 6 tests), test_0850 15/15.

Underworld development team with AI support from Claude Code

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