Skip to content

ForceFreeStates.Galerkin - BUGFIX! - Jacobi-scale the banded Galerkin LU solve - #512

Merged
matt-pharr merged 2 commits into
developfrom
bugfix/galerkin-scaled-lu-pr
Oct 8, 2026
Merged

matt-pharr merged 2 commits into
developfrom
bugfix/galerkin-scaled-lu-pr

Conversation

@StuartBenjamin

@StuartBenjamin StuartBenjamin commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: Galerkin (integrator = "galerkin") results move. In gal_resistive_diiid: Δ′ diagonal 0.05%, ‖Δ′‖ 0.01%, ‖Δ_coil‖ 1.85%, ‖match cout‖ 0.40%. Forward, Riccati and ideal results are bit-identical (diiid_n1, diiid_n1_riccati). (harness @ 8a8a3cf)
  • Migration: no input changes. Re-run integrator = "galerkin" cases and discard previously computed Galerkin Δ′, which could be wrong by O(1), including sign, on outer surfaces.

The Galerkin outer-region Δ′ solve lost accuracy in the small resonant coefficients that carry Δ′. As a result, some Δ′ entries changed erratically with gal_nx and could even flip sign, for example the 4/1 row of DIII-D 147131 n=1. The banded LU system is now Jacobi-scaled before factorization. Δ′ now converges in gal_nx, and the 2/1–4/1 diagonal agrees with Riccati to within 0.4%.

Regression report

================================================================
Case: diiid_n1 — DIII-D-like equilibrium, n=1, ideal + perturbed equilibrium
================================================================

Regression Report: diiid_n1
==============================================================================================================
Ref 1: origin/develop  @ 6553fe205 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
Ref 2: bugfix/galerkin-scaled-lu-pr  @ 8a8a3cf17 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
--------------------------------------------------------------------------------------------------------------
Quantity                                      origin/develop   bugfix/galerkin-scaled-lu-pr  Diff       Status
--------------------------------------------------------------------------------------------------------------
total energy Re(et[1])                        8.013468e-01     8.013468e-01                  0.0e+00    OK    
total energy Im(et[1])                        4.143487e-05     4.143487e-05                  0.0e+00    OK    
plasma energy Re(ep[1])                       -1.348157e+00    -1.348157e+00                 0.0e+00    OK    
vacuum energy Re(ev[1])                       2.149503e+00     2.149503e+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)                             1431             1431                          0.0e+00    OK    
ODE steps (total)                             1426             1426                          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.842212e-01     8.842212e-01                  0.0e+00    OK    
internal inductance li2                       7.080713e-01     7.080713e-01                  0.0e+00    OK    
internal inductance li3                       7.304295e-01     7.304295e-01                  0.0e+00    OK    
poloidal beta betap1                          6.680737e-01     6.680737e-01                  0.0e+00    OK    
poloidal beta betap2                          5.349836e-01     5.349836e-01                  0.0e+00    OK    
poloidal beta betap3                          5.518763e-01     5.518763e-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.236480e-01     4.236480e-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)                          2cdacd6807fb...  2cdacd6807fb...               identical  OK    
pressure profile (checksum)                   3d101fa873b1...  3d101fa873b1...               identical  OK    
Mercier D_I profile (checksum)                25ada2b687ca...  25ada2b687ca...               identical  OK    
resistive interchange D_R profile (checksum)  7dc243ce58a9...  7dc243ce58a9...               identical  OK    
ballooning Delta' profile (checksum)          9140d3dfe986...  9140d3dfe986...               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.187848e-04     5.187848e-04                  0.0e+00    OK    
dominant-coupling singular values             [3 elem]         [3 elem]                      0.0e+00    OK    
|forcing overlap with dominant mode|          1.415721e-04     1.415721e-04                  0.0e+00    OK    
|delta_nominal| of coil set 1                 7.055416e-05     7.055416e-05                  0.0e+00    OK    
||ddelta/d(shift)|| over coil sets            1.246245e-04     1.246245e-04                  0.0e+00    OK    
||ddelta/d(tilt)|| over coil sets             4.180789e-07     4.180789e-07                  0.0e+00    OK    
PE plasma energy                              3.422634e+00     3.422634e+00                  0.0e+00    OK    
PE vacuum energy                              3.174510e+00     3.174510e+00                  0.0e+00    OK    
PE surface energy                             5.825956e+00     5.825956e+00                  0.0e+00    OK    
PE toroidal torque                            5.085625e-02     5.085625e-02                  0.0e+00    OK    
NTV torque FGAR [N·m]                         5.294859e-01     5.294859e-01                  0.0e+00    OK    
NTV kinetic energy dW FGAR [J]                7.920700e-02     7.920700e-02                  0.0e+00    OK    
Runtime (s)                                   361.4s           351.4s                                   --    
||forcing b~|| (root-area-weighted)           4.683329e-04     4.683329e-04                  0.0e+00    OK    
resonant area-weighted field b^r              [5 elem]         [5 elem]                      0.0e+00    OK    
==============================================================================================================
Summary: 53 unchanged


================================================================
Case: gal_resistive_diiid — DIII-D-like, n=1, RDCON outer-region Galerkin Δ′ with rpec coil columns (delta_coil)
================================================================

Regression Report: gal_resistive_diiid
=======================================================================================================
Ref 1: origin/develop  @ 6553fe205 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
Ref 2: bugfix/galerkin-scaled-lu-pr  @ 8a8a3cf17 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
-------------------------------------------------------------------------------------------------------
Quantity                 origin/develop  bugfix/galerkin-scaled-lu-pr  Diff               Status       
-------------------------------------------------------------------------------------------------------
gal # singular surfaces  4               4                             0.0e+00            OK           
gal singular q values    [4 elem]        [4 elem]                      0.0e+00            OK           
gal PEST3 Δ diagonal     [4 elem]        [4 elem]                      7.560e+00 (0.05%)  ** CHANGED **
||gal Δ′ matrix||        1.243991e+04    1.243825e+04                  1.663e+00 (0.01%)  ** CHANGED **
||gal Δ_coil block||     2.452669e+02    2.498141e+02                  4.547e+00 (1.85%)  ** CHANGED **
gal D_I per surface      [4 elem]        [4 elem]                      0.0e+00            OK           
gal α per surface        [4 elem]        [4 elem]                      0.0e+00            OK           
||gal inner-layer Δ||    1.474630e+06    1.474630e+06                  0.0e+00            OK           
||gal match cout||       3.074025e-02    3.061595e-02                  1.243e-04 (0.40%)  ** CHANGED **
gal match residual       2.637801e-16    1.877542e-16                  7.6e-17            OK           
Runtime (s)              208.6s          195.9s                                           --           
=======================================================================================================
Summary: 4 changed, 6 unchanged


================================================================
Case: gal_resistive_pe — DIII-D-like, n=1, DRIVEN/RPEC: gal-matched resistive ξ → PerturbedEquilibrium (coil-driven singular coupling)
================================================================

Regression Report: gal_resistive_pe
==================================================================================
Ref 1: origin/develop  @ 6553fe205 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
Ref 2: bugfix/galerkin-scaled-lu-pr  @ 8a8a3cf17 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
----------------------------------------------------------------------------------
Quantity                origin/develop  bugfix/galerkin-scaled-lu-pr  Diff  Status
----------------------------------------------------------------------------------
rational q values       N/A             N/A                           N/A   N/A   
island half-widths      N/A             N/A                           N/A   N/A   
Chirikov parameter      N/A             N/A                           N/A   N/A   
||resonant b field||    N/A             N/A                           N/A   N/A   
resonant b field (all)  N/A             N/A                           N/A   N/A   
penetrated b field      N/A             N/A                           N/A   N/A   
PE Δ' per surface       N/A             N/A                           N/A   N/A   
||C resonant b field||  N/A             N/A                           N/A   N/A   
Runtime (s)             214.4s          216.8s                              --    
==================================================================================
Summary: 8 missing/N/A


================================================================
Case: diiid_n1_riccati — DIII-D-like equilibrium, n=1, Riccati integrator Δ' matrix
================================================================

Regression Report: diiid_n1_riccati
============================================================================================
Ref 1: origin/develop  @ 6553fe205 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
Ref 2: bugfix/galerkin-scaled-lu-pr  @ 8a8a3cf17 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
--------------------------------------------------------------------------------------------
Quantity                       origin/develop  bugfix/galerkin-scaled-lu-pr  Diff     Status
--------------------------------------------------------------------------------------------
delta prime (BVP diagonal)     [5 elem]        [5 elem]                      0.0e+00  OK    
delta prime (raw side-major)   [10 elem]       [10 elem]                     0.0e+00  OK    
edge coil response delta_coil  [10 elem]       [10 elem]                     0.0e+00  OK    
total energy Re(et[1])         8.038337e-01    8.038337e-01                  0.0e+00  OK    
plasma energy Re(ep[1])        -1.344423e+00   -1.344423e+00                 0.0e+00  OK    
vacuum energy Re(ev[1])        2.148256e+00    2.148256e+00                  0.0e+00  OK    
total energy (all)             [35 elem]       [35 elem]                     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    
ODE steps (saved)              104             104                           0.0e+00  OK    
ODE steps (total)              4040            4040                          0.0e+00  OK    
mpert                          35              35                            0.0e+00  OK    
npert                          1               1                             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_n                         1.372511e+00    1.372511e+00                  0.0e+00  OK    
Runtime (s)                    233.7s          243.3s                                 --    
============================================================================================
Summary: 17 unchanged

Notes for reviewers

Harness: gal_resistive_pe is N/A on both refs. This is pre-existing on develop and not caused by this PR. With the Galerkin integrator, PerturbedEquilibrium skips the plasma-response and singular-coupling stages ("free_boundary was not produced by the galerkin integrator"). None of that case's quantities are written on either commit. The Galerkin Δ′ it shares with gal_resistive_diiid is covered there.

Cause. The assembled Galerkin matrix diagonal spans about 25 decades: resonant-cell entries are around 1e-14, sitting next to extension-cell entries around 1e-3. Unscaled partial-pivoting banded LU (gbtrf!/gbtrs!) loses the small resonant coefficients, and Δ′ is extracted from exactly those.

Fix (src/ForceFreeStates/Galerkin/GalerkinSolve.jl, LU branch only):

  • The new gal_scaled_lu_solve! applies symmetric Jacobi scaling, A ← D·A·D with d_j = |A_jj|^(-1/2) (1 for a zero diagonal), in gbtrf! band storage.
  • It scales the right-hand sides, factors and solves, then un-scales every solution column, including the rpec coil columns.
  • The gal_solver = "cholesky" branch is untouched. It still fails on develop with UndefVarError: pbtrf!. A separate feature PR (feature/galerkin-cholesky) fixes that. The two PRs edit disjoint lines and merge cleanly in either order.

Evidence: DIII-D g147131 n=1, no wall, Re Δ′ diagonal, gal_solver = "LU"

galerkin_pr_package.zip

All runs on develop @ 6553fe205 ("before") and this PR @ 8a8a3cf17 ("after"), same decks. Riccati is identical on both commits, which serves as a control.

2/1 3/1 4/1 5/1 6/1
Riccati (reference) 7.378 -6.626 -60.48 -53.44 -114.4
before, gal_nx = 128 7.337 -6.292 -72.58 -54.35 -107.5
before, gal_nx = 256 7.313 -6.360 -135.2 -34.12 -149.5
before, gal_nx = 384 7.256 -5.737 +84.11 -46.30 -86.17
before, gal_nx = 512 7.310 -8.579 +125.8 +35.84 -227.6
after, gal_nx = 128 7.359 -6.652 -60.51 -57.40 -126.4
after, gal_nx = 256 7.359 -6.651 -60.50 -57.38 -126.1
after, gal_nx = 384 7.359 -6.651 -60.50 -57.38 -126.1
after, gal_nx = 512 7.359 -6.651 -60.50 -57.38 -126.1
  • Off-diagonals. The 4/1 row at gal_nx = 512 was [-29.2, -184.7, 125.8, 2482, 5474] before. After, it is [3.192, 5.214, -60.50, 57.84, 68.17], against Riccati's [3.184, 5.196, -60.48, 57.90, 77.06].
  • 5/1 and 6/1. These now converge in gal_nx but stay 7–10% from Riccati. That is a separate edge/vacuum-dominated discrepancy and is not addressed here.
  • Fixed boundary (vac_flag = false, gal_nx = 256). The diagonal moves slightly (2.033 / -14.48 / -76.32 / -207.0 / -1968 → 2.041 / -14.33 / -78.38 / -207.5 / -1933). The 4/1 off-diagonals change substantially ([-6.86, -110.1] → [18.4, 0.06] in the 5/1 and 6/1 columns).

In-repo example decks. DIIID-like_gal_resistive{,_pe}_example back harness cases gal_resistive_diiid and gal_resistive_pe. They are well enough conditioned that they move only slightly:

  • Δ′ diagonal: 3.767 / -10.08 / -23.44 / -2725 → 3.776 / -10.08 / -23.44 / -2726.
  • ‖Δ_coil‖_F: 245.27 → 249.81.

Tests. The new test/runtests_galerkin.jl builds a banded system with an alternating 4e-8 / 1e5 diagonal scaling. Unscaled LU solves it to about 1e-4 relative error; the scaled solve reaches about 1e-15. The test also covers multiple-RHS un-scaling and a zero diagonal entry. At 8a8a3cf17 (1 CPU job):

Test set Passed
Galerkin scaled banded LU 3/3
solve API (includes the Galerkin solve testset) 70/70
ForceFreeStatesResult 133/133
Runner: Control + run_slayer + HDF5 output 79/79
gpec.h5 schema naming 6/6 and 16/16

Reproducing. All commands run from a GPEC checkout.

  • Unit test (on this branch):

    julia --project=. test/runtests.jl test/runtests_galerkin.jl
  • Regression report:

    julia --project=regression-harness regression-harness/regress.jl \
        --cases diiid_n1,gal_resistive_diiid,gal_resistive_pe,diiid_n1_riccati \
        --refs origin/develop,bugfix/galerkin-scaled-lu-pr --force
  • Before/after Δ′ tables. The attached
    galerkin_pr_package.zip
    contains reproduce/:

    • the g147131 and example decks, with their equilibrium files;
    • the validate.jl driver;
    • run_validation.sh, run once against a checkout at 6553fe205 and once at 8a8a3cf17;
    • summarize.py, which prints the tables above;
    • a README with the exact commands.

    The expected output and the logs of the runs cited here are in validation_8a8a3cf/.

Δ′ test coverage found while preparing this (not addressed here):

  • No unit test pins Galerkin Δ′ values. Harness cases gal_resistive_diiid and gal_resistive_pe are the only value-level Galerkin Δ′ trackers. diiid_n1_riccati tracks Riccati Δ′.
  • docs/src/developer_notes.md says the Δ′ diagonal is pinned in test/runtests_parallel_integration.jl. That file now only has a comment saying the per-surface assertions were removed.
  • The diiid_n1 sample report in the docs shows a delta prime row that the current diiid_n1.toml no longer tracks; it tracks only the ballooning Δ′ checksum.

… LU solve

The assembled Galerkin matrix diagonal spans ~2e-15..2e10, and unscaled banded LU
(gbtrf/gbtrs) lost the small resonant coefficients that carry Delta': on DIII-D
g147131 n=1 the 4/1 row varied erratically with gal_nx and disagreed with Riccati.
Symmetrically scale A <- D A D, d_j = |A_jj|^(-1/2) (1 for a zero diagonal), before
factoring, and un-scale every solution column (incl. rpec coil columns).

Add test/runtests_galerkin.jl: a badly scaled banded system that unscaled LU solves
to ~1e-4 relative error, now ~1e-15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added bugfix Something was wrong and now is not changed-results Results move or an interface breaks - read before upgrading labels Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

This pull request is missing an assignee.

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.

@matt-pharr matt-pharr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@StuartBenjamin this is YUGE and I wish I had the numerical linear algebra knowledge to have thought of this myself. I am very excited to do a broader sensitivity study to see how much this improves RDCON's convergence/accuracy. This is a nice atomic addition, thank you.

@matt-pharr
matt-pharr enabled auto-merge October 8, 2026 13:15
@matt-pharr
matt-pharr disabled auto-merge October 8, 2026 13:37
@matt-pharr
matt-pharr merged commit eb8e5f1 into develop Oct 8, 2026
6 of 7 checks passed
@matt-pharr
matt-pharr deleted the bugfix/galerkin-scaled-lu-pr branch October 8, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Something was wrong and now is not changed-results Results move or an interface breaks - read before upgrading

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants