fix(pseg): cycle-number bound is a ceiling; Hermite interpolation evaluates the actual interpolant - #353
Merged
ofloveandhate merged 2 commits intoJul 16, 2026
Conversation
…nsigned conversion ComputeBoundOnCycleNumber applied the max_cycle_number config with max() instead of min(), so the 'largest cycle number to consider' never bounded anything: a near-unity sample ratio (slow convergence -- high multiplicity, or a slow diverger) made the amplified estimate itself the bound, costing hundreds of Hermite solves per approximation -- and a ratio within ~1e-14 of 1 amplifies past UINT_MAX, where the unclamped conversion to unsigned is undefined behavior. Clamp in the real type before converting: anything not provably below the ceiling (including inf/NaN) becomes the ceiling. The old expectation 'max(5,6) = 6' in compute_bound_on_cycle_num pinned the inverted behavior; it is now 5 (the amplified estimate, under the ceiling). Named regression test covers both the slow-ratio cap and the UB-magnitude clamp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e interpolant
The Horner reconstruction walked the doubled node list at half speed
(node z_ii paired with coefficient a_{2*ii}), evaluating a polynomial that
was NOT the interpolant: a cubic -- which 3-node Hermite must reproduce
exactly -- came back 1.6e-5 off. The limit as the sample window slides to
the target was unaffected, so PSEG still converged, but at degraded order:
extra iterations per path, poorer approximations than the samples support.
The pinned test expectation ('found using matlab') was itself not the
Hermite value, and its loose tolerance let the wrong code pass. All
expectations re-derived in exact rational arithmetic: the interpolant of
the x^8+1 test data at 0 is exactly 0.999999998837890625, and the
window-halving errors are 1.162e-9 -> 4.539e-12 -> 1.773e-14 (~256x per
halving -- the restored order, visible in-test). New named regression
test pins cubic exactness at 1e-20.
Computational-semantics note (records doctrine): PSEG approximations
change value with this fix -- this is the behavior-epoch case; the epochs
mechanism is not yet built, so this note is the marker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
I rest easier, with this Hermite interpolant bug solved. |
ofloveandhate
added a commit
that referenced
this pull request
Jul 16, 2026
…valve (#354) 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](https://claude.com/claude-code)
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.
Two power-series endgame defects found by auditing PSEG against the Cauchy fix history ("many of the bugs fixed in cauchy probably have analogues in PSEG"). Both are old; both were masked by tests whose expectations were calibrated to the buggy implementations.
1.
max_cycle_numberwas a floor, not a ceilingComputeBoundOnCycleNumberapplied the config withmax()instead ofmin(), so "largest cycle number to consider" never bounded the candidate search. A near-unity sample ratio — slow convergence: high multiplicity, or a slow diverger — made the amplified estimate itself the bound (hundreds of Hermite solves per approximation), and a ratio within ~1e-14 of 1 amplifies pastUINT_MAX, where the unclamped conversion tounsignedis undefined behavior.Now clamped in the real type before any conversion;
!(amplified < ceiling)also routes inf/NaN to the ceiling. The old test literally pinned the inverted behavior ("max_cycle_num implemented max(5,6) = 6"); a named regression test covers both the slow-ratio cap and the UB-magnitude clamp.2.
HermiteInterpolateAndSolvedid not evaluate the Hermite interpolantThe Horner reconstruction walked the doubled node list at half speed (node
z_iipaired with coefficienta_{2·ii}), evaluating a polynomial that is not the interpolant: a cubic — which 3-node Hermite must reproduce exactly — came back 1.6e-5 off. The limit as the sample window slides to the target was unaffected, so PSEG still converged; but at degraded order, costing extra iterations per path and delivering poorer approximations than the samples support.The pinned expectation ("found using matlab") was itself not the Hermite value, and its loose tolerance let the wrong code pass. All expectations re-derived in exact rational arithmetic:
x^8+1test data at 0 is exactly0.999999998837890625(pinned at 1e-20);1.162e-9 → 4.539e-12 → 1.773e-14(~256× per halving — the restored convergence order, visible in-test);Records note
Fix 2 changes the values PSEG produces — this is a computational semantics change in the sense of the records-identity doctrine (the behavior-epoch case). The epochs mechanism doesn't exist yet; this note is the marker.
Tests
387 endgame cases green (all three PSEG flavors × precisions), full ctest battery green, Python suite 926 passed / 3 skipped, both doclints clean.
🤖 Generated with Claude Code