Skip to content

Equilibrium - BUGFIX - Make the magnetic-axis search robust to an unreliable psi-spline Hessian - #444

Draft
d-burg wants to merge 1 commit into
developfrom
bugfix/equilibrium-axis-search-robustness
Draft

Equilibrium - BUGFIX - Make the magnetic-axis search robust to an unreliable psi-spline Hessian#444
d-burg wants to merge 1 commit into
developfrom
bugfix/equilibrium-axis-search-robustness

Conversation

@d-burg

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: none (harness @ 7513e20)
  • Migration: none

Reading some perfectly good geqdsks (seen on 257×257 TJ circular files, two of an 81-point β scan) used to abort with Jacobian matrix is singular near (…) or Failed to find magnetic axis after 200 iterations. The cause is the 2-D ψ spline's second derivatives near the axis, which on these files are unreliable (∂B_z/∂R wanders between ~5 and ~0 over a few cells, so the Newton Hessian goes near-singular or indefinite). The axis search now caps each Newton step at one midplane-march step, and if Newton still fails, falls back to first-derivative bisection (B_z along the midplane, B_r along a column) before retrying Newton from there. Equilibria the old code handled never enter the fallback and are bit-identical.

Regression report

Regression Report: diiid_n1
=================================================================================================
Ref 1: 349a0c262 @ 349a0c262 (2026-09-04)
env: julia 1.11.6, arm64-apple-darwin24.0.0, manifest 4f2359f8 (pinned), 8 threads/8 BLAS
Ref 2: local (7513e2040)
env: julia 1.11.6, arm64-apple-darwin24.0.0, manifest 4f2359f8 (pinned), 8 threads/8 BLAS
-------------------------------------------------------------------------------------------------
Quantity 349a0c262 local Diff Status
-------------------------------------------------------------------------------------------------
total energy Re(et[1]) 8.013525e-01 8.013525e-01 0.0e+00 OK total energy Im(et[1]) 1.234104e-04 1.234104e-04 0.0e+00 OK plasma energy Re(ep[1]) -1.348325e+00 -1.348325e+00 0.0e+00 OK vacuum energy Re(ev[1]) 2.149678e+00 2.149678e+00 0.0e+00 OK vacuum matrix min eigenvalue 1.873975e-01 1.873975e-01 0.0e+00 OK plasma energy (all) [35 elem] [35 elem] 0.0e+00 OK vacuum energy (all) [35 elem] [35 elem] 0.0e+00 OK total energy (all) [35 elem] [35 elem] 0.0e+00 OK ODE steps (saved) 2655 2655 0.0e+00 OK ODE steps (total) 4692 4692 0.0e+00 OK q0 1.204212e+00 1.204212e+00 0.0e+00 OK q95 4.781723e+00 4.781723e+00 0.0e+00 OK beta_t 1.327024e-02 1.327024e-02 0.0e+00 OK beta_n 1.372511e+00 1.372511e+00 0.0e+00 OK internal inductance li1 8.842261e-01 8.842261e-01 0.0e+00 OK internal inductance li2 7.080749e-01 7.080749e-01 0.0e+00 OK internal inductance li3 7.304332e-01 7.304332e-01 0.0e+00 OK poloidal beta betap1 6.680738e-01 6.680738e-01 0.0e+00 OK poloidal beta betap2 5.349834e-01 5.349834e-01 0.0e+00 OK poloidal beta betap3 5.518762e-01 5.518762e-01 0.0e+00 OK # singular surfaces 5 5 0.0e+00 OK singular psi locations [5 elem] [5 elem] 0.0e+00 OK singular q values [5 elem] [5 elem] 0.0e+00 OK current beta betaj 4.236478e-01 4.236478e-01 0.0e+00 OK plasma volume 1.829472e+01 1.829472e+01 0.0e+00 OK plasma current 1.152130e+00 1.152130e+00 0.0e+00 OK mpert 35 35 0.0e+00 OK npert 1 1 0.0e+00 OK toroidal field bt0 2.006573e+00 2.006573e+00 0.0e+00 OK wall field bwall 3.880145e-01 3.880145e-01 0.0e+00 OK aspect ratio 2.845746e+00 2.845746e+00 0.0e+00 OK elongation kappa 1.708322e+00 1.708322e+00 0.0e+00 OK q profile (checksum) 1acf82ba075d... 1acf82ba075d... identical OK pressure profile (checksum) 9a852570d709... 9a852570d709... identical OK Mercier D_I profile (checksum) 7843a082c01b... 7843a082c01b... identical OK resistive interchange D_R profile (checksum) cc65e9fe2fac... cc65e9fe2fac... identical OK ballooning Delta' profile (checksum) 763e34d8fe3d... 763e34d8fe3d... identical OK island half-widths [5 elem] [5 elem] 0.0e+00 OK Chirikov parameter [5 elem] [5 elem] 0.0e+00 OK ||resonant area-weighted field|| 5.208127e-04 5.208127e-04 0.0e+00 OK PE plasma energy 3.422688e+00 3.422688e+00 0.0e+00 OK PE vacuum energy 3.174510e+00 3.174510e+00 0.0e+00 OK PE surface energy 5.841902e+00 5.841902e+00 0.0e+00 OK PE toroidal torque -5.063420e-02 -5.063420e-02 0.0e+00 OK NTV torque FGAR [N·m] 5.335149e-01 5.335149e-01 0.0e+00 OK NTV kinetic energy dW FGAR [J] 7.994507e-02 7.994507e-02 0.0e+00 OK Runtime (s) 156.9s 156.2s -- resonant area-weighted field b^r [5 elem] [5 elem] 0.0e+00 OK =================================================================================================
Summary: 47 unchanged
Regression Report: diiid_slayer_n1
========================================================================
Ref 1: 349a0c262 @ 349a0c262 (2026-09-04)
env: julia 1.11.6, arm64-apple-darwin24.0.0, manifest 4f2359f8 (pinned), 8 threads/8 BLAS
Ref 2: local (7513e2040)
env: julia 1.11.6, arm64-apple-darwin24.0.0, manifest 4f2359f8 (pinned), 8 threads/8 BLAS
------------------------------------------------------------------------
Quantity 349a0c262 local Diff Status
------------------------------------------------------------------------
SLAYER surface indices [6 elem] [6 elem] 0.0e+00 OK SLAYER poloidal m [6 elem] [6 elem] 0.0e+00 OK SLAYER toroidal n [6 elem] [6 elem] 0.0e+00 OK SLAYER minor radius rs [6 elem] [6 elem] 0.0e+00 OK SLAYER r-based shear [6 elem] [6 elem] 0.0e+00 OK SLAYER Lundquist S [6 elem] [6 elem] 0.0e+00 OK SLAYER D_norm [6 elem] [6 elem] 0.0e+00 OK SLAYER P_perp [6 elem] [6 elem] 0.0e+00 OK SLAYER tauk [6 elem] [6 elem] 0.0e+00 OK SLAYER iota_e [6 elem] [6 elem] 0.0e+00 OK SLAYER Q_root [2/1,3/1,4/1] [3 elem] [3 elem] 3.0e-07 OK SLAYER ω_Hz [2/1,3/1,4/1] [3 elem] [3 elem] 4.5e-03 OK SLAYER γ_Hz [2/1,3/1,4/1] [3 elem] [3 elem] 1.1e-03 OK SLAYER no_root flags [2/1,3/1,4/1] [3 elem] [3 elem] 0.0e+00 OK SLAYER enabled flag 1 1 0.0e+00 OK Runtime (s) 131.2s 135.7s -- ========================================================================
Summary: 15 unchanged
Regression Report: efit_fixedbdy_separatrix
=======================================================================
Ref 1: 349a0c262 @ 349a0c262 (2026-09-04)
env: julia 1.11.6, arm64-apple-darwin24.0.0, manifest 4f2359f8 (pinned), 8 threads/8 BLAS
Ref 2: local (7513e2040)
env: julia 1.11.6, arm64-apple-darwin24.0.0, manifest 4f2359f8 (pinned), 8 threads/8 BLAS
-----------------------------------------------------------------------
Quantity 349a0c262 local Diff Status
-----------------------------------------------------------------------
psio (total poloidal flux) 3.344390e-01 3.344390e-01 0.0e+00 OK q0 1.155202e+00 1.155202e+00 0.0e+00 OK q95 5.741022e+00 5.741022e+00 0.0e+00 OK beta_t 3.945205e-05 3.945205e-05 0.0e+00 OK beta_n 1.009512e-01 1.009512e-01 0.0e+00 OK Runtime (s) 23.8s 23.7s -- =======================================================================
Summary: 5 unchanged

regress --cases diiid_n1,diiid_slayer_n1,efit_fixedbdy_separatrix --refs 349a0c262,local (349a0c2 = the develop tip this branch is based on). 67/67 quantities unchanged.

Notes for reviewers

  • Two failure modes, two stages. The TJ_circular_axis_newton_regression.geqdsk fixture (singular Jacobian) is fixed by the step cap alone — capped Newton converges on its own. The TJ_circular_axis_newton_cycling.geqdsk fixture (indefinite Hessian, Newton cycles) needs the bisection stage. The test asserts which path each file takes via the @info message, and that a healthy geqdsk (CHEASE_test_data/EQDSK_COCOS_02) takes the plain Newton path.
  • Why not bisection first? An earlier draft ran bisection first with Newton as a polish. On a healthy DIII-D geqdsk that lands on a different point within Newton's 1e-12 tolerance ball (axis moved ~1e-9), which reshuffled the adaptive ODE step count and moved diiid_n1 energies by 0.02% and SLAYER γ by 0.2%. Newton-first with bisection as the fallback leaves the converging path untouched, hence the clean harness.
  • Fixtures are 1.1 MB each (two 257×257 geqdsks). They are the smallest files I have that reproduce each mode; a coarser grid does not trigger either. Happy to drop them to a lower resolution if someone finds a smaller reproducer.
  • A singular Hessian inside Newton now returns "not converged" (→ fallback) instead of error; the only remaining hard error in direct_position! is the midplane march failing to find a B_z sign change, which is a genuinely broken file.
  • Validation beyond the harness: the full 81-point TJ β scan (pc 0.001–0.1811) reads every equilibrium with this branch; the two files that previously failed land on the axis at (R, Z) = (2.000, 0.000) as the midplane crossing predicts.

🤖 Generated with Claude Code

…eliable psi-spline Hessian
Two failure modes of `direct_position!` on otherwise healthy geqdsks, both caused by
the 2-D psi spline's second derivatives near the axis (dB_z/dR wanders between ~5
and ~0 within a few cells, so the Hessian goes near-singular or indefinite):
1. "Jacobian matrix is singular" -- one bad iterate sent the undamped Newton step
across the whole box, into a region where det J underflowed.
2. "Failed to find magnetic axis after 200 iterations" -- Newton cycled between
two points either side of the axis on an indefinite Hessian.
Fix, in two stages that leave the converging path untouched:
* Cap each Newton step at one midplane-march step. Inactive for well-behaved
iterations (their steps are < dr/2); it only keeps a bad iterate inside the
axis neighbourhood until the Hessian recovers. A singular Hessian now returns
"not converged" instead of erroring.
* If Newton does not converge, locate the axis with first derivatives only:
B_z changes sign across the axis along the midplane and B_r along a column,
so four alternating 1-D bisections (80 iterations each, widening bracket in Z)
converge to grad(psi) = 0 without touching the spline's second derivatives.
Newton is retried from that point; if it still fails, the bisection point is
used as the axis (it is a converged zero of the first derivatives) with an
@info message instead of an error.
Healthy equilibria never enter the fallback, so their results are bit-identical
(regression harness: diiid_n1 47/47, diiid_slayer_n1 15/15,
efit_fixedbdy_separatrix 5/5 unchanged vs 349a0c2).
Two 257x257 TJ circular geqdsks reproducing each failure mode are added as
fixtures with a test that both now locate the axis where the midplane B_z
crossing says it is, and that a healthy geqdsk takes the plain Newton path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added the bugfix Something was wrong and now is not label Sep 5, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request is missing an assignee and a reviewer.

If you are not ready to name them, mark this pull request as a draft.
docs/development/contributors.md suggests lead developers to ask.
Merging is not blocked here, but no pull request may be merged without human review.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfixSomething was wrong and now is not

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@d-burg