Skip to content

fix(endgames): NaN is a failure, never Converged; NaN-aware security valve - #354

Merged
ofloveandhate merged 1 commit into
bertiniteam:developfrom
ofloveandhate:fix/endgame-nan-guard
Jul 16, 2026
Merged

fix(endgames): NaN is a failure, never Converged; NaN-aware security valve#354
ofloveandhate merged 1 commit into
bertiniteam:developfrom
ofloveandhate:fix/endgame-nan-guard

Conversation

@ofloveandhate

Copy link
Copy Markdown
Contributor

Closes the NaN-blindness hole in both endgames, found during the PSEG audit (follow-up to #353).

The hole

Every IEEE comparison against NaN is false. PSEG's run loop converges on approx_error > FinalTolerance() going false — so a NaN extrapolation exited the loop down the success path, reporting Converged with a poisoned answer. Cauchy's inverse-polarity loop (while(true) + explicit error < tol check) instead slogged pointlessly to MinTrackTime doing NaN arithmetic. The security valve shared the blindness: norm > max_norm is false for a NaN dehomogenized norm, disarming the divergence bailout for exactly the paths most likely at infinity (a NaN dehom norm means the homogenizing coordinate vanished). There was not a single isnan anywhere in endgames/ or trackers/.

The subtlety worth knowing

complex_mp equality does not follow IEEE NaN semantics: a NaN complex_mp compares EQUAL to itself (real_mp behaves correctly). So the standard self-inequality trick (z != z) — and Eigen's own hasNaN() — silently miss NaN at exactly the mp types. Detection must be component-wise isnan on real and imaginary parts.

Changes

  • bertini::ContainsNaN in eigen_extensions.hpp (next to IsEmpty, usable everywhere): component-wise isnan over any complex-valued Eigen object, with the mp caveat documented at the definition.
  • Both extrapolation functions (ComputeApproximationOfXAtT0, ComputeCauchyApproximationOfXAtT0) return FailedToConverge on a NaN result; the run loops already bail on any non-Success extrapolation code, so no loop restructuring.
  • EndgameBase::BeyondSecurityMaxNorm: NaN counts as beyond max_norm; all four valve sites (2 PSEG, 2 Cauchy) route through it.
  • ComputeCycleNumber defaults its selection to 1 before the candidate loop: with poisoned samples no candidate ever wins (NaN comparisons), and a fresh endgame carried cycle number 0 into TransformToSPlane and threw. Behavior-neutral in normal operation (candidate 1 always beats highest()).
  • Named regression tests, one per endgame, injecting a NaN sample and asserting a failure code. The tests were written red-first: the first version used z != z and passed nowhere on mp — which is how the complex_mp equality wrinkle was discovered.

Tests

411 endgame cases green, full ctest battery green, C++ doclint clean.

🤖 Generated with Claude Code

…valve

A NaN approximation used to ride the SUCCESS path: PSEG's run loop converges
on 'approx_error > FinalTolerance()' going false, and every IEEE comparison
against NaN is false -- so a poisoned extrapolation exited the loop as
Converged.  Cauchy's inverse-polarity loop instead slogged to MinTrackTime
doing NaN arithmetic.  The security valve had the same blindness: 'norm >
max_norm' is false for a NaN dehomogenized norm, disarming the divergence
bailout for exactly the paths most likely at infinity.

- bertini::ContainsNaN (eigen_extensions.hpp, next to IsEmpty, usable
  everywhere): component-wise isnan over any complex-valued Eigen object.
  Component-wise is REQUIRED: complex_mp equality does not follow IEEE NaN
  semantics (a NaN complex_mp compares EQUAL to itself), so z != z -- and
  Eigen's own hasNaN() -- silently miss NaN at exactly the mp types.
- Both extrapolation functions return FailedToConverge on a NaN result;
  the run loops already bail on any non-Success extrapolation code.
- EndgameBase::BeyondSecurityMaxNorm: NaN counts as beyond max_norm; all
  four valve sites (2 PSEG, 2 Cauchy) route through it.
- ComputeCycleNumber defaults its selection to 1 before the candidate loop:
  with poisoned samples no candidate is ever assigned (NaN comparisons),
  and a fresh endgame carried cycle number 0 into TransformToSPlane and
  threw.
- Named regression tests, one per endgame: a NaN sample must yield a
  failure code, not Success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ofloveandhate
ofloveandhate merged commit 7fd211f into bertiniteam:develop Jul 16, 2026
18 of 22 checks passed
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