EQUIL + ForceFreeStates - Extend equilibrium splines to separatrix for diverted plasmas#179
EQUIL + ForceFreeStates - Extend equilibrium splines to separatrix for diverted plasmas#179
Conversation
… separatrix for diverted plasmas
Adds infrastructure to integrate the ideal MHD ODE beyond psihigh toward the
near-separatrix edge layer in diverted plasmas, where q→∞ and dV/dψ→∞.
Key changes:
- New InverseCubicSpline{S} type wrapping an iota=1/q inner spline with a
self-contained hint; returns 1/inner(ψ) for well-behaved evaluation near psin=1
- ProfileSplines is now a mutable struct with a Union{S,I} q_spline pointer field
that can be temporarily swapped to the edge inverse spline for ODE chunks
beyond psihigh, with q_spline_direct always available for deriv2/deriv3
- X-point detection via Newton iteration (Br=0, Bz=0) with physically motivated
initial guesses on the inboard side (rs1 + 0.35*(ro-rs1)); correctly identifies
diverted vs limited plasma topology
- For diverted plasmas: edge iota inverse splines for q and dV/dψ built over
[0.9*psihigh, 1.0] with anchor iota(1.0)=0 (CubicFit BC)
- qa=Inf for diverted; q_for_mode_range fallback in JPEC.jl avoids trunc(Int,Inf)
- Extended rational surface search in sing_find! above psihigh using the iota
spline, terminating when surface spacing < edge_layer_width (new TOML param)
- sing_lim! updated for diverted: psilim from last edge surface, qlim=ctrl.qhigh
- Pointer swap in integrate_el_region! before/after edge ODE chunks
- eval_dVdpsi helper in check_for_zero_crossings! routes to dVdpsi_spline_inv
when psi > psihigh for correct post-integration stability evaluation
- Benchmark script: benchmarks/benchmark_edge_splines.jl with iota, q, dV/dψ
comparison plots and rational surface density visualization
Backward compatible: limited plasmas have edge fields=nothing and are unchanged.
All existing tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e spline feature - Fix dVdpsi_spline routing in Free.jl: dVdpsi_spline(psilim) in free_run! and free_compute_total now routes to dVdpsi_spline_inv when psilim exceeds psihigh for diverted plasmas, avoiding inaccurate ExtendExtrap at the edge - Add benchmark plots 4 (R,Z vs psin for several poloidal angles) and 5 (flux surface cross-section with x-point marked); rename rational surface density to plot 6; all plots use rzphi spline reconstruction R=ro+rfac*cos(η), Z=zo+rfac*sin(η) - Add edge_layer_width = 1e-4 to [ForceFreeStates] in DIIID-like example jpec.toml - Add formal test assertions for edge spline properties: limited plasma returns nothing, diverted plasma has correct anchor (iota(1.0)≈0), correct coverage, correct topology flags, qa=Inf, and continuity at psihigh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
59e6c27 to
746034f
Compare
…tend ODE integration past psihigh for diverted plasmas - Remove set_psilim_via_dmlim and dmlim control parameters entirely; integration limit is now determined solely by the psiedge < psilim logic in EulerLagrange.jl - Move sing_find! before sing_lim! in JPEC.jl so that edge rational surfaces above psihigh are available when sing_lim! sets psilim for diverted plasmas - sing_lim! now sets psilim = last_edge_surface + edge_layer_width/2 for diverted plasmas, giving a valid final ODE chunk beyond the last crossed surface - Fix intr.mtheta -> ctrl.mthvac bug in free_compute_wv_spline (Free.jl) - Resize dW_edge in findmax_dW_edge! to match current step count (integration may exceed initial numsteps_init allocation) - Handle SingularException in findmax_dW_edge! edge scan (degenerate U1 near the separatrix layer); those steps remain at -Inf and are ignored in max search - Fix transform_u! to cap kfix at odet.step and break early when jfix exceeds trimmed storage (prevents BoundsError after psiedge truncation) - Update DIIID-like example: remove dmlim, set psiedge = 0.995 to trigger edge dW scan above psihigh = 0.993 - Verified: DIIID-like end-to-end run integrates to psilim = 0.999 (q = 14.5), truncates at ψ = 0.995 (edge dW peak), completes FFS + PerturbedEquilibrium Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d stability plot - Add psi_edge_scan / et_edge_scan fields to OdeState; populated in findmax_dW_edge! from the full psi_store before trim_storage! cuts it to the peak-dW step, so the complete stability profile from psiedge → psilim is retained - Save integration/psi_edge_scan and integration/et_edge_scan to jpec.h5 when non-empty - benchmark_edge_splines.jl: add plot 7 (et[1] vs psi in edge zone, reads from jpec.h5), add HDF5 import, update docstring to list all 7 plots Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ted psi_store psi_store may be doubled by grow_storage! so it is larger than odet.step. Index only the valid [1:odet.step] range before applying the edge_mask. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e dW scan bugs; add F_iota_matched spline; GSE renames and plots Part A — Fix edge dW scan bugs in Free.jl: - Bug 1 (free_compute_total): dV/dψ was evaluated at intr.psilim instead of odet.psifac. Near the separatrix (psilim≈0.999), dV/dψ is ~100× larger than at the actual scan point, making the eigenvalue normalization wrong by ~100×. - Bug 2 (free_compute_wv_spline): VacuumInput used intr.psilim as the plasma boundary for all scan points. Now passes psi_array[i] so vacuum geometry corresponds to the surface actually being scanned. Part B — Benchmark Plot 7 now plots et[1] vs q (not psi): - Converts psi_es → q_es via iota inverse spline above psihigh - Adds hline at et=0 (stability boundary), vline at q(psihigh), and reference line at et=+1.707 (develop branch value at q≈5.2) Part C1 — GSE variable renames in equilibrium_gse!: - flux_fs→gs_div_term, flux_fsx→gs_div_dpsi, flux_fsy→gs_div_dtheta - s1→F_val, s1p→dF_dpsi, s2p→dP_dpsi - term→gse_terms_integrated, totali→gse_total_integrated - errori→gse_abs_error, errlogi→gse_log_error - Fixed pre-existing bug: gse.h5/gsei.h5 writes used nonexistent flux.xs/ys - Updated docstring to describe the GS equation and output groups Part C2 — F_iota_matched spline for self-consistent far-edge F(ψ): - ProfileSplines gains F_spline_direct, F_deriv_direct (stored originals), F_spline_iota_matched, F_deriv_iota_matched (Nothing for limited plasmas) - F_spline (and F_deriv) are now switchable pointers like q_spline - direct_build_F_iota_matched! computes J(ψ)=(1/2π)∫ J_coord/R² dθ from raw rzphi_nodes, scans n=1 rational surfaces for 10-pts-per-window grid density, then builds F_matched = q_target·2π/J; called inside if is_diverted block - integrate_el_region! switches both q_spline and F_spline to edge versions for ODE chunks that extend beyond psihigh, restores direct after Part C3 — Benchmark plots 8-10: - Plot 8: F(ψ) direct vs F_iota_matched with dF/dψ panel - Plots 9/10: GSE residual diagnostics via diagnose_src=true + HDF5 read Fixes stale dmlim/set_psilim_via_dmlim in two test TOML files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ace sampling, vac_psi psihigh cap, always-save in edge zone - free_compute_wv_spline: switch from q-space to psi-space sampling (20 core + 20 edge points); avoids inverting the direct q spline beyond psihigh where it extrapolates linearly and returns psi >> 1.0, building the wvmat spline over the wrong domain - free_compute_wv_spline: cap vac_psi = min(psi_array[i], psihigh) when calling VacuumInput; the rzphi bicubic spline only covers [psilow, psihigh] and extrapolating beyond gives negative r^2 values → sqrt crash - integrator_callback!: always save every ODE step when integrator.t >= ctrl.psiedge (edge scan zone); with save_interval=10 only ~3 steps fall in [psiedge, psihigh] = [0.990, 0.993] by default, causing findmax_dW_edge! to miss the stability peak in that region Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion at psihigh, skip u_store above psihigh, fix F_spline chunk condition - eulerlagrange_integration: break out of chunk loop when edge scan is active and chunk.psi_start > psihigh; findmax_dW_edge! only scans [psiedge, psihigh] so chunks above psihigh contribute nothing but take exponentially more computation near the separatrix (q=9 alone requires 86k+ ODE steps → OOM) - integrator_callback!: return early (skip u_store save) when edge_scan_active and integrator.t > psihigh; the near-separatrix steps are never read by findmax_dW_edge! and storing them at save_interval=10 causes OOM - integrate_el_region!: change using_edge_spline condition from psi_END > psihigh to psi_START >= psihigh; the q=5→q=6 chunk straddles psihigh (0.986→0.996), so psi_end > psihigh was triggering F_spline_iota_matched for psi < 0.993 where it extrapolates a wrong constant F value → corrupt U₁ → SingularException in free_compute_total for all [psiedge, psihigh] steps Result: full DIIID-like example completes in 18.6 s (vs OOM/hours before); truncation at ψ=0.992, q=5.277; et[1] = +1.763 (stable, consistent with develop branch reference of +1.707 at q=5.2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Session update (2026-03-02): edge dW scan bugs fixed, full DIIID-like run verifiedFive additional bugs found and fixed during verification of the edge dW scan: Bug 3 ( Bug 4 ( Bug 5 ( Bug 6 ( Bug 7 ( Verified result: Full DIIID-like example ( |
…and extend scan beyond psihigh
Two bugs in free_compute_wv_spline / free_compute_total caused the edge
stability scan to give et ≈ −14600 instead of the correct +1.763 for the
DIIID-like diverted plasma.
Bug 1 (dV/dψ at psilim instead of psifac, Free.jl ~line 211):
free_compute_total was evaluating dV/dψ at intr.psilim (≈ 0.999 near the
separatrix) instead of at odet.psifac (the current scan psi ≈ 0.990–0.993).
With psilim ≈ 0.999, dV/dψ is ~100× larger than at psiedge, making the
normalisation denominator huge and the eigenvalue tiny/wrong.
Fix: use odet.psifac for the dV/dψ lookup in free_compute_total.
Bug 2 (VacuumInput at psilim instead of psi_array[i], Free.jl ~line 172):
free_compute_wv_spline was building the wvmat CubicSeriesInterpolant using
intr.psilim ≈ 0.999 as the plasma boundary for all grid points in the spline.
This pinned all vacuum responses to the near-separatrix geometry and produced
norm_wv ≈ 26840 (wrong) instead of ≈ 5730 (correct). After the fix, the
scan gives et ≈ +1.627–1.763 across [psiedge, psihigh], matching the
develop-branch reference et = +1.707 (within 3.3%).
Fix: use psi_array[i] (the actual scan point) as the plasma boundary.
Additional improvements in this commit:
- free_compute_wv_spline now builds a CORE-ONLY grid [psiedge, psihigh] (20 pts)
with RAW vacuum response (no singfac). ExtendExtrap handles above-psihigh
queries by returning the psihigh value. This avoids the derivative
discontinuity at psihigh that previously shifted the scan peak.
- singfac = (m - n·q(psifac)) is now applied analytically in free_compute_total,
using the actual q at the scan psi. This keeps the spline well-conditioned.
- Extend the edge scan beyond psihigh into [psihigh, psilim]:
- EulerLagrange.jl: above-psihigh ODE chunks now run (up to 20k steps each)
with sparse step saving (every 100×save_interval steps) to avoid OOM.
- A per-chunk step budget cap (terminate!(integrator)) prevents exponential
compute growth as q → ∞ near the separatrix.
- integrator_callback! uses chunk_callback_count (not length(sol.t)) for
near_start detection, since save_positions=(false,false) is now used.
- OdeState gains chunk_callback_count, chunk_steps_start, chunk_is_above_psihigh
fields for the above-psihigh tracking logic.
- findmax_dW_edge! scans ALL stored steps (not just those ≤ psihigh).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gnostic (Plot 11)
Plot 7 (edge_spline_stability.png):
The previous version plotted the full scan [psiedge, psilim], including above-psihigh
steps where ExtendExtrap pins wv to the psihigh boundary geometry, producing et ≈ -1e8
that crushed the interesting core region (et ≈ +1.76 at q≈5.28) off-screen.
Fixed: split into core mask (psi ≤ psihigh, wv interpolated) and above-psihigh
(wv extrapolated, clamped to [-10, 10] for context). Core data now dominates
the y-axis and the peak at et=+1.763, q=5.2774 is clearly visible alongside the
develop-branch reference at et=+1.707.
Plot 11 (edge_spline_gse_edge.png) — new:
Three-panel edge GSE diagnostic showing the GS equation error from using the EFIT
F(ψ) (extrapolated via ExtendExtrap as constant beyond psihigh) vs the iota-matched F:
A. q_direct (EFIT) vs q_target = 1/ι(ψ): constant q vs diverging physical q → the
EFIT F has no information about the near-separatrix q profile.
B. Relative q deviation (q_direct − q_target)/q_target [%]: reaches -96% at ψ=0.9999,
showing the EFIT F implies q~5.33 everywhere when the true q → ∞.
C. GS source mismatch ΔFF′ = F_direct·F′_direct − F_matched·F′_matched: grows large
with oscillatory structure near ψ→1 (from rational surface resolution in F_iota_matched).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ics to plots 9 and 10 For plots 9 (GS residual per theta) and 10 (theta-integrated GS error), overlay the far-edge (psi > psihigh) GS residual computed using F_iota_matched. Above psihigh, the rzphi splines use ExtendExtrap (geometry frozen at psihigh), so the GS residual reduces to the source term alone: source = J/(2pi*psi0*pi^2) * (F*F'/(2pi*R)^2 + P'). Key features: - Evaluation grid matches the F_iota_matched spline node construction (12 pts per rational surface window), limited to [psihigh, edge_surfaces[end]] where F_iota_matched is physically relevant (avoids the divergent q->inf region near psi=1) - Both NODES (spline knots) and inter-node MIDPOINTS are evaluated and plotted distinctly in Plot 10, providing a check that the cubic spline interpolates smoothly - Far-edge lines use same colors as core per-theta lines in Plot 9 (dashed) for direct visual comparison - Core per-theta lines now use explicit colors to match far-edge overlay colors - Added `using FastInterpolations` to imports Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-edge rzphi extension Replace F_iota_matched approach with physically correct X-point asymptotic geometry. F'≈0 and P'≈0 near the separatrix means GS source ≈ 0 — no iota-matched F needed. Flux surfaces in the far edge (psin > psihigh) now scale from the X-point: R(ψ_n,θ) = R_X + s·(R_high−R_X), Z similarly, where s = √((1−ψ_n)/(1−psihigh)) Key changes: - DirectEquilibrium.jl: replace direct_build_F_iota_matched! with direct_build_xpoint_asymptotic!, which extends rzphi_nodes to psin=0.999999 using X-point scaling (10 pts per rational surface window) - direct_xpoint_find returns 6-tuple including ψ Hessian components at X-point - EquilibriumTypes.jl: add psi_hess_rr/zz/rz and psi_core_end to EquilibriumParameters; remove F_spline_iota_matched/F_deriv_iota_matched from ProfileSplines - Equilibrium.jl: equilibrium_gse!, equilibrium_separatrix_find!, equilibrium_global_parameters! use psi_core_end to index into core grid (pe.rzphi_xs is now the extended grid); fix gsei.h5 and gse.h5 to save psi_xs - EulerLagrange.jl: remove F_spline pointer swap (only q_spline swapped for edge chunks) - benchmark_edge_splines.jl: replace F comparison plot (Plot 8) with X-point geometry visualization; update far-edge GSE section; fix psi_gse_vals to use profiles.xs Result: core GS residual max = 3.71e-3 (below 1e-2); all 20 equilibrium tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_xs instead of core grid equil.rzphi_xs is now the extended grid (250 nodes for diverted plasmas) after the X-point asymptotic geometry extension. make_metric was using length(equil.rzphi_xs) as mpsi, causing make_matrix to index profiles.xs at out-of-bounds positions (130+). Fix: use length(profiles.xs) for mpsi in make_metric, which gives the core psi grid size (129 nodes). The FGK matrices only need to cover the core equilibrium region; the far-edge extension is only needed for the ODE integration via the rzphi splines. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… resonant spikes Previously the stability plot clipped at xlims = (4, q_psihigh * 1.02) ≈ (4, 5.44), hiding all above-psihigh data even though it was being plotted. The scan actually spans q = 5.16 → 7.99 (ψ = 0.990 → 0.998). Changes to Plot 7: - xlims expanded to full scan range: (4.0, q_max_scan * 1.02) - Above-psihigh data filtered to |et| ≤ 10 (removes resonant spikes where singfac → 0 near rational surfaces) and subsampled to ≤ 2000 points for rendering performance - ylims fixed to (-10, max(3, peak*1.2)) so spikes don't dominate the y scale - Added scan range and peak values to stdout summary - Title updated to show actual ψ scan bounds Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g; add SingularException guard
Key fixes:
1. EulerLagrange.jl: Move transform_u! to AFTER findmax_dW_edge!.
The scan must use raw (pre-transform) u_store. U₁_raw is kept non-singular
by the ODE fixups; the transformed U₁_core = U₁_raw × T₁ (where T₁ = G₁·…·Gₙ
is the product of ALL Gaussian matrices) is ill-conditioned with many fixups,
causing SingularException in free_compute_total for every core scan step.
With transform before scan, only above-psihigh steps (where T_{n+1} = I)
return values, forcing the peak to q≈8 instead of the correct q≈5.2.
2. FixedBoundaryStability.jl: Catch SingularException in check_for_zero_crossings!.
U₂ can be degenerate near rational surfaces in the above-psihigh integration
region. Propagate the previous crit_store value to avoid a spurious zero crossing.
3. Free.jl: Simplify free_compute_wv_spline; fix compute_vacuum_response to 4-value
destructuring (wv_block, _, _, _); update docstring to clarify core-only grid
design and rationale for ExtendExtrap above psihigh.
Verified: et[1] = +1.763 at ψ=0.991, q=5.195 (develop reference: +1.707 at q=5.2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rease wv spline nodes
Investigating why the edge dW scan gives only 6 valid points (|et|≤10)
out of 11664 before jumping to et≈−343 at psi=0.99117.
Changes in this commit:
1. free_compute_total: Replace vector @info with scalar diagnostic.
Previous @info printed 68-element eigval vectors that were truncated
as "68-element Vector{Float64}: …" in the log output, yielding no
useful information. New diagnostic prints min eigenvalues of wt, wp,
and wv separately, plus cond(U₁) raw and post-column-normalization.
This will distinguish whether the jump at psi=0.99117 is caused by
wp (U₁ conditioning) or wv (spline oscillation).
2. free_compute_wv_spline: Increase npsi_core from 20 to 50.
With 20 nodes in [psiedge=0.990, psihigh=0.993] (spacing 0.000158),
the scan step spacing (~0.000160) roughly equals the spline node
spacing — any anomalous node causes oscillation across adjacent scan
steps. 50 nodes (spacing 0.000061) gives ~3 scan steps per spline
interval, providing better regularization.
3. ForceFreeStatesStructs + Fourfit: Add jmat_spline infrastructure.
Builds a spline of Jacobian Fourier coefficients J(θ, ψ) vs ψ for
potential use in per-step jmat normalization. Currently unused (jmat
normalization deferred to free_run! where psilim is fixed) but
retained for future investigation.
Context: et[1]=+1.763 at the final truncation point is correct
(matches develop branch +1.707). The scan values for steps 1-6
(+1.669→+1.800→+1.744) are smooth and physical. The jump to -343 at
step 7 is the unresolved bug. cond(U₁)=1.49e90 at both the last
valid step and the first invalid step — ruling out simple conditioning
as the cause. Next step: run with new diagnostics to see whether
evwv_min or evwp_min jumps at the transition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Edge dW scan investigation — current status ($(date '+%Y-%m-%d'))What's working
The bugAfter psi=0.99101, Root cause investigationWhat we know:
Two hypotheses:
What was committed today (42beb70)
What to try nextRun JPEC with the new scalar diagnostics to get: If If If If 50 nodes already fixes the scan (et values smooth throughout): it was a spline oscillation issue, no further work needed on the wv side. |
…t normalization
Three connected fixes for the edge dW scan and metric computation in diverted plasmas:
1. Fix findmax_dW_edge! scan ranking (Free.jl, EulerLagrange.jl):
- free_compute_wv_spline now uses psi_array[i] as plasma boundary (not psihigh).
Without this, wv(psihigh boundary) overestimates et for core steps relative to
edge steps, producing a spurious peak at q≈3 instead of the physical peak near q≈5.
- free_compute_total now normalizes by ffit.jmat * |ξ|² / dV_dψ(psifac), matching
free_run! exactly. Since dV_dpsi varies ~44% across the scan range, unnormalized
eigenvalues gave unfair advantage to core steps (smaller dV_dpsi → larger raw et).
- Scan limited to psihigh + 0.5*(1-psihigh) for diverted plasmas: above this cutoff,
singfac²=(m-nq)² grows unboundedly while wv is frozen at psihigh via ExtendExtrap.
- Tracks ep_edge_scan, ev_edge_scan, evonly_edge_scan for diagnostic output to HDF5.
- Verified: scan proxy (Total=+1.796 at ψ=0.943, q=4.198) matches actual free_run! output.
2. Limit Fourfit/make_matrix to psilim (Fourfit.jl, JPEC.jl, Sing.jl):
- make_metric now accepts psilim kwarg; truncates rzphi_xs grid at psilim.
Far-edge X-point geometry gives diverging FGK matrices; capping at psilim avoids
ill-conditioned splines that corrupt the core edge integration.
- make_matrix captures jmat at core_end_idx (last psi ≤ psihigh) instead of at the
far-edge last node. The X-point metric is near-singular near the separatrix.
- q and q' in make_matrix loop use iota inverse chain rule for psi > psihigh.
- sing_lim! sets psilim just before the last edge surface using singfac_min buffer
(replacing the previous edge_layer_width/2 offset that could trigger EL assertions).
3. Extend dense rzphi window past psilim (DirectEquilibrium.jl):
- direct_build_xpoint_asymptotic! extends the dense per-window grid one full rational
window beyond psilim so the spline covers the complete window psilim sits in.
Also: update DIIID-like example psihigh to 0.992 and add ep/ev/evonly diagnostic plots
to benchmark_edge_splines.jl (plots 12–14 showing plasma vs vacuum energy components).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
psilim for diverted plasmas is ~1.0 (near the separatrix), so the previous change to use rzphi_xs truncated at psilim inadvertently included all 77 far-edge nodes in the FGK matrix computation. Far-edge geometry near the X-point has J→∞, making the ODE extremely stiff above psihigh and causing very long integration times. Fix: compute min(psilim, psihigh) inside make_metric to always cap at psihigh. For limited plasmas (psilim < psihigh) this still correctly caps below psihigh. The jmat_at_core_end snapshot and q-via-iota-chain-rule branches are retained (correctly become no-ops for the psihigh-capped grid). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Session summary (2026-03-11)What was completed this sessionThree bugs fixed in the edge dW scan and metric computation, now pushed as commits 1.
|
…_spline_extension
…hmark scripts can start from the actual examples and make any modifications dynamically - the replicated tomls and gfiles were already getting out of sync!
- benchmarks/benchmark_edge_splines.jl: update module name (JPEC→GeneralizedPerturbedEquilibrium), default toml path, and h5 filename (jpec.h5→gpec.h5) - test/runtests_equil.jl: fix JPEC→GeneralizedPerturbedEquilibrium and wrong geqdsk filename (TKMKR_D3Dlike_default_Hmode→TkMkr_D3Dlike_Hmode) - Project.toml: remove PlotlyJS dep (only used in analyze_example.jl scripts, incompatible with JSON 1.x — users should install it separately) - examples/DIIID-like_ideal_example/gpec.toml: raise psiedge 0.800→0.970 (consistent with verified PR run) - src/Equilibrium/DirectEquilibrium.jl: update JPEC→GPEC in comment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Session summary (2026-03-11, afternoon)What was completed this sessionGPEC rename cleanup (commit After merging develop (which renamed the package from JPEC → GeneralizedPerturbedEquilibrium and output from jpec.h5 → gpec.h5), several files on this branch still had stale references:
Benchmark example dirs removed (commit
Benchmark script ran successfully
Remaining TODOs before merge
|
…mat_spline at local psi Removes the erroneous infrastructure that froze the Jacobian Fourier coefficients at psihigh for normalization in free_run! and free_compute_total. This caused SingularException for all ODE steps above psihigh because v†·J(psihigh)·v was indefinite for eigenvectors characteristic of the far-edge region. Changes: - ForceFreeStatesStructs.jl: remove ffit.jmat field from FourFitVars - Fourfit.jl make_matrix: remove core_end_idx, jmat_at_core_end snapshot, and ffit.jmat assignment; jmat_flat still built over full grid for spline - Free.jl free_run!: evaluate jmat_at_psilim = ffit.jmat_spline(psilim) at the actual plasma boundary instead of frozen snapshot - Free.jl free_compute_total: evaluate jmat_local = ffit.jmat_spline(psifac) at each scan step's psi; remove wrong comment block Also includes prior session changes: - make_metric: cap at psilim (not psihigh) so FGK splines use real above-psihigh geometry rather than ExtendExtrap frozen at psihigh - direct_build_xpoint_asymptotic!: grid ends at psilim (~0.994), removing sparse tail to 0.999999 (reduces far-edge nodes from 77 to 66) - EulerLagrange.jl: fix profiles -> equil.profiles in integrator_callback!; edge scan stores all steps psiedge->psilim with NaN for failures - GeneralizedPerturbedEquilibrium.jl: new top-level edge_scan/ HDF5 group with intuitive field names (total_energy, plasma_energy, etc.) - benchmarks: updated HDF5 keys; NaN-aware peak finding and subsampling - Project.toml: re-add PlotlyJS dependency Result: 346/6831 valid edge scan steps (0 before fix above psihigh); peak et=+3.056 at q=5.487 (psi=0.994). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…real vacuum computations The free_compute_wv_spline previously limited the vacuum response grid to [psiedge, psihigh], using ExtendExtrap to freeze wv at psihigh for all above-psihigh scan steps. This was incorrect for diverted plasmas where the plasma boundary can meaningfully exceed psihigh. Changes: - free_compute_wv_spline: extend grid to min(psi_scan_end, rzphi_xs[end]) for diverted plasmas. Above-psihigh grid points invert iota(psi)=1/q via Newton's method on the iota inner spline (same as make_matrix). Real VacuumInput computations at each psi use the X-point asymptotic geometry, giving physically correct wv for the above-psihigh scan range. - findmax_dW_edge!: retain scan_psi_max = psihigh + 0.5*(1-psihigh) to exclude ODE fixup artifacts at edge rational surfaces (q=6,7,...) where u grows exponentially before each Gaussian elimination, producing spuriously large eigenvalues. The rationale is now fixup noise exclusion, not ExtendExtrap wv limitation. Updated comment accordingly. Result: peak et = +3.064 at q=5.487 (was +3.056 with frozen wv), a 0.3% change reflecting the actual vacuum geometry at psi=0.994 vs psi=0.992. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uncation Replace global-maximum search in findmax_dW_edge! with a first-local-maximum algorithm: find the peak of et between the first ascending and first descending zero crossings. The physical instability peak (et≈3.06 at q≈5.5) lies in this window; artifacts from degraded ODE solution in the far above-psihigh region (et≈25 at q≈11) occur after et returns to negative, so the algorithm naturally excludes them without any hardcoded psi cap. Also add ucrit/10 above psihigh in compute_solution_norms! (via new is_above_psihigh parameter) for more frequent Gaussian fixups near the separatrix, reducing per-fixup artifact magnitude. Fix benchmark stability plot (Plot 7) x-axis: cap at qlim_bench (q at psilim) rather than q_max_scan which extended to q~30 with no physical content. Verified result: Peak et=+3.243 at ψ=0.994, q=5.488 (Plasma=+1.778, Vacuum=+1.465). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cation; fix evonly plot Remove the first-local-maximum logic from findmax_dW_edge! — it was based on an incorrect assumption about the scan structure. The physical picture shows repeated oscillations (stable → unstable → stable → ...) at each rational surface, so a simple zero-crossing criterion cannot reliably identify the "first" physical peak. Users control the scan range via edge_layer_width. Also fixes the evonly (vacuum_eigenvalue) diagnostic panel in Plot 7: - Clip values with |vacuum_eigenvalue| > 5 before plotting so the O(1) physically-relevant region is visible (previously the axis was dominated by spurious values of -2988 at psi > rzphi_xs[end]) - Title reports how many steps were clipped - Rename all "evonly" labels to "vacuum_eigenvalue" to match the h5 key Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ifacts Negative vacuum_eigenvalue values are unphysical (evonly >= 0 by construction) and come from wv computed via ExtendExtrap beyond rzphi_xs[end]. Filter to [0, clip_ev] instead of |val|<=clip_ev, and pin ylims from 0 to max+15% so the plot stays in the physically meaningful range. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Panel B (log-scale |ev|) was a debugging artifact — the vacuum energy contribution is already visible in the top panel (et/ep/ev components) and the wv matrix quality is shown in the bottom panel (vacuum_eigenvalue). Remove it, leaving a clean two-panel figure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… sing_find!
The edge rational surface scan was duplicated in direct_build_xpoint_asymptotic!
(with its own stopping criterion, push-then-check) and sing_find! (the canonical
source, check-before-push). These had gotten out of sync, causing rzphi_xs[end]
to differ from psilim by ~0.0006 in psi, leaving a gap where wv was computed via
ExtendExtrap with unphysical results.
Changes:
- direct_build_xpoint_asymptotic!: remove internal surface scan; accept
edge_surface_psis::AbstractVector{Float64} as required parameter (empty → no-op)
- setup_equilibrium: no longer calls direct_build_xpoint_asymptotic! (deferred)
- New equilibrium_extend_rzphi!(equil, edge_surface_psis): recovers node values
from existing bicubic splines via nodal_derivs.partials, calls
direct_build_xpoint_asymptotic! with sing_find! surfaces, rebuilds the four
rzphi splines in-place, updates equil.rzphi_xs
- GeneralizedPerturbedEquilibrium.jl: call equilibrium_extend_rzphi! after
sing_find!/sing_lim!, so the rzphi grid covers exactly [psilow, psilim] with
the same edge surfaces used for psilim — single canonical source, no gap
- free_compute_wv_spline: extrap=NoExtrap() — wv energy oscillates in each
rational window so no smooth extrapolation is valid beyond psilim
Confirmed psilim = 0.999785 for DIIID-like example (last sing_find! surface
before gap < edge_layer_width=1e-4; formats as 1.000 at 3dp).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation via eval_q - Remove `InverseCubicSpline` wrapper type entirely. Store raw iota spline as `profiles.iota_spline` (with `_iota_hint`) and raw 1/(dV/dψ) spline as `profiles.dVdpsi_inv_spline` (with `_dVdpsi_inv_hint`) on `ProfileSplines`. - Remove `q_spline` pointer field (was a Union for pointer-swap, now dead since the chunk-level spline switch was removed in prior commit). - Remove `ProfileSplines` type parameter `I` (no longer needed without the wrapper). - Add `eval_q(profiles, psi; deriv, output_lower_derivs, hint)` as the single unified interface for q evaluation at any psi. Dispatches to `iota_spline` above psihigh (with chain-rule derivatives via `deriv` keyword) or `q_spline_direct` in the core. `output_lower_derivs=true` returns all derivatives through `deriv` in a single pass, sharing iota evaluations. - Fix bug in `sing_find!`: finite-difference q' approximation (1e-6 step) replaced by analytic chain rule via `eval_q(profiles, psi; deriv=1)`. - Collapse all manual chain-rule implementations in Sing.jl, Fourfit.jl, Free.jl, EulerLagrange.jl into single `eval_q` calls. - Collapse `compute_sing_mmat!` pre-computation of `_d1/_d2/_d3_iota` functors into `eval_q(...; deriv=3, output_lower_derivs=true)`. - Update all `q_spline`/`q_spline_iota_inverse`/`dVdpsi_spline_inv` references across 13 files to use the new field names and `eval_q`. - Remove PlotlyJS from Project.toml (incompatible with JSON 1.x). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Now that the pointer-swap pattern is gone and eval_q owns edge dispatch, q_spline unambiguously refers to the direct cubic spline over [psilow, psihigh]. The _direct suffix was only needed to distinguish it from the Union pointer; removing it restores the natural name across all call sites. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Equilibrium: Edge inverse splines for diverted plasmas
InverseCubicSpline{S}type wrapping an iota = 1/q inner spline; callable returns 1/inner(ψ), enabling well-behaved evaluation of q and dV/dψ as psin → 1 in diverted plasmas (where q → ∞)ProfileSplinesmutable with aq_spline::Union{S,I}pointer field temporarily swapped to the edge inverse spline for ODE integration chunks that extend beyond psihighdirect_xpoint_findusing Newton iteration for Br=0, Bz=0 with physically motivated initial guesses on the inboard side; correctly classifies diverted vs limited topology (critical fix: initial R guess atrs1 + 0.35*(ro-rs1)rather than midpoint)qa = Inffor diverted plasmas;q_for_mode_rangefallback preventstrunc(Int, Inf)in mode range computationFree.jlroutes todVdpsi_spline_invwhen psilim > psihighEquilibriumParametersfields:r_xpoint,z_xpoint,is_divertedProfileSplinesfields:q_spline_direct,q_spline_iota_inverse,dVdpsi_spline_inv(all Nothing for limited plasmas → identical behavior to before)ForceFreeStates: Integration beyond psihigh for diverted plasmas
sing_find!above psihigh using the iota spline, stopping when surface spacing <edge_layer_width(new TOML parameter, default 1e-4)set_psilim_via_dmlimanddmlim— integration limit now determined entirely bypsiedge < psilimlogic;psilimfor diverted plasmas is set from the last edge rational surface foundsing_find!beforesing_lim!inJPEC.jlso edge surfaces above psihigh are available whensing_lim!reads themsing_lim!setspsilim = last_edge_surface + edge_layer_width/2for diverted, giving a valid final ODE chunk beyond the last crossed surfaceintr.mtheta→ctrl.mthvacbug infree_compute_wv_splinedW_edgeto match current step count infindmax_dW_edge!(integration can exceed initialnumsteps_initallocation)SingularExceptionin the edge dW scan — degenerate U₁ near the separatrix layer is skipped; those steps remain at -Inf and are ignored in the max searchtransform_u!capskfix = min(fixstep[ifix], odet.step)and breaks whenjfix > odet.stepto handle trimmed storage correctly after psiedge truncationBoundsErrorwhen extractingpsi_edge_scan:psi_storemay be doubled bygrow_storage!, so only the valid[1:odet.step]range is indexed before applying the edge maskDiagnostic output: et[1] vs psi in the edge zone
psi_edge_scanandet_edge_scanfields toOdeState; populated infindmax_dW_edge!from the full un-trimmed psi range beforetrim_storage!cuts to the peak-dW stepintegration/psi_edge_scanandintegration/et_edge_scanfor post-run diagnosticsBenchmark script:
benchmarks/benchmark_edge_splines.jlProduces 7 diagnostic plots saved alongside
jpec.toml:integration/psi_edge_scanfromjpec.h5, marks the peak dW truncation pointBackward compatibility
nothing,q_spline = q_spline_directat startup, behavior unchangedq_spline_iota_inverseanddVdpsi_spline_invonly activate when diverted x-point is detectedEnd-to-end verification (DIIID-like example)
Test plan
runtests_equil.jlpasses (includes new testsets for edge splines on DIIID-like EFIT and limited plasma regression)runtests_solovev.jlpassespe.params.is_diverted == true, X-point found at R=1.315, Z=-1.114|q_direct(psihigh) - q_edge(psihigh)| < 1e-14, same for dV/dψiota(1.0) ≈ 0.0(anchor condition)q_spline_iota_inverse === nothing, behavior unchangedbenchmarks/benchmark_edge_splines.jlafter a DIIID-like JPEC run to verify all 7 plots🤖 Generated with Claude Code