Skip to content

fix(pseg): cycle-number bound is a ceiling; Hermite interpolation evaluates the actual interpolant - #353

Merged
ofloveandhate merged 2 commits into
bertiniteam:developfrom
ofloveandhate:fix/pseg-cycle-bound-cap
Jul 16, 2026
Merged

fix(pseg): cycle-number bound is a ceiling; Hermite interpolation evaluates the actual interpolant#353
ofloveandhate merged 2 commits into
bertiniteam:developfrom
ofloveandhate:fix/pseg-cycle-bound-cap

Conversation

@ofloveandhate

Copy link
Copy Markdown
Contributor

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_number was a floor, not a ceiling

ComputeBoundOnCycleNumber applied the config with max() instead of min(), 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 past UINT_MAX, where the unclamped conversion to unsigned is 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. HermiteInterpolateAndSolve did not evaluate the Hermite 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 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:

  • the interpolant of the x^8+1 test data at 0 is exactly 0.999999998837890625 (pinned at 1e-20);
  • the window-halving errors are 1.162e-9 → 4.539e-12 → 1.773e-14 (~256× per halving — the restored convergence order, visible in-test);
  • new named regression test pins cubic exactness at 1e-20.

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

ofloveandhate and others added 2 commits July 16, 2026 07:32
…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>
@ofloveandhate
ofloveandhate merged commit 03c2c25 into bertiniteam:develop Jul 16, 2026
33 checks passed
@ofloveandhate

Copy link
Copy Markdown
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)
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