fix(endgames): NaN is a failure, never Converged; NaN-aware security valve - #354
Merged
ofloveandhate merged 1 commit intoJul 16, 2026
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, reportingConvergedwith a poisoned answer. Cauchy's inverse-polarity loop (while(true)+ expliciterror < tolcheck) instead slogged pointlessly toMinTrackTimedoing NaN arithmetic. The security valve shared the blindness:norm > max_normis 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 singleisnananywhere inendgames/ortrackers/.The subtlety worth knowing
complex_mpequality does not follow IEEE NaN semantics: a NaNcomplex_mpcompares EQUAL to itself (real_mpbehaves correctly). So the standard self-inequality trick (z != z) — and Eigen's ownhasNaN()— silently miss NaN at exactly the mp types. Detection must be component-wiseisnanon real and imaginary parts.Changes
bertini::ContainsNaNineigen_extensions.hpp(next toIsEmpty, usable everywhere): component-wiseisnanover any complex-valued Eigen object, with the mp caveat documented at the definition.ComputeApproximationOfXAtT0,ComputeCauchyApproximationOfXAtT0) returnFailedToConvergeon a NaN result; the run loops already bail on any non-Successextrapolation code, so no loop restructuring.EndgameBase::BeyondSecurityMaxNorm: NaN counts as beyondmax_norm; all four valve sites (2 PSEG, 2 Cauchy) route through it.ComputeCycleNumberdefaults 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 intoTransformToSPlaneand threw. Behavior-neutral in normal operation (candidate 1 always beatshighest()).z != zand passed nowhere on mp — which is how thecomplex_mpequality wrinkle was discovered.Tests
411 endgame cases green, full ctest battery green, C++ doclint clean.
🤖 Generated with Claude Code