Skip to content

Commit c55acb5

Browse files
committed
Merge branch 'feature/3Dvac_nfp' into cleanup/PE_cleanup
2 parents 629f62d + cb12d9f commit c55acb5

90 files changed

Lines changed: 16952 additions & 15259 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 0 additions & 52 deletions
This file was deleted.

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../CLAUDE.md

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*.i
99
*.out
1010
*.h5
11+
# kinetic-profile inputs are tracked despite the blanket *.h5 (output) ignore
12+
!examples/**/*kinetic*.h5
1113
.vscode
1214
*.DS_Store
1315
*.pdf

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLAUDE.md

GEMINI.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLAUDE.md

benchmarks/benchmark_diiid_ideal_ntv_torque.jl

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ Read `T_total_*` and `dW_total_*` scalar reference values from the Fortran
7777
function read_pentrc_reference(pentrc_nc::String)
7878
NCDatasets.Dataset(pentrc_nc, "r") do ds
7979
gatt = ds.attrib
80+
# Reference runs may enable only a subset of methods (e.g. fgar only); missing attributes → NaN.
81+
att(k) = haskey(gatt, k) ? Float64(gatt[k]) : NaN
8082
return (
81-
T_total_fgar=Float64(gatt["T_total_fgar"]),
82-
T_total_tgar=Float64(gatt["T_total_tgar"]),
83-
dW_total_fgar=Float64(gatt["dW_total_fgar"]),
84-
dW_total_tgar=Float64(gatt["dW_total_tgar"])
83+
T_total_fgar=att("T_total_fgar"),
84+
T_total_tgar=att("T_total_tgar"),
85+
dW_total_fgar=att("dW_total_fgar"),
86+
dW_total_tgar=att("dW_total_tgar")
8587
)
8688
end
8789
end
@@ -142,7 +144,6 @@ nn_high = 1
142144
delta_mlow = 8
143145
delta_mhigh = 8
144146
mthvac = 512
145-
thmax0 = 1
146147
147148
kinetic_source = "fixed"
148149
kinetic_factor = 0.0
@@ -264,15 +265,14 @@ function run_benchmark(fortran_dir::String=DEFAULT_FORTRAN_DIR)
264265
_p("\n--- Build PE state (Fortran Clebsch → dbob_m/divx_m) ---")
265266
t1 = time()
266267

267-
# Construct PerturbedEquilibriumState with Fortran Clebsch data
268-
# Fortran xclebsch stores ξ^α directly; PE convention is ξ^α/χ₁
269-
chi1 = 2π * equil.psio
268+
# Construct PerturbedEquilibriumState with Fortran Clebsch data.
269+
# Fortran gpec_xclebsch already writes ξ^α = xms/χ₁, exactly what the KF operator consumes — feed directly.
270270
pe_state = PE.PerturbedEquilibriumState(;
271271
psi_grid=psi_grid_f,
272272
xi_modes=(
273273
clebsch_psi=clebsch_psi,
274274
clebsch_psi1=clebsch_psi1,
275-
clebsch_alpha=clebsch_alpha ./ chi1 # Store as ξ^α/χ₁ per PE convention
275+
clebsch_alpha=clebsch_alpha
276276
)
277277
)
278278

@@ -341,7 +341,7 @@ function run_benchmark(fortran_dir::String=DEFAULT_FORTRAN_DIR)
341341
if haskey(kf_state.method_results, method_key)
342342
mr = kf_state.method_results[method_key]
343343
T_julia = real(mr.total_torque)
344-
dW_julia = real(mr.total_energy)
344+
dW_julia = mr.total_energy
345345
T_err = abs(T_julia - ref_T) / abs(ref_T) * 100
346346
dW_err = abs(dW_julia - ref_dW) / abs(ref_dW) * 100
347347

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Quantifies whether splining iota = 1/q instead of q would reduce the knot count needed
2+
# for a given edge accuracy (GitHub discussion around the auto-grid redesign; PR #179
3+
# lineage owns any production iota work). For a grid ending at psihigh < 1, interpolation
4+
# error transforms as err_q ≈ err_iota · q², cancelling the flattening of the profile, so
5+
# no leading-order gain is expected — this script documents that with numbers on the
6+
# DIII-D-like example equilibrium.
7+
#
8+
# Usage: julia --project=. benchmarks/benchmark_q_vs_iota_edge.jl
9+
# Outputs (not committed): benchmarks/q_vs_iota_edge.csv, benchmarks/q_vs_iota_edge.png
10+
11+
using Pkg;
12+
Pkg.activate(joinpath(@__DIR__, ".."))
13+
using GeneralizedPerturbedEquilibrium
14+
using FastInterpolations
15+
using Printf
16+
using Plots
17+
18+
const GPE = GeneralizedPerturbedEquilibrium
19+
const EXAMPLE_DIR = joinpath(@__DIR__, "..", "examples", "DIIID-like_ideal_example")
20+
21+
# Dense ldp reference equilibrium: treat its q(ψ) as ground truth
22+
function reference_q()
23+
_, eq_config, additional_input = GPE.build_inputs_from_toml(EXAMPLE_DIR)
24+
eq_config.grid_type = "ldp"
25+
eq_config.mpsi = 1024
26+
equil = GPE.Equilibrium.setup_equilibrium(eq_config, additional_input)
27+
return equil, eq_config
28+
end
29+
30+
# Rational surface ψ_s(q = m/n) and q'(ψ_s) recovered from a fitted spline by bisection
31+
function rational_locations(q_itp, dq_itp, psis, q_targets)
32+
out = Dict{Float64,Tuple{Float64,Float64}}()
33+
for qt in q_targets
34+
lo, hi = psis[1], psis[end]
35+
(q_itp(lo) - qt) * (q_itp(hi) - qt) > 0 && continue
36+
for _ in 1:200
37+
mid = 0.5 * (lo + hi)
38+
(q_itp(lo) - qt) * (q_itp(mid) - qt) <= 0 ? (hi = mid) : (lo = mid)
39+
end
40+
ps = 0.5 * (lo + hi)
41+
out[qt] = (ps, dq_itp(ps))
42+
end
43+
return out
44+
end
45+
46+
function main()
47+
equil, eq_config = reference_q()
48+
xs_ref = equil.profiles.xs
49+
q_ref_itp = equil.profiles.q_spline
50+
dq_ref_itp = equil.profiles.q_deriv
51+
psilow, psihigh = xs_ref[1], xs_ref[end]
52+
53+
q_lo = ceil(q_ref_itp(psilow))
54+
q_hi = floor(q_ref_itp(psihigh))
55+
q_targets = collect(q_lo:q_hi) # n=1 rationals
56+
ref_rats = rational_locations(q_ref_itp, dq_ref_itp, xs_ref, q_targets)
57+
58+
psi_eval = collect(range(psilow, psihigh; length=4001))
59+
edge_band = psi_eval .> 0.9
60+
61+
rows = String[]
62+
push!(rows, "N,repr,max_rel_q_err,edge_rel_q_err,edge_rel_qprime_err,max_psi_s_err,max_rel_q1_err")
63+
results = Dict{String,Vector{NTuple{2,Float64}}}("q" => [], "iota" => [])
64+
65+
for N in (16, 32, 64, 128, 256)
66+
# Same log_asymptotic knot layout the auto grid uses for fixed mpsi
67+
cfg = deepcopy(eq_config)
68+
cfg.mpsi = N
69+
knots = GPE.Equilibrium._build_psi_grid(cfg, psilow, psihigh)
70+
q_nodes = [q_ref_itp(p) for p in knots]
71+
72+
for repr in ("q", "iota")
73+
itp = repr == "q" ? cubic_interp(knots, q_nodes; extrap=ExtendExtrap()) :
74+
cubic_interp(knots, 1.0 ./ q_nodes; extrap=ExtendExtrap())
75+
ditp = deriv1(itp)
76+
qf = repr == "q" ? (p -> itp(p)) : (p -> 1.0 / itp(p))
77+
dqf = repr == "q" ? (p -> ditp(p)) : (p -> -ditp(p) / itp(p)^2) # q' = -ι'/ι²
78+
79+
q_err = [abs(qf(p) - q_ref_itp(p)) / abs(q_ref_itp(p)) for p in psi_eval]
80+
dq_err = [abs(dqf(p) - dq_ref_itp(p)) / max(abs(dq_ref_itp(p)), 1e-10) for p in psi_eval]
81+
rats = rational_locations(qf, dqf, knots, q_targets)
82+
psi_s_err = maximum(abs(rats[qt][1] - ref_rats[qt][1]) for qt in keys(ref_rats); init=0.0)
83+
q1_err = maximum(abs(rats[qt][2] - ref_rats[qt][2]) / abs(ref_rats[qt][2]) for qt in keys(ref_rats); init=0.0)
84+
85+
push!(rows, @sprintf("%d,%s,%.3e,%.3e,%.3e,%.3e,%.3e",
86+
N, repr, maximum(q_err), maximum(q_err[edge_band]), maximum(dq_err[edge_band]), psi_s_err, q1_err))
87+
push!(results[repr], (Float64(N), maximum(q_err[edge_band])))
88+
end
89+
end
90+
91+
csv_path = joinpath(@__DIR__, "q_vs_iota_edge.csv")
92+
open(csv_path, "w") do io
93+
foreach(r -> println(io, r), rows)
94+
end
95+
println("Wrote ", abspath(csv_path))
96+
foreach(println, rows)
97+
98+
p = plot(; xscale=:log10, yscale=:log10, xlabel="knots N", ylabel="max relative q error, ψ > 0.9",
99+
title="q-spline vs ι-spline edge accuracy", legend=:topright, left_margin=12Plots.mm, bottom_margin=4Plots.mm)
100+
for (repr, pts) in results
101+
plot!(p, first.(pts), last.(pts); marker=:circle, lw=2, label="$repr-spline")
102+
end
103+
png_path = joinpath(@__DIR__, "q_vs_iota_edge.png")
104+
savefig(p, png_path)
105+
println("Wrote ", abspath(png_path))
106+
end
107+
108+
main()

benchmarks/benchmark_solovev_kinetic_stability.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ band (`delta_mlow=delta_mhigh=0`), same `nl`, collision operator, grid, and
1414
edge truncation (`sas_flag`/`dmlim`).
1515
1616
The comparison is the least-stable total-energy eigenvalue:
17-
- Julia: `FreeBoundaryStability/XiNorm/eigenmode_energies[1]` in `gpec.h5`
17+
- Julia: `FreeBoundaryStability/eigenmode_energies[1]` in `gpec.h5`
1818
- Fortran: the "Energies: ... real = …, imaginary = …" line printed by `dcon`
1919
(== `W_t_eigenvalue[1]`; parsing the log avoids a NetCDF dependency).
2020
@@ -157,7 +157,7 @@ function run_julia_reference()
157157
GPE.main([rundir])
158158
wall = time() - t0
159159
et = h5open(joinpath(rundir, "gpec.h5"), "r") do h5
160-
read(h5["FreeBoundaryStability/XiNorm/eigenmode_energies"])
160+
read(h5["FreeBoundaryStability/eigenmode_energies"])
161161
end
162162
return real(et[1]), imag(et[1]), wall
163163
end

benchmarks/compare_gal_vs_el.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# EL : ξ_EL(ψ) = U_EL(ψ) · (U_EL_edge \ w) (fundamental matrix, integration/xi_psi)
55
# gal: ξ_gal(ψ) = U_gal(ψ) · w (identity-at-edge ⇒ coefficient is w itself)
6-
# w = eigenvector of the total energy operator W = W_plasma + W_vacuum (FreeBoundaryStability/XiNorm).
6+
# w = eigenvector of the total energy operator W = W_plasma + W_vacuum (FreeBoundaryStability).
77
#
88
# Needs ONE gpec.h5 from a run with populate_dense_xi=true (EL dense u_store), gal_match_flag=true,
99
# gal_ideal_flag=true (gal ideal matched), vac_flag=true (energy operator).
@@ -19,8 +19,8 @@ ksel = length(ARGS) >= 3 ? ARGS[3] : "highest"
1919
to_c(a) = eltype(a) <: Complex ? ComplexF64.(a) : map(x -> ComplexF64(x.re, x.im), a)
2020

2121
et, wt, u1, psiE, gxi, psiG, issing, mlow, sing_psi = h5open(h5path) do f
22-
(to_c(read(f["FreeBoundaryStability/XiNorm/eigenmode_energies"])),
23-
to_c(read(f["FreeBoundaryStability/XiNorm/W_freeboundary_eigenmodes"])),
22+
(to_c(read(f["FreeBoundaryStability/eigenmode_energies"])),
23+
to_c(read(f["FreeBoundaryStability/W_freeboundary_eigenmodes"])),
2424
to_c(read(f["integration/xi_psi"])), read(f["integration/psi"]),
2525
to_c(read(f["galerkin/match/xi"])), read(f["galerkin/solution/psi"]),
2626
Bool.(read(f["galerkin/solution/issing"])), read(f["info/mlow"]),
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Visualizes where the two-pass auto grid places its radial knots compared to the fixed
2+
# ldp (sin²) grids, on the DIII-D-like example. Two panels: cumulative knot fraction
3+
# index/mpsi vs ψ_N (packing profile), and local knot spacing Δψ vs ψ_N (log scale),
4+
# with the n=1 rational surfaces marked. Documents the auto-grid PR.
5+
#
6+
# Usage: julia --project=. benchmarks/plot_grid_knot_placement.jl
7+
# Output (not committed): benchmarks/grid_knot_placement.png
8+
9+
using Pkg;
10+
Pkg.activate(joinpath(@__DIR__, ".."))
11+
using GeneralizedPerturbedEquilibrium
12+
using Plots
13+
14+
const GPE = GeneralizedPerturbedEquilibrium
15+
const EXAMPLE_DIR = joinpath(@__DIR__, "..", "examples", "DIIID-like_ideal_example")
16+
17+
function build_grids()
18+
_, eq_config, additional_input = GPE.build_inputs_from_toml(EXAMPLE_DIR)
19+
tau = eq_config.psi_accuracy
20+
21+
# Pass 1 (coarse layout) and the two-pass refined grid, as the driver builds them
22+
equil1 = GPE.Equilibrium.setup_equilibrium(eq_config, additional_input)
23+
pass1 = copy(equil1.profiles.xs)
24+
rationals = GPE.ForceFreeStates.rational_psi_nodes(equil1; nlow=1, nhigh=1)
25+
auto = GPE.Equilibrium.refined_psi_grid(equil1; tau, mandatory=rationals)
26+
27+
ldp(mpsi) = [eq_config.psilow + (eq_config.psihigh - eq_config.psilow) * sin((i / mpsi) */ 2))^2 for i in 0:mpsi]
28+
29+
grids = [
30+
("auto two-pass, τ=$(tau), mpsi=$(length(auto) - 1)", auto),
31+
("pass-1 layout, mpsi=$(length(pass1) - 1)", pass1),
32+
("ldp, mpsi=256", ldp(256)),
33+
("ldp, mpsi=512", ldp(512))
34+
]
35+
return grids, rationals
36+
end
37+
38+
function main()
39+
grids, rationals = build_grids()
40+
# Okabe-Ito colorblind-safe hues, fixed order; pass-1 recessive gray
41+
colors = ["#0072B2", "#999999", "#E69F00", "#009E73"]
42+
styles = [:solid, :dash, :solid, :solid]
43+
44+
p1 = plot(; xlabel="ψ_N", ylabel="knot index / mpsi", title="Radial knot packing — DIIID-like example (n=1)",
45+
legend=:bottomright, left_margin=12Plots.mm, bottom_margin=4Plots.mm, size=(900, 420))
46+
p2 = plot(; xlabel="ψ_N", ylabel="local knot spacing Δψ_N", yscale=:log10,
47+
legend=:bottomleft, left_margin=12Plots.mm, bottom_margin=6Plots.mm, size=(900, 420))
48+
49+
for (i, (label, g)) in enumerate(grids)
50+
n = length(g) - 1
51+
plot!(p1, g, (0:n) ./ n; lw=2, color=colors[i], ls=styles[i], label=label)
52+
mids = 0.5 .* (g[1:(end-1)] .+ g[2:end])
53+
plot!(p2, mids, diff(g); lw=2, color=colors[i], ls=styles[i], label=label)
54+
end
55+
for (j, r) in enumerate(rationals)
56+
for p in (p1, p2)
57+
vline!(p, [r]; color="#CC79A7", ls=:dot, lw=1.5, label=(j == 1 && p === p1) ? "rational surfaces q=m/1" : "")
58+
end
59+
end
60+
61+
fig = plot(p1, p2; layout=(2, 1), size=(900, 840))
62+
out = joinpath(@__DIR__, "grid_knot_placement.png")
63+
savefig(fig, out)
64+
println("Wrote ", abspath(out))
65+
end
66+
67+
main()

0 commit comments

Comments
 (0)