Repository navigation
KineticForces - BUGFIX - Decide cubic and quadratic root counts by conditioned tests - #483
Merged
Merged
Conversation
…nditioned tests _real_cubic_roots classified the root count by the sign of the depressed cubic's discriminant (q/2)^2 + (p/3)^3. When two roots nearly coincide the two terms cancel to the rounding floor and the computed sign is noise: for three genuine real roots with two of them 1e-8 apart it came out positive, the one-real-root branch ran, and two real roots were silently dropped. The bounce-point pipeline was safety-netted -- the lost root left the monotone window empty, the NaN emptied the lambda's root set, and the find_zeros fallback ran -- so no physics answer was wrong, but the helper is generic. Decide the count with the conditioned ratio instead: for p < 0 there are three real roots iff |q/2| <= r^3 with r = sqrt(-p/3), tested as the ratio against 1, where the cancellation never happens. A ratio a rounding step past 1 is a repeated root, which the clamped trigonometric form returns directly, so the explicit repeated-root branch and its ad-hoc discriminant tolerance go away. The same cancellation sat at two more sites -- the quadratic branch for a degenerate leading coefficient, and the stationary-point search in _surface_b_field, where a dropped near-double stationary point would have lost a root pair with no fallback at all -- so both now share one _quadratic_real_roots that treats a discriminant within rounding of zero as a double root rather than none. The tolerance is deliberately generous. A wrongly admitted repeated root is a near-touch at a stationary point, which cannot sit strictly inside the monotone window the cell solve filters on, so it can only lose the residual ranking there, never win it. Tests cover the near-double cubic and quadratic cases, built from their roots so the polynomials have them exactly, and the previously unexercised path where both stationary points bounding an interval fall inside a single spline cell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186CLfjeJeSusNTva5RMfGU
…o-line standard Comment-only. Six annotations ran three to five lines; each now states what the code does in at most two, with the one load-bearing justification -- why the root-count tolerance can be generous -- kept beside the constant it governs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186CLfjeJeSusNTva5RMfGU
…ot-classification
adrianaghiozzi
approved these changes
Oct 6, 2026
Collaborator
There was a problem hiding this comment.
Reviewed with Claude Code. Verified independently in an isolated worktree: the exact #456 misclassification case, and a full sweep of the old failure band (1e-7 to 1e-17, both signs), now resolve correctly. test/runtests_kinetic.jl passes 5073/5073. Triple-root and monotone-cubic edge cases the old explicit branches handled are still correct.
logan-nc
enabled auto-merge
October 7, 2026 12:39
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.
Release note
The closed-form root solvers behind the NTV bounce-point search now decide how many real roots a cubic or quadratic has by a conditioned test rather than by the sign of a discriminant that cancels to rounding noise when two roots nearly coincide. Previously such a case could silently drop real roots; the bounce-point pipeline fell back to its adaptive scan and gave the right answer, but the helper is generic and would not have in another caller.
What this fixes
Review of #456 found, and reproduced with an out-of-tree probe, that
_real_cubic_rootsmisclassifies a cubic with three genuine real roots when two are about 1e-8 apart:disc = (q/2)^2 + (p/3)^3computes to+8.8e-20for a true negative, lands just outside the repeated-root tolerance, and the one-real-root branch returns only the far root. Independent review of the merged code found the same cancellation at two more sites._real_cubic_roots, depressed-cubic discriminantfind_zerosfallback — correct answer, pre-#456 cost_real_cubic_roots, quadratic branch for a degenerate leading coefficient_surface_b_field, stationary-point quadraticThe third is physically a near-flat inflection, so the lost pair is nearly coincident and the impact small; but the discipline has to be consistent across all three sites.
The fix
_real_cubic_rootsdecides the count with the conditioned ratio: forp < 0, three real roots iff|q/2| <= r^3withr = sqrt(-p/3), tested as the ratio against 1, where nothing cancels. A ratio a rounding step past 1 is a repeated root, which the clamped trigonometric form returns directly — so the explicit repeated-root branch and its ad-hoc tolerance are deleted. One real root otherwise (Cardano, radicals do not cancel there)._quadratic_real_rootstreats a discriminant within rounding of zero as a double root rather than none, and serves both the degenerate-cubic branch and the stationary-point search.ROOT_COUNT_TOL = 64 eps. Deliberately generous: a wrongly admitted repeated root is a near-touch at a stationary point, which cannot sit strictly inside the monotone window_cell_level_rootfilters on, so it can only lose the residual ranking there, never win it. The measured margin for the reported case is 6 eps, so the earlier 8 eps would have been one FMA reordering away from failing on Julia 1.12.Tests
5073 assertions in
runtests_kinetic.jl(from 1844). New coverage:(0.37, 0.37000001, 0.91)and two more triples so the polynomial has them exactly — all three return, residual < 1e-12;2 + 0.45 cos 2πθ + 0.20 cos 6πθ: six stationary points, a pair 0.077 apart sharing a cell), swept across the full B-range between them againstRoots.find_zeros: counts agree, the crossing lies strictly between the two stationary points, and|v_par| < 1e-14at every root.A fixture that drives the stationary-point discriminant itself to the rounding boundary through a spline is not attempted — that is fiddly and non-deterministic across Julia versions; the helper test is the unit check and the existing stationary-point-count test (against a dense
deriv1sign scan) is the integration check.Verification
Real DIII-D geometry, 40 surfaces, every trapped lambda on the production grid plus lambdas injected to within 1e-13 of both trapped/passing boundaries, against
Roots.find_zeroson the identical spline:find_zerosfind_zerosThe two previous fallbacks were exactly the misclassification firing on injected lambda; they now solve directly. The one figure that moved is the residual at those same two lambdas: they are genuinely near-double roots, where the single Newton polish in
_cell_level_rootconverges linearly rather than quadratically, so the direct path leaves 5.5e-14 wherefind_zerosleft 4.4e-16. That is the conditioning limit of a near-double root, at a point where the root location itself is only determined to about 1e-7; the production grid never gets within 1e-4 of a boundary and stays at machine precision. A multiplicity-aware iteration would fix it and is not worth the machinery for a regime the lambda grid cannot reach.Regression report
diiid_n1solovev_kinetic_ntvsolovev_kinetic_multiionsolovev_kinetic_calculatedsolovev_kinetic_nuzeroBaseline
develop@56eafc2a5, branch@be686c388(develop merged in; comment-only commit since the last run); no stale-baseline warning. Every tracked quantity unchanged, as predicted below — includingsolovev_kinetic_calculatedandsolovev_kinetic_nuzero, which moved at the 1e-8 level under #456.Notes for reviewers
Prediction, stated before the harness ran: every row unchanged, including the two Mode A cases that moved under #456. The real-geometry sweep for #456 showed zero fallbacks on the production lambda grid, so the NaN path never fired there; where three real roots genuinely exist the trigonometric branch is the identical formula; and the stationary-point change only acts when a discriminant is within rounding of zero, which clean geometry never hits. Any movement would have been a red flag to investigate, not a reason for a
!.No review package for this one — a small bug fix, below the threshold for it.
Credit. The cubic misclassification and the untested same-cell path were both found by @adrianaghiozzi's review of #456, probe and all; the two further sites with the same cancellation came from re-reviewing the merged code with her example in hand.
🤖 Generated with Claude Code
https://claude.ai/code/session_0186CLfjeJeSusNTva5RMfGU