Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
ce6d0a6
ForceFreeStates - NEW FEATURE - Dual Riccati reformulation of EL ODE …
logan-nc Feb 28, 2026
0385e7f
ForceFreeStates - NEW FEATURE - Parallel FM integration + Δ' output
logan-nc Feb 28, 2026
1d2a863
ForceFreeStates - IMPROVEMENT - Fix Δ' computation and add Riccati/pa…
logan-nc Mar 1, 2026
11f394b
ForceFreeStates - CLEANUP - Correct explanation for why standard path…
logan-nc Mar 1, 2026
0ca20e2
ForceFreeStates - IMPROVEMENT - Off-diagonal Δ' column + parallel FM …
logan-nc Mar 1, 2026
5a7b756
ForceFreeStates - NEW FEATURE - STRIDE global BVP inter-surface Δ' ma…
logan-nc Mar 1, 2026
af7f359
ForceFreeStates - NEW FEATURE - Bidirectional parallel FM integration…
logan-nc Mar 2, 2026
9961fbd
ForceFreeStates - NEW FEATURE - Thread-scaling benchmark script
logan-nc Mar 8, 2026
7bb3942
ForceFreeStates - BUG FIX - Address Claude code review of perf/riccati
logan-nc Mar 8, 2026
88448fc
ForceFreeStates - NEW FEATURE - Sanity-check benchmarks for riccati_d…
logan-nc Mar 8, 2026
86b60a2
ForceFreeStates - CLEANUP - Clarify Riccati integration strategy docs…
logan-nc Mar 9, 2026
cb4c2bf
ForceFreeStates - IMPROVEMENT - Refactor runtests_riccati.jl: shared …
logan-nc Mar 9, 2026
c7dfa41
ForceFreeStates - IMPROVEMENT - Remove dead parallel_threads field; a…
logan-nc Mar 9, 2026
2eb3c26
ForceFreeStates - MERGE - Merge origin/develop (GPEC rename) into per…
logan-nc Mar 9, 2026
2f494c9
ForceFreeStates - CLEANUP - Update JPEC→GeneralizedPerturbedEquilibri…
logan-nc Mar 9, 2026
142a79c
ForceFreeStates - NEW FEATURE - Add stability analysis documentation …
logan-nc Mar 9, 2026
1515591
ForceFreeStates - BUG FIX - Fix CI failures (Random stdlib + docs mar…
logan-nc Mar 9, 2026
725a527
ForceFreeStates - IMPROVEMENT - Thread safety, psilim guard, consiste…
logan-nc Mar 9, 2026
a63dc69
Merge develop into perf/riccati
matt-pharr Apr 8, 2026
6431392
TESTING - FIX - fixed failing tests post merge
matt-pharr Apr 8, 2026
0d72584
TESTING - FIX - Re-add comments to gpec.toml accidentally removed in …
matt-pharr Apr 8, 2026
dc2b44b
BENCH - NEW - integration paths benchmark script
matt-pharr Apr 8, 2026
8dc4dc4
Merge remote-tracking branch 'origin/develop' into perf/riccati
matt-pharr Apr 8, 2026
290cfc5
EQUIL - NEW FEATURE - TJ analytic model (tj_run inverse + tj_run_direct)
d-burg Apr 18, 2026
5be4c98
ForceFreeStates - NEW FEATURE - Parallel FM Δ' BVP with inter-surface…
d-burg Apr 18, 2026
97a6826
BENCH - NEW - TJ pole-approach scans, regression case, and unit tests
d-burg Apr 18, 2026
d67cabd
CLEANUP - Drop unused deps, fix stale comments and docs
d-burg Apr 18, 2026
b9c177e
CLEANUP - Drop brittle Fortran/TJ source line-number citations from c…
d-burg Apr 18, 2026
57db4e7
Merge origin/develop into perf/riccati
d-burg Apr 18, 2026
5d5b8ee
ForceFreeStates - NEW FEATURE - Decouple edge-dW scan from integratio…
d-burg Apr 18, 2026
685a92a
EXAMPLES - BUG FIX - Remove stale kin_flag/con_flag from LAR/TJ scan …
d-burg Apr 18, 2026
defcec8
CI - BUG FIX - Restore Random stdlib dep and refresh test regression …
d-burg Apr 19, 2026
1dfc3ae
CI - BUG FIX - Loosen Solovev Δ'(surface 2) regression test
d-burg Apr 19, 2026
c6c845f
CI - BUG FIX - Handle maxthreadid() and decouple DIIID cross-path test
d-burg Apr 19, 2026
2fdae82
SLAYER - NEW FEATURE - Add SLAYERParameters and dimensional builder (…
d-burg Apr 19, 2026
e0c7397
SLAYER - NEW FEATURE - Add Fitzpatrick Riccati inner-layer Δ solver (…
d-burg Apr 19, 2026
61d844a
Dispersion - NEW FEATURE - Add SurfaceCoupling residual building bloc…
d-burg Apr 19, 2026
9d089be
Dispersion - CLEANUP - Remove leftover Newton root-finder files
d-burg Apr 19, 2026
71d69c5
Dispersion - NEW FEATURE - Add MultiSurfaceCoupling determinant resid…
d-burg Apr 19, 2026
dba61ca
Dispersion - NEW FEATURE - Brute-force Q-plane scan + find_growth_rat…
d-burg Apr 19, 2026
6cd5a5c
Dispersion - NEW FEATURE - AMR scan + triangulation-based growth-rate…
d-burg Apr 19, 2026
7a0f507
SLAYER - NEW FEATURE - KineticProfiles + LayerInputs builders (PR 7/9)
d-burg Apr 19, 2026
b170b49
SLAYER - NEW FEATURE - SLAYERRunner orchestration module (PR 8/9)
d-burg Apr 19, 2026
43c3b1d
SLAYER - NEW FEATURE - main() integration + Solovev example + regress…
d-burg Apr 19, 2026
8bfe74f
REFACTOR - Group tearing-mode modules under src/Tearing/ umbrella
d-burg Apr 19, 2026
3f82b7d
GGJ - NEW FEATURE - Per-surface E, F, G, H, K, M coefficients + build…
d-burg Apr 19, 2026
54d12fe
ForceFreeStates - NEW FEATURE - Port set_psilim_via_dmlim + default t…
d-burg Apr 20, 2026
4845ec8
ForceFreeStates - IMPROVEMENT - Default use_parallel=true, singfac_mi…
d-burg Apr 20, 2026
3ddc6f5
ForceFreeStates - IMPROVEMENT - Tighten defaults + use_parallel for d…
d-burg Apr 21, 2026
0a91a46
Utilities - NEW FEATURE - NeoclassicalResistivity module + switchable…
d-burg Apr 21, 2026
ede6fe2
ForceFreeStates - NEW FEATURE - Expose full 2m×2m D' matrix via delta…
d-burg Apr 21, 2026
ded86fe
InnerLayer - BUG FIX - InnerLayerResponse{tearing,interchange} + fix …
d-burg Apr 21, 2026
6410cd7
Dispersion - NEW FEATURE - CoupledFull 2m×2m det(D'−D(γ)) dispersion …
d-burg Apr 21, 2026
2172518
Dispersion - NEW FEATURE - CoupledFull 2m×2m det(D'−D(γ)) dispersion …
d-burg Apr 22, 2026
f3fe71a
Dispersion - IMPROVEMENT - CoupledFortranMatch inner_kwargs pass-through
d-burg Apr 22, 2026
ec00846
SLAYER - BUG FIX - Align Julia coupled-SLAYER dispersion with Fortran
d-burg Apr 23, 2026
2573553
Dispersion / GGJ - PERFORMANCE - Parallel amr_scan + preallocated Gal…
d-burg Apr 23, 2026
dd39a49
GGJ - BUG FIX - Remove erroneous Δ_crit offset from 4m×4m Pletzer-Dew…
d-burg Apr 24, 2026
568e431
WIP - SLAYER + GGJ - BUG FIX - Equilibrium-derived per-surface dr_val…
d-burg Apr 25, 2026
cce935a
SLAYER - NEW FEATURE - Adaptive pole_threshold = |mean(Δ)| for find_g…
d-burg Apr 26, 2026
db7c490
ForceFreeStates - BUG FIX - Wire ctrl.parallel_threads into BVP path;…
d-burg Apr 28, 2026
c45a634
ForceFreeStates - BUG FIX - Wire ctrl.parallel_threads into BVP path;…
d-burg Apr 28, 2026
7ac87c8
ForceFreeStates - PERFORMANCE - parallel_threads default 1 → 2 (≈20% …
d-burg Apr 28, 2026
48f433d
ForceFreeStates - PERFORMANCE - parallel_threads default 1 → 2 (≈20% …
d-burg Apr 28, 2026
c49d86b
SLAYER - PERFORMANCE - Convert Riccati ODE to scalar state (~30-40% f…
d-burg Apr 28, 2026
9ec12a0
SLAYER - DOCS - Document solver-AMR-topology coupling in Riccati docs…
d-burg Apr 28, 2026
adf27aa
SLAYER - PERFORMANCE - Pre-compute x-independent Riccati constants (~…
d-burg Apr 28, 2026
e7ce1c1
Dispersion - NEW FEATURE - amr_scan: snapshot_callback + max_cells_ac…
d-burg Apr 28, 2026
0fb5d75
Dispersion - NEW FEATURE - convergence_amr_resolution.jl: γ vs (nre0,…
d-burg Apr 28, 2026
5fd3a83
Dispersion - NEW FEATURE - multi_box_amr_scan: stripe scan with pre-s…
d-burg Apr 28, 2026
8bcd7f2
Dispersion - NEW FEATURE - find_growth_rates: spurious-root detection…
d-burg Apr 29, 2026
e97225c
Dispersion - IMPROVEMENT - find_growth_rates: polyline-walk concavity…
d-burg Apr 29, 2026
4c6fbe3
Dispersion - REFACTOR - find_growth_rates: drop :density flag, keep :…
d-burg Apr 29, 2026
33d791f
Dispersion - REFACTOR - find_growth_rates: remove dead angle_threshol…
d-burg Apr 29, 2026
af76269
Tearing.Runner - IMPROVEMENT - multi-box stripe scan + median-based p…
d-burg Apr 29, 2026
fda6597
EQUIL - BUG FIX - find_separatrix_crossing tolerates fixed-boundary e…
d-burg May 1, 2026
528062f
[WIP] Tearing.Dispersion - chooser_overrides warn-not-discard policy
d-burg May 9, 2026
3c8130d
ForceFreeStates - BUG FIX + EXAMPLES - truncate_at_dW_peak self-consi…
d-burg May 13, 2026
5acf147
ForceFreeStates - NEW FEATURE - Dense ξ in parallel BVP path + bit-id…
d-burg May 14, 2026
6d07c07
EQUIL - REFACTOR - Rename TJ → TJ-like with Fitzpatrick citation ever…
d-burg May 14, 2026
085de13
EXAMPLES - CLEANUP - LAR scans: single-file gpec.toml, per-line annot…
d-burg May 14, 2026
5a4c2c2
EXAMPLES - CLEANUP - Remove TJ_epsilon_pole_example and its regressio…
d-burg May 14, 2026
cc2affe
EQUIL - REFACTOR - Rename tj_like → tj_analytic (cleaner, less hedge-y)
d-burg May 14, 2026
8073c12
ForceFreeStates - BUG FIX - Preserve Riccati-gauge ca_l/ca_r across d…
d-burg May 14, 2026
3653a15
DOCS - CLEANUP - Move stride dev notes out of repo to CTM-processing
d-burg May 21, 2026
8bbe836
ForceFreeStates - CLEANUP - Address pre-merge review items
d-burg May 21, 2026
7efb15d
ForceFreeStates - TEST - Tighten Solovev kinetic multi-n rtol (drift …
d-burg May 21, 2026
c6c379c
ForceFreeStates - REFACTOR - Address PR 178 review: flag surfacing + …
d-burg May 21, 2026
0296956
ForceFreeStates / PerturbedEquilibrium - REFACTOR - De-emphasize per-…
d-burg May 21, 2026
a89bd5d
ForceFreeStates - CLEANUP - Pre-merge audit response (H1-H5, D1-D3, V…
d-burg May 22, 2026
ace5e21
Merge remote-tracking branch 'origin/develop' into perf/riccati
d-burg May 22, 2026
4fbd882
ForceFreeStates - CLEANUP - Pre-merge audit fixes (B1 thread-safety, …
d-burg May 25, 2026
9a42a61
Merge branch 'perf/riccati' into feature/tearing-growthrates
d-burg May 26, 2026
9609432
test - FIX - Unmask pre-existing SLAYER + multi-n fullruns failures p…
d-burg May 27, 2026
6f2a76e
Tearing - CLEANUP - Pre-merge audit: Coupled* consolidation, multi-n …
d-burg May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ version = "0.1.0"
[deps]
AdaptiveArrayPools = "4f381ef7-9af0-4cbe-99d4-cf36d7b0f233"
Contour = "d38c429a-6771-53c6-b99e-75d170b6e991"
DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df"
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78"
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838"
FastInterpolations = "9ea80cae-fc13-4c00-8066-6eaedb12f34b"
Expand All @@ -35,9 +37,11 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[compat]
AdaptiveArrayPools = "0.3.5"
Contour = "0.6.3"
DelaunayTriangulation = "1.6.6"
DelimitedFiles = "1.9.1"
DiffEqCallbacks = "4.9.0"
Documenter = "1.14.1"
DoubleFloats = "1.6.2"
FFTW = "1.9.0"
FastGaussQuadrature = "1.1.0"
FastInterpolations = "0.4"
Expand All @@ -60,3 +64,9 @@ Statistics = "1"
TOML = "1"
Test = "1"
julia = "1.11"

[extras]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[targets]
test = ["Random"]
95 changes: 95 additions & 0 deletions benchmarks/benchmark_delta_prime_methods.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Sanity check: compute_delta_prime_from_ca! vs inline Δ' from riccati_cross_ideal_singular_surf!
#
# riccati_cross_ideal_singular_surf! computes Δ' inline at each singular surface crossing
# using the diagonal formula (no Gaussian reduction permutation):
# Δ'[s] = (ca_r[ipert_res, ipert_res, 2, s] - ca_l[ipert_res, ipert_res, 2, s]) / (4π²·ψ₀)
#
# compute_delta_prime_from_ca! applies the identical formula post-hoc from the stored
# ca_l/ca_r arrays. Since both operate on the same data with the same formula, results
# should match to floating-point precision (not just approximately — exactly).
#
# This verifies that compute_delta_prime_from_ca! is a correct standalone implementation
# of the Δ' formula that can be used for testing or alternative integration drivers.
#
# Usage (from JPEC_main root):
# julia --project=. benchmarks/benchmark_delta_prime_methods.jl

using LinearAlgebra, Printf, TOML
using GeneralizedPerturbedEquilibrium

const FFS = GeneralizedPerturbedEquilibrium.ForceFreeStates

function setup_and_run_solovev()
ex = joinpath(@__DIR__, "..", "test", "test_data", "regression_solovev_ideal_example")
inputs = TOML.parsefile(joinpath(ex, "gpec.toml"))
inputs["ForceFreeStates"]["verbose"] = false
inputs["ForceFreeStates"]["use_riccati"] = true
intr = FFS.ForceFreeStatesInternal(; dir_path=ex)
ctrl = FFS.ForceFreeStatesControl(;
(Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...)
eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex)
equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config)
intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(;
(Symbol(k) => v for (k, v) in inputs["Wall"])...)
FFS.sing_lim!(intr, ctrl, equil)
intr.nlow = ctrl.nn_low; intr.nhigh = ctrl.nn_high; intr.npert = 1
FFS.sing_find!(intr, equil)
intr.mlow = min(intr.nlow * equil.params.qmin, 0) - 4 - ctrl.delta_mlow
intr.mhigh = trunc(Int, intr.nhigh * equil.params.qmax) + ctrl.delta_mhigh
intr.mpert = intr.mhigh - intr.mlow + 1
intr.mband = intr.mpert - 1
intr.numpert_total = intr.mpert * intr.npert
metric = FFS.make_metric(equil; mband=intr.mband, fft_flag=ctrl.fft_flag)
ffit = FFS.make_matrix(equil, intr, metric)
odet = FFS.riccati_eulerlagrange_integration(ctrl, equil, ffit, intr)
return ctrl, equil, ffit, intr, odet
end

println("\n=== compute_delta_prime_from_ca! consistency check ===")
println("Verifies the standalone Δ' formula matches the inline Riccati crossing computation.")
println("Expected error: exactly zero (same formula, same data).\n")

ctrl, equil, ffit, intr, odet = setup_and_run_solovev()
msing = intr.msing

# Capture Δ' values set inline by riccati_cross_ideal_singular_surf! during integration
delta_prime_inline = [copy(intr.sing[s].delta_prime) for s in 1:msing]

# Now call compute_delta_prime_from_ca! — it reads the same ca_l/ca_r arrays and
# overwrites intr.sing[s].delta_prime using the identical diagonal formula
FFS.compute_delta_prime_from_ca!(odet, intr, equil)

println(" N=$(intr.numpert_total) modes, $msing singular surfaces\n")
@printf(" %6s %4s %4s %22s %22s %12s\n",
"Surf", "m", "n", "Δ' (inline)", "Δ' (from_ca)", "abs diff")
println(" " * "-"^76)

max_absdiff = let max_absdiff = 0.0
for s in 1:msing
sing = intr.sing[s]
dp_from_ca = intr.sing[s].delta_prime
for i in eachindex(delta_prime_inline[s])
dp_il = delta_prime_inline[s][i]
dp_fc = dp_from_ca[i]
absdiff = abs(dp_fc - dp_il)
max_absdiff = max(max_absdiff, absdiff)
@printf(" %6d %4d %4d %22.6f%+.6fi %22.6f%+.6fi %12.4e\n",
s, sing.m[i], sing.n[i],
real(dp_il), imag(dp_il),
real(dp_fc), imag(dp_fc),
absdiff)
end
end
max_absdiff
end

println()
if max_absdiff == 0.0
println("PASSED — Δ' values are bit-for-bit identical (max abs diff = 0.0)")
elseif max_absdiff < 1e-14
@printf("PASSED — max abs diff = %.2e (floating-point rounding only)\n", max_absdiff)
else
@printf("FAILED — max abs diff = %.2e (expected exact agreement)\n", max_absdiff)
exit(1)
end
println()
131 changes: 131 additions & 0 deletions benchmarks/benchmark_riccati_der.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Sanity check: riccati_der! correctly evaluates the explicit Riccati ODE.
#
# riccati_der! implements [Glasser 2018 Phys. Plasmas 25, 032507, Eq. 19]:
# dS/dψ = w†·F̄⁻¹·w - S·Ḡ·S, w = Q - K̄·S
#
# where Q = diag(1/(m - n·q)), F̄ = L·L† (Cholesky), K̄ and Ḡ are the MHD
# metric matrices evaluated at ψ.
#
# NOTE: The identity between this Riccati ODE and the EL chain rule
# dS/dψ = dU₁·U₂⁻¹ - S·dU₂·U₂⁻¹
# holds ONLY for Hermitian S (physical states evolved from the axis, where
# S†=S is preserved by the EL symmetry). For arbitrary non-Hermitian (U₁, U₂),
# the two expressions differ — so this script compares riccati_der! against the
# explicit formula rather than against sing_der!.
#
# Usage (from JPEC_main root):
# julia --project=. benchmarks/benchmark_riccati_der.jl

using LinearAlgebra, Random, Printf, TOML
using GeneralizedPerturbedEquilibrium

const FFS = GeneralizedPerturbedEquilibrium.ForceFreeStates

function setup_solovev()
ex = joinpath(@__DIR__, "..", "test", "test_data", "regression_solovev_ideal_example")
inputs = TOML.parsefile(joinpath(ex, "gpec.toml"))
inputs["ForceFreeStates"]["verbose"] = false
intr = FFS.ForceFreeStatesInternal(; dir_path=ex)
ctrl = FFS.ForceFreeStatesControl(;
(Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...)
eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex)
equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config)
intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(;
(Symbol(k) => v for (k, v) in inputs["Wall"])...)
FFS.sing_lim!(intr, ctrl, equil)
intr.nlow = ctrl.nn_low; intr.nhigh = ctrl.nn_high; intr.npert = 1
FFS.sing_find!(intr, equil)
intr.mlow = min(intr.nlow * equil.params.qmin, 0) - 4 - ctrl.delta_mlow
intr.mhigh = trunc(Int, intr.nhigh * equil.params.qmax) + ctrl.delta_mhigh
intr.mpert = intr.mhigh - intr.mlow + 1
intr.mband = intr.mpert - 1
intr.numpert_total = intr.mpert * intr.npert
metric = FFS.make_metric(equil; mband=intr.mband, fft_flag=ctrl.fft_flag)
ffit = FFS.make_matrix(equil, intr, metric)
return ctrl, equil, ffit, intr
end

# Evaluate the Riccati RHS explicitly from splines: dS = w†·F̄⁻¹·w - S·Ḡ·S
function riccati_rhs_manual(S, psi, equil, ffit, intr)
N = intr.numpert_total
L = zeros(ComplexF64, N, N)
Kmat = zeros(ComplexF64, N, N)
Gmat = zeros(ComplexF64, N, N)
ffit.fmats_lower(vec(L), psi; hint=ffit._hint)
ffit.kmats(vec(Kmat), psi; hint=ffit._hint)
ffit.gmats(vec(Gmat), psi; hint=ffit._hint)

q = equil.profiles.q_spline(psi)
singfac = vec(1.0 ./ ((intr.mlow:intr.mhigh) .- q .* (intr.nlow:intr.nhigh)'))

# w = Q - K̄·S (Q is diagonal; add only the diagonal entries)
w = -Kmat * S
for i in 1:N
w[i, i] += singfac[i]
end

# v = F̄⁻¹·w via stored Cholesky factor L (L·L† = F̄)
v = copy(w)
ldiv!(LowerTriangular(L), v)
ldiv!(UpperTriangular(L'), v)

return adjoint(w) * v - S * Gmat * S
end

println("\n=== riccati_der! formula verification ===")
println("Verifies riccati_der! output matches manual evaluation of Glasser 2018 Eq. 19.")
println("Test state: Hermitian S (physical constraint). Expected error: ~machine epsilon.\n")

ctrl, equil, ffit, intr = setup_solovev()
N = intr.numpert_total

odet = FFS.OdeState(N, ctrl.numsteps_init, ctrl.numunorms_init, intr.msing)
FFS.initialize_el_at_axis!(odet, ctrl, equil.profiles, intr)
chunks = FFS.chunk_el_integration_bounds(odet, ctrl, intr)

# 30% into each chunk: well inside the interval, away from singularities at psi_end
test_psis = [c.psi_start + 0.3 * (c.psi_end - c.psi_start) for c in chunks]

println(" N=$N modes, $(length(test_psis)) test ψ points (30% into each chunk)\n")
@printf(" %8s %14s %14s %12s\n", "ψ", "‖dS_manual‖", "‖dS_ric‖", "rel error")
println(" " * "-"^54)

rng = Random.MersenneTwister(42)
threshold = 1e-10

max_err = let max_err = 0.0
for psi in test_psis
# Hermitian S: physical Riccati matrix is Hermitian (preserved by EL symmetry)
A = randn(rng, ComplexF64, N, N)
S = (A + A') / 2 # Hermitian by construction

# Manual RHS
dS_manual = riccati_rhs_manual(S, psi, equil, ffit, intr)

# riccati_der! RHS
u_ric = zeros(ComplexF64, N, N, 2)
du_ric = zeros(ComplexF64, N, N, 2)
u_ric[:, :, 1] .= S
u_ric[:, :, 2] .= Matrix{ComplexF64}(I, N, N)
dummy_chunk = FFS.IntegrationChunk(psi, psi, false, 0, 1)
params = (ctrl, equil, ffit, intr, odet, dummy_chunk)
FFS.riccati_der!(du_ric, u_ric, params, psi)
dS_ric = du_ric[:, :, 1]

ref = max(norm(dS_manual), 1e-10)
err = norm(dS_ric - dS_manual) / ref
max_err = max(max_err, err)
status = err < threshold ? "" : " ← FAIL"
@printf(" %8.4f %14.4e %14.4e %12.4e%s\n", psi, norm(dS_manual), norm(dS_ric), err, status)
end
max_err
end

println()
if max_err < threshold
@printf("PASSED — max rel error = %.2e (threshold %.0e)\n", max_err, threshold)
else
@printf("FAILED — max rel error = %.2e exceeds threshold %.0e\n", max_err, threshold)
exit(1)
end
println()
76 changes: 76 additions & 0 deletions benchmarks/benchmark_threads.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Thread-scaling benchmark for the bidirectional parallel FM integration.
# Runs the Solovev (N=8) and DIIID-like (N=26) examples with use_parallel=true
# across 1, 2, 4, 8 threads and compares against the serial Riccati path.
#
# Usage (from JPEC_main root):
# for t in 1 2 4 8; do julia -t $t --project=. benchmarks/benchmark_threads.jl; done

using GeneralizedPerturbedEquilibrium, TOML, Printf, Statistics

function run_ffs(ex; use_parallel, use_riccati=false)
inputs = TOML.parsefile(joinpath(ex, "gpec.toml"))
inputs["ForceFreeStates"]["verbose"] = false
inputs["ForceFreeStates"]["use_parallel"] = use_parallel
inputs["ForceFreeStates"]["use_riccati"] = use_riccati
inputs["ForceFreeStates"]["write_outputs_to_HDF5"] = false
intr = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesInternal(; dir_path=ex)
ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(;
(Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...)
eq_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(inputs["Equilibrium"], ex)
equil = GeneralizedPerturbedEquilibrium.Equilibrium.setup_equilibrium(eq_config)
intr.wall_settings = GeneralizedPerturbedEquilibrium.Vacuum.WallShapeSettings(;
(Symbol(k) => v for (k, v) in inputs["Wall"])...)
GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_lim!(intr, ctrl, equil)
intr.nlow = ctrl.nn_low; intr.nhigh = ctrl.nn_high; intr.npert = 1
GeneralizedPerturbedEquilibrium.ForceFreeStates.sing_find!(intr, equil)
intr.mlow = min(intr.nlow * equil.params.qmin, 0) - 4 - ctrl.delta_mlow
intr.mhigh = trunc(Int, intr.nhigh * equil.params.qmax) + ctrl.delta_mhigh
intr.mpert = intr.mhigh - intr.mlow + 1
intr.mband = intr.mpert - 1
intr.numpert_total = intr.mpert * intr.npert
metric = GeneralizedPerturbedEquilibrium.ForceFreeStates.make_metric(equil; mband=intr.mband, fft_flag=ctrl.fft_flag)
ffit = GeneralizedPerturbedEquilibrium.ForceFreeStates.make_matrix(equil, intr, metric)
odet, _, _, _ = GeneralizedPerturbedEquilibrium.ForceFreeStates.eulerlagrange_integration(ctrl, equil, ffit, intr)
vac = GeneralizedPerturbedEquilibrium.ForceFreeStates.free_run!(odet, ctrl, equil, ffit, intr)
return real(vac.et[1]), intr.numpert_total
end

function timed_run(ex; use_parallel, use_riccati=false, nwarm=1, nrep=2)
# Warmup
for _ in 1:nwarm
run_ffs(ex; use_parallel, use_riccati)
end
# Timed runs
times = Float64[]
local et1, N
for _ in 1:nrep
t0 = time()
et1, N = run_ffs(ex; use_parallel, use_riccati)
push!(times, time() - t0)
end
return mean(times), et1, N
end

nthreads = Threads.nthreads()
root = joinpath(@__DIR__, "..")
sol_ex = joinpath(root, "test", "test_data", "regression_solovev_ideal_example")
diiid_ex = joinpath(root, "examples", "DIIID-like_ideal_example")

println("\n=== Thread-scaling benchmark ($(nthreads) thread(s)) ===\n")

for (label, ex) in [("Solovev", sol_ex), ("DIIID-like", diiid_ex)]
t_std, et_std, N = timed_run(ex; use_parallel=false, use_riccati=false)
t_ric, et_ric, _ = timed_run(ex; use_parallel=false, use_riccati=true)
t_par, et_par, _ = timed_run(ex; use_parallel=true, use_riccati=false)

err_ric = abs(et_ric - et_std) / abs(et_std) * 100
err_par = abs(et_par - et_std) / abs(et_std) * 100

println("$label (N=$N, nthreads=$nthreads)")
@printf(" standard et[1]=%.5f t=%.2fs speedup=1.00×\n", et_std, t_std)
@printf(" riccati et[1]=%.5f t=%.2fs speedup=%.2f× err=%.4f%%\n",
et_ric, t_ric, t_std/t_ric, err_ric)
@printf(" parallel et[1]=%.5f t=%.2fs speedup=%.2f× err=%.4f%%\n",
et_par, t_par, t_std/t_par, err_par)
println()
end
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ makedocs(;
"API Reference" => [
"Vacuum" => "vacuum.md",
"Equilibrium" => "equilibrium.md",
"Stability Analysis" => "stability.md",
"Utilities" => "utilities.md",
"Forcing Terms" => "forcing_terms.md",
"Perturbed Equilibrium" => "perturbed_equilibrium.md",
Expand Down
2 changes: 1 addition & 1 deletion docs/src/equilibrium.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,4 @@ println("Built LAR equilibrium with a = ", lorcfg.lar_a)

## See also

- `docs/src/vacuum.md` — coupling between equilibrium and vacuum solvers
- `docs/src/stability.md` — ideal MHD stability analysis built on top of the equilibrium
Loading