Skip to content

ForceFreeStates.Galerkin - FEATURE - Add the banded Cholesky solve for gal_solver = "cholesky" - #513

Open
StuartBenjamin wants to merge 4 commits into
developfrom
feature/galerkin-cholesky
Open

StuartBenjamin wants to merge 4 commits into
developfrom
feature/galerkin-cholesky

Conversation

@StuartBenjamin

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: none on existing cases: diiid_n1, gal_resistive_diiid and diiid_n1_riccati are bit-identical. The new case lar_gal_cholesky sets its first values. (harness @ b5b430f)
  • Migration: none

gal_solver = "cholesky" now works for the Galerkin Δ′ solver. Before, it crashed with UndefVarError: pbtrf!. It factors the Galerkin matrix with LAPACK's banded Hermitian Cholesky (zpbtrf/zpbtrs, lower band, unscaled), the same method as the original RDCON implementation. When the matrix is not positive definite, it stops with an error that points to gal_solver = "LU". The new example examples/LAR_gal_cholesky_example shows a case where Cholesky applies.

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: feature/galerkin-cholesky  @ b5b430f57 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
-----------------------------------------------------------------------------------------------------------
Quantity                                      origin/develop   feature/galerkin-cholesky  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)                                   364.8s           361.5s                                --    
||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: feature/galerkin-cholesky  @ b5b430f57 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
-----------------------------------------------------------------------------------
Quantity                 origin/develop  feature/galerkin-cholesky  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]                   0.0e+00  OK    
||gal Δ′ matrix||        1.243991e+04    1.243991e+04               0.0e+00  OK    
||gal Δ_coil block||     2.452669e+02    2.452669e+02               0.0e+00  OK    
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.074025e-02               0.0e+00  OK    
gal match residual       2.637801e-16    2.637801e-16               0.0e+00  OK    
Runtime (s)              190.0s          197.8s                              --    
===================================================================================
Summary: 10 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: feature/galerkin-cholesky  @ b5b430f57 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
-------------------------------------------------------------------------------
Quantity                origin/develop  feature/galerkin-cholesky  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.1s          206.9s                           --    
===============================================================================
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: feature/galerkin-cholesky  @ b5b430f57 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
-----------------------------------------------------------------------------------------
Quantity                       origin/develop  feature/galerkin-cholesky  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)                    241.7s          236.0s                              --    
=========================================================================================
Summary: 17 unchanged


================================================================
Case: lar_gal_cholesky — LAR analytic equilibrium, n=1, Galerkin Δ′ via the banded Cholesky solver
================================================================

Regression Report: lar_gal_cholesky
================================================================================
Ref 1: origin/develop  @ 6553fe205 (2026-10-07) (FAILED)
       env: environment unknown (cached before fingerprinting)
Ref 2: feature/galerkin-cholesky  @ b5b430f57 (2026-10-07)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 4 threads/1 BLAS
  Ref 1 error: Example directory not found: examples/LAR_gal_cholesky_example
--------------------------------------------------------------------------------
Quantity                 origin/develop  feature/galerkin-cholesky  Diff  Status
--------------------------------------------------------------------------------
gal # singular surfaces  FAILED          2                                FAILED
gal singular q values    FAILED          [2 elem]                         FAILED
gal PEST3 Δ diagonal     FAILED          [2 elem]                         FAILED
gal PEST3 Δ (all)        FAILED          [2 elem]                         FAILED
||gal Δ′ matrix||        FAILED          2.005979e+03                     FAILED
gal D_I per surface      FAILED          [2 elem]                         FAILED
gal α per surface        FAILED          [2 elem]                         FAILED
Runtime (s)              FAILED          166.5s                           --    
================================================================================
Summary: 7 missing/N/A

Notes for reviewers

Reading the harness report.

  • lar_gal_cholesky shows FAILED for origin/develop, which makes the harness exit non-zero. That is expected: the case's example deck is new in this PR, so develop has nothing to run. On this branch it records the values below.
  • gal_resistive_pe is N/A on both refs. This is pre-existing on develop: with the Galerkin integrator, PerturbedEquilibrium skips the plasma-response and singular-coupling stages ("free_boundary was not produced by the galerkin integrator").

What changed (src/ForceFreeStates/Galerkin/GalerkinSolve.jl, Cholesky branch only):

  • LinearAlgebra.LAPACK has no pbtrf!/pbtrs!, so the option could never run. Two small ccall wrappers, gal_zpbtrf! and gal_zpbtrs!, call LAPACK zpbtrf('L')/zpbtrs('L') through libblastrampoline. That is the same library the LU path's gbtrf! uses, so no new dependency is added.
  • They operate on the lower band the assembly already builds for Cholesky (ldab = kl + 1). There is no scaling, matching the reference implementation.
  • One deliberate deviation from the reference. The reference implementation ignores zpbtrf's info code, so on a matrix that is not positive definite it continues with a partial factor and returns a wrong Δ′ silently. Here info > 0 raises an error naming gal_solver = "LU".
  • Other changes: a docstring fix in GalerkinStructs.jl, a solver note in docs/src/galerkin.md, the new example, the new harness case and a new unit test.
  • The LU path is untouched. The bugfix PR bugfix/galerkin-scaled-lu-pr changes only the LU branch, so the two PRs edit disjoint lines and merge cleanly in either order (checked with git merge-tree).

Where Cholesky applies. It needs a Hermitian positive-definite Galerkin matrix.

Case Positive definite? Notes
LAR analytic equilibrium (ε = 0.2, low β, circular) Yes See the table below.
Solovev example Yes Runs, but Δ′ there is pathological for any solver (about -1e10; Solovev is near marginal).
DIII-D-like example, gal_nx 256 and 1024 No Stops with the error above.
DIII-D g147131, gal_nx 256, 1024 and 2048 No Stops with the error above. Raising gal_nx does not help.

Evidence: examples/LAR_gal_cholesky_example (n = 1), Re Δ′. All runs on this PR @ b5b430f57, same deck.

2/1 diag 3/1 diag 2/1→3/1 3/1→2/1
Cholesky, gal_nx = 128 10.5699 1.44769
Cholesky, gal_nx = 256 (example as committed) 10.5699 1.44799 4.73215 0.940684
Cholesky, gal_nx = 512 10.5699 1.44814
Cholesky, gal_nx = 1024 10.5699 1.44823
LU, gal_nx = 256 10.5698 1.44803 4.73268 0.940704
Riccati, same deck 10.5675 1.43765
Riccati, LAR_ideal_match_test ODE settings (eulerlagrange_tolerance = 1e-12, …) 10.5692 1.44073 4.73208 0.940792
  • Cholesky equals LU to about 3e-5 relative and is converged in gal_nx: 2/1 doesn't change, and 3/1 moves 0.04% from 128 to 1024.
  • Cholesky is within 0.03% of Riccati on 2/1 and 0.7% on 3/1.
  • About a third of the 3/1 gap is Riccati's own sensitivity to its ODE settings: Riccati's 3/1 value moves by 0.2% between this deck's defaults and the tighter LAR_ideal_match_test settings.
  • On develop, the same deck fails with UndefVarError: pbtrf! not defined in LinearAlgebra.LAPACK.

Tests. The new test/runtests_galerkin_cholesky.jl checks four things:

  • the solve on a Hermitian positive-definite banded system against the exact solution, with two right-hand sides;
  • the band factor against the dense cholesky factor;
  • that an indefinite matrix is refused.

At b5b430f57 (1 CPU job):

Test set Passed
Galerkin banded Cholesky 4/4
solve API 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 of this branch.

  • Unit test:

    julia --project=. test/runtests.jl test/runtests_galerkin_cholesky.jl
  • The example:

    julia --project=. -e 'using GeneralizedPerturbedEquilibrium; GeneralizedPerturbedEquilibrium.main(["examples/LAR_gal_cholesky_example"])'

    Δ′ is written to SingularSurfaces/Delta_prime_matrix in examples/LAR_gal_cholesky_example/gpec.h5.

  • Regression report:

    julia --project=regression-harness regression-harness/regress.jl \
        --cases diiid_n1,gal_resistive_diiid,gal_resistive_pe,diiid_n1_riccati,lar_gal_cholesky \
        --refs origin/develop,feature/galerkin-cholesky --force
  • Full table above (LU, Riccati and gal_nx variants, the DIII-D refusal, and the develop failure). The attached
    cholesky_pr_package.zip
    contains reproduce/:

    • the decks;
    • demo.jl and run_demo.sh;
    • a README with the exact commands.

    The expected logs of the runs cited here are in results_b5b430f/.

New regression case. lar_gal_cholesky runs examples/LAR_gal_cholesky_example and tracks:

  • the singular-surface count and q values;
  • the Δ′ diagonal and the full matrix;
  • the raw Δ′ norm;
  • D_I and α.

On develop this case has no example deck and Cholesky fails, so the harness reports no develop values for it. The report above records its first values.

…r gal_solver = "cholesky"

gal_solver = "cholesky" called LinearAlgebra.LAPACK.pbtrf!/pbtrs!, which do not exist, so the
option died with an UndefVarError. Call LAPACK zpbtrf('L')/zpbtrs('L') directly through
libblastrampoline, unscaled, on the lower band the assembly already builds for Cholesky. Unlike
the reference implementation, a non-positive-definite matrix (zpbtrf info > 0) raises an error
naming gal_solver = "LU" instead of continuing with a partial factor.

Add examples/LAR_gal_cholesky_example (LAR analytic equilibrium, positive-definite Galerkin
matrix), regression case lar_gal_cholesky, test/runtests_galerkin_cholesky.jl, and a solver note
in docs/src/galerkin.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feature New capability label Oct 7, 2026

@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.

Couple cleanup items and one main question, being whether we should do away with the flag altogether and just make every galerkin solve try to use cholesky first and then use LU as a fallback. See the relevant comment.

Comment thread examples/LAR_gal_cholesky_example/gpec.toml Outdated
Comment thread regression-harness/cases/lar_gal_cholesky.toml Outdated

# --- banded solve (gal.f) ---
ws.sol .= ws.rhs
if ws.solver == "LU"

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.

So we cannot always use Cholesky. However, we could remove this flag by making the code default to Cholesky since it is always the superior solve method, and then if we are not positive definite fall back to LU. This would increase the runtime by cases that are not LU, but the galerkin matrix solve is not the biggest piece in the rdcon runtime (I believe it is the computation of the weak form integrals which we have slowed down here by switching to Gauss–Kronrod quadrature from LSODE). How do you feel about this @StuartBenjamin ? Perhaps we should just make it, in pseudocode:

try: 
   sol = choelsky(galmat,b)
except MatrixIsNotPositiveDefinite:
   sol = LU(galmat, b)

Comment thread src/ForceFreeStates/Galerkin/GalerkinStructs.jl Outdated
x = ComplexF64[cis(0.1j) * (1 + j / n) for j in 1:n]
B = A * hcat(x, -3x)
ab = lowerband(A)
FFS.gal_zpbtrf!(ab, kl)

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.

is the point of this test just to make sure that gal_zpbtrf! works and does a cholesky solve?

This branch has not been deployed

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

Labels

feature New capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants