Repository navigation
KineticForces - FEATURE! - Suppress kinetic terms inside the near-axis validity limit and report validity profiles - #414
Conversation
…h reltol integrate_ballooning_ode set abstol=reltol^2 (1e-16, at machine epsilon) against a non-stiff DP5 solver, forcing needlessly tight step control. Profiling (warmed, no JIT noise) showed ballooning boundary search as the dominant real cost on gal-resistive-PE-style cases -- ~55% of pipeline runtime, driven by many repeated ODE shoots per flux surface (linear pre-scan + bisection to locate the marginal-stability crossing). Setting abstol=reltol=1e-8 (the standard pairing) gives a measured ~8% speedup on Force-Free States for DIIID-like_gal_resistive_pe_example, confirming reltol was already the binding constraint for most of the integration. Validated bit-for-bit identical locstab/alpha_critical (the value that actually drives stability classification) before/after; the raw ballooning_Delta_prime diagnostic drifts ~1e-5 relative, which flips a few profile checksums in the regression harness but is far below any threshold that matters physically. Full 11-case regression suite against merge-base a0cad26 is otherwise clean.
Pulls in 171 develop commits, including the ForceFreeStates -> standalone LocalStability module extraction (Ballooning.jl moved to src/LocalStability/Ballooning.jl). Clean merge, no conflicts; Git's rename detection correctly carried this branch's ballooning abstol fix (69ad76e) over to the new path. Regression harness (develop @ 9491f89 vs merged local, 13 cases, full suite): clean across the board. 12/13 cases fully unchanged (energies, ODE step counts, equilibrium/stability quantities, all profile checksums identical). The only diff anywhere is the "ballooning Delta' profile" checksum in diiid_n1 (1 changed / 46 unchanged) -- the expected, already-validated fingerprint of this branch's own abstol=reltol fix (~1e-5 relative drift in the raw diagnostic; locstab/alpha_critical, the value that actually drives stability classification, is bit-for-bit identical). No numeric regressions introduced by the merge itself.
…ordering fails near the axis Physics ruling (issue #376 DIII-D kinetic pathology): the drift-kinetic model loses validity where thermal ion orbit widths reach the local minor radius. psi_c = outermost crossing of <r> by max(potato width (q^2 rho^2 R0)^(1/3), banana width q rho/sqrt(eps), poloidal gyroradius q rho/eps), computed from the equilibrium and kinetic profiles at runtime -- no user tuning parameters (the Fortran ktanh_flag precedent needed four). A C2 quintic envelope zeroes the calculated kinetic increments below psi_c (kernel evaluation skipped) and rises to 1 at 2 psi_c; the same boundary and envelope apply to the NTV torque psi quadrature (one source of truth). One Bool (axis_validity_suppression, default true) to disable for debugging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
…icForces/Validity Whenever kinetic profiles are used (self-consistent matrices or NTV post-processing), write the thermal orbit-width scales (rho_i, rho_banana, rho_theta, w_potato), the local geometry (r_minor, d_separatrix), the profile gradient lengths (L_p, L_q), the near-axis boundary psi_c with its applied envelope, and an is_valid array (orbit width < r, rho_banana < L_p and L_q, orbit width < distance to separatrix). Validity outside the near-axis envelope is flagged, never suppressed -- the far edge can dominate the physical NTV. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
…n-or-create the KineticForces group The envelope has structure on the psi_c scale; coarse kinetic decks (m16) cannot represent env*(increment) and the spline overshoot can land on a rational surface inside the transition band, corrupting the eigenvalues. Augment the kernel grid with knots across [psi_c, 2 psi_c] (band ends pinned -- the smoothstep is only C2 there) on the full-grid path and seed them on the certified path. Also open-or-create KineticForces in the NTV writer, which collided with the Validity group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
|
Reviewer package (figures: validity-scale crossings defining ψ_c, before/after step distribution, flow diagram, verification record): https://claude.ai/code/artifact/09745676-ff9b-49c6-a14b-74f093427d00 — flip to shared for reviewers as with #398/#408. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
…ata contract) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
… auto psi grid The two-pass auto grid's criterion is ideal-driven and knows nothing about kinetic resonance locations. When a run builds calculated kinetic matrices, locate the Omega_l = 0 surfaces (same locator as the NTV quadrature paneling) and insert them as plain knots via merge_mandatory_nodes -- knot-at-node, no cleared zone, inserted before rational bracketing so the Delta-prime clean-interval treatment wins locally. Nodes inside the near-axis validity region are suppressed anyway and not pinned. DIII-D: +2 net knots, et[1] unchanged to 2e-6, EL steps drop 14%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
…nsistent kinetic runs reg_spot smooths the ideal 1/(m-nq) divergence of the displacements before they drive the NTV integrand. The self-consistent kinetic Euler-Lagrange operator has no such divergence -- det(F-bar) is complex and nonzero at the rationals (Park & Logan, Phys. Plasmas 24, 032505 (2017) Eq. 70) -- so regularizing there suppresses a finite physical response, and inconsistently: xi^psi is never regularized, so damping the other two breaks their near-resonance cancellation in dB/B. Measured (DIII-D-like, n=1) against the EL solution's own dissipation: NTV torque 0.1655 vs 0.1322 N*m with reg_spot=0.05, and 0.1324 vs 0.1322 (0.15%) with it off. Kinetic runs now force reg_spot=0 and log the override; ideal runs are unchanged, where without it the displacement and torque diverge by four orders of magnitude. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
|
Review package updated with the regularization analysis (same link): https://claude.ai/code/artifact/09745676-ff9b-49c6-a14b-74f093427d00 — adds the operator σ_min evidence, the ideal-vs-kinetic ξ^α overplot, and the torque table. Verified on the shipped DIII-D deck (which sets |
…structure The cond(F-bar) scan that locates kinetic singular surfaces already sweeps 2000 points and is written to SingularSurfaces/Kinetic/scan_cond, but only peaks above the 1e8 singular threshold were surfaced. On a DIII-D-like case the strongest peaks sit at 3e5-5e6 -- real shifted/split resonance structure (Park & Logan Eq. 70) that stayed silent, and which tracks the NTV torque-density peaks at low collisionality/rotation. Report the strongest few. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
… F-bar structure The cond(F-bar) scan already locates shifted/split kinetic resonances (Park & Logan Eq. 70) but only reported them. Measure each sub-threshold peak's FWHM and, where the grid puts fewer than three knots inside it, evaluate the kernel at a few targeted psi and splice them in -- existing values are reused, so the cost is one kernel call per added knot, not a re-formation. Respects MIN_KNOT_SPACING, the near-axis validity band, and a 24-knot cap; a resolved grid inserts nothing. Measured on DIII-D: no insertions on the nominal or low-collisionality cases; on the collisionless slow-rotation case two peaks (psi=0.53, 0.51, FWHM 1.5e-3 and 2.5e-3) had zero knots inside them, and one of the two coincides with a top NTV torque-density peak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
530ade4 to
b3d7ab9
Compare
…ts the key The override tested get(inputs,"reg_spot",0.0)!=0, so it only fired when a deck set reg_spot explicitly; a deck relying on the struct default (0.05) silently kept regularization on in a self-consistent kinetic run -- exactly the case the change exists to prevent. Compare against the struct default and always force 0, logging whenever the prior effective value was nonzero. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
…b-layer matching The SLAYER dispersion relation paired a ψ_N-referenced BVP Δ' with an r_s-referenced layer side (Δ(Q), S=τ_R/τ_H on r_s, and the critical-Δ, whose Ŵ_d is W/r_s). Apply the Frobenius reference-length transform Δ̂_ij = K_i^(1/2+μ_i)·Δ'_ij·K_j^(μ_j−1/2), K = r_s·(dψ_N/dr)|_s, μ = √(−D_I), at the matching point. Verified parameter-free against the TJ circular benchmarks (median residual +1% over 23 points; absolute 2/1 agreement 17.5%→2.5% (β) and 17.6%→1.1% (ε)). BVP Δ' outputs are unchanged; only the SLAYER matching (γ, Δ_eff) moves. GGJ untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…el options to build_slayer_inputs The cylindrical layer model needs a single minor radius; on a shaped torus that label is ambiguous. Add rs_method options :halfwidth (midplane half-chord, the shift-free stand-in for the circular-theory flux label; reproduces it to ~1% on circular benchmarks) and :volume (cylinder-equivalent √(V/2π²R₀), the Rutherford-literature convention) alongside the existing :midplane default and :fsa. Every label feeds S, the r-based shear, W_d, and k_ref together, so each choice is self-consistent by construction. Programmatic API only — not exposed via TOML; the default and all TOML-driven results are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he HDF5 annotation The PerSurface/D_norm long_name still gave the retired √(τ/(1+τ)) factor, so every new gpec.h5 labelled the corrected values with the old formula. Match the struct docstring: D = (d_β/r_s)·S^(1/3)·√ι_e. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…slayer_parameters Neither ArgumentError guard on the diamagnetic frequencies was exercised. Assert the degenerate case (iota_e singular), same-sign drifts with the ion drift dominant (iota_e < 0), and zero electron drift (iota_e = 0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… share, not the temperature ratio (#434)
…undir Placing the override rundir beside its example fixed relative file references, but the name became fixed per example, so two harness runs from the same checkout on an override case would delete and rewrite each other's deck and output. Create it with mktempdir under the example's parent instead: still a sibling, so relative references resolve, but unique per invocation, as the override rundir, worktrees and subprocess files already were. Only concurrent local-ref runs could collide; git-ref comparisons each run in their own temporary worktree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uadrature type instability (#380)
…diamagnetic frequencies (#433)
…ourier transform (#458)
…e states one The current is optional in IMAS and reading an absent one throws rather than returning missing, so checking that the field exists was not enough: every equilibrium without a stated current failed to load. It now reads through the default-returning accessor, leaving the helicity positive when the file is silent, as the g-file path already does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
…rium file's current sign (#457)
…heta-parametrization' into feature/kinetic-axis-validity
…ional clearing Independent review of #414 found the near-axis suppression boundary computed three different ways, and two ways it can run away. One boundary per run. The NTV psi torque quadrature recomputed psi_c from the single species it was integrating and never called clear_rational_windows, while the EL kinetic matrices and the Validity output used the widest-orbit species' cleared boundary. Validity/psi_c in gpec.h5 therefore described a different domain from the one the torque was integrated over, and the standalone torque kept near-axis contributions the EL build had already zeroed - which is exactly the cross-check the PE-vs-NTV agreement rests on. axis_validity_boundary is now the only entry point: run_kinetic_forces computes it once and threads it through compute_torque_all_methods! into integrate_psi_quadgk. Contiguity. kinetic_axis_validity_psi took the outermost trigger anywhere on the profile grid. q is not monotonic, so an edge q rise or a pedestal gradient could re-trigger the criterion far from the axis and drag the suppressed band across a large fraction of the plasma. The scan now stops at the first surface where the ordering holds and warns if the criterion fires again further out. Cascade. clear_rational_windows rescans [psi_c, 2 psi_c] after each move, so the window widens as psi_c grows; with rationals at 0.15/0.25/0.45 a psi_c of 0.10 walked out to 0.455. The move is capped at VALIDITY_CLEAR_MAX_FACTOR times the orbit-width boundary, past which the uncleared boundary is kept with a warning - a band cutting a rational is resolved by the band knots, whereas suppressing half the plasma is not recoverable. Grid pinning. resonance_grid_nodes cleared its boundary against intr.sing, which sing_find! does not populate until long after the grid is built, so the clearing was silently a no-op. The nodes are now located inside maybe_reform_equilibrium against that pass's own rational list. Validity coverage. write_validity! was reachable only from run_kinetic_forces, whose body is gated on a perturbed-equilibrium state, so a calculated-kinetic FFS-only run wrote no Validity group at all. It is written from the ForceFreeStates output stage instead, wherever kinetic profiles were loaded. kinetic_regularization_kwargs is duck-typed on ffs so the override is testable, and its message no longer says "self-consistent" - it covers both kinetic sources. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
…larization override The validity machinery shipped with no unit coverage. Adds runtests_kinetic_validity.jl (registered in runtests.jl) over the pieces the review found defects in: - kinetic_axis_validity_envelope: endpoint values, the midpoint, monotonicity, and a C2 check that the second difference straddling each band end vanishes linearly in h - clear_rational_windows: empty and far-rational no-ops, a single in-band rational, and the cascade construction that must hit the cap and keep the orbit-width boundary - kinetic_axis_validity_psi: a q spike far from the axis must not move the boundary - kinetic_regularization_kwargs: ideal pass-through, kinetic forcing 0, and the case where the deck omits reg_spot entirely and must pick up the struct default The equilibrium is a duck-typed stand-in - the boundary scan reads only q, <r>, <R>, ro and b0 - so the tests run in seconds without building a PlasmaEquilibrium. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
Review feedback on #414: the block comments are too long. Collapses the pinned-knot block in refined_psi_grid, the BAND_KNOTS and validity-envelope blocks in Kinetic.jl, and the sub-threshold reporting block in Finding.jl to one or two lines each, and rewraps the refined_psi_grid docstring line that had grown past the 180-char margin. Also folds find_kinetic_singular_surfaces!'s numbered "Algorithm:" list into prose, per the repo's no-step-numbering rule. docs/src/kinetic_forces.md keeps the regularization argument and its citations - it is user-facing reference material, not a comment - but drops the single-case DIII-D validation table, which belongs in the review package rather than in permanent docs where it goes stale. Adds the Logan (2015) Ch. 7 Eq. 7.46 reference alongside Park & Logan (2017), since it states the same result in GPEC's own notation. The three example decks get a shorter inline description for axis_validity_suppression, matching the surrounding comment column. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
Review feedback on #414 flagged this block as too long. It is not actually this branch's code - it arrived on develop with the nested energy/pitch tolerances and shows in the pull request diff only because the merge base makes develop's commits look like additions - but the comment rule applies either way, so it is trimmed here rather than deferred: 27 comment lines down to 14, one line per field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
KineticForces/Validity carried no long_name/units at all. Its metadata table sat in HDF5Schema.MAIN_H5_ANNOTATIONS, which apply_main_h5_metadata! applies at the end of write_outputs_to_HDF5 - but write_validity! runs afterwards, in a separate h5open, so the pass never saw those datasets and the entries were dead. HDF5Schema's own header states the rule: sub-writers keep their tables next to their own writers. The table moves to Output.jl as KF_VALIDITY_H5_ANNOTATIONS and write_validity! annotates the group it just created, matching how write_to_hdf5! handles the per-method groups. The entries also gain the `dims` attribute the contract requires of rank >= 1 profiles. Corrects the rationale given in 2dcbbf1 for the cascade cap. That commit argued a transition band cutting a rational is harmless because the band knots resolve it; the history says otherwise - the band knots landed in 3f1dbfe and the NaN that motivated clear_rational_windows was found after them, in 37c4eeb. The capped branch does reinstate the hazard, so the warning now says so and names the two ways out (axis_validity_suppression = false, or a finer grid there) instead of implying the knots cover it. The behaviour is unchanged: swallowing half the plasma is the worse failure. The contiguity warning gains maxlog=1 - it is reachable from four call sites, once per species - and its unit test now asserts the boundary rather than the log line, since maxlog makes the log order-dependent across the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
Review round pushed (
|
e8b5791
into
performance/consistent-surface-theta-parametrization
Stacked on #408 (
performance/decoupled-el-matrix-grid). Physics ruling from the issue #376 DIII-D kinetic investigation: do not chase drift-kinetic physics where the model has lost validity near the axis.What
Near-axis kinetic validity suppression (
axis_validity_suppression, default on; a singleBool— no tuning parameters). The zero-orbit-width drift-kinetic ordering fails where thermal ion orbit widths reach the local minor radius. The boundary is computed from the equilibrium + kinetic profiles at runtime: ψ_c = outermost ψ where max( potato width (q²ρ²R₀)^⅓, banana width qρ/√ε, poloidal gyroradius qρ/ε ) ≥ ⟨r⟩ (all coefficient 1). A C² quintic envelope zeroes the calculated kinetic increments below ψ_c (kernel evaluation is skipped there), rising to 1 at 2ψ_c; the kernel grid is augmented with knots across the band so coarse decks resolve the envelope. The same boundary and envelope apply to the NTV ψ torque quadrature — one source of truth. (This was claimed but not true until the review round: the quadrature recomputed ψ_c per species and skipped the rational clearing. See Review round below.)KineticForces/Validity/output group, written whenever kinetic profiles are used (from the ForceFreeStates output stage — it was previously gated behind an NTV stage that calculated-kinetic runs never reach, so FFS-only runs got no group at all):rho_i,rho_banana,rho_theta,w_potato,r_minor,L_p,L_q,d_separatrix,psi_c,envelope,is_valid(true where max orbit width < ⟨r⟩, ρ_banana < L_p and L_q, and max orbit width < distance to separatrix). The far edge and steep-gradient regions are flagged, never suppressed — they can dominate the physical NTV; suppression is reserved for the region where the model both fails and poisons the numerics.Resonance-aware auto grid (was EQUIL - NEW FEATURE - Pin located kinetic-resonance surfaces into the auto psi grid #422, combined here): the two-pass auto grid's criterion is ideal-driven and knows nothing about kinetic resonance locations, so when a run builds calculated kinetic matrices the located Ω_ℓ = 0 surfaces (same
kinetic_resonance_psi_nodeslocator the NTV quadrature panels at) are pinned into the grid as plain knots viamerge_mandatory_nodes— knot-at-node, no cleared zone, inserted before rational bracketing so the Δ′ clean-interval treatment wins where a resonance sits inside a rational's bracket. Nodes below ψ_c are suppressed anyway and not pinned. DIII-D: 6 nodes located, +2 net knots, et[1] unchanged to 2e-6, EL steps −14% (the ψ=0.17 resonance was previously unknown to the grid). Stress-tested withtoroidal_rotation_factor = 0.2(parks resonances at ψ = 0.578, 0.812) against a 1024-knot gold: naive auto grid already agrees to 1.24e-3 and pinning reproduces it to 1e-6 — cheap insurance plus the step win, and the same margin held collisionless (nufac = 0.02).Regularization off in self-consistent kinetic runs.
reg_spotsmooths the ideal 1/(m−nq) divergence of ξ^ψ′ and ξ^α before they drive the NTV integrand. The self-consistent kinetic Euler–Lagrange operator has no such divergence — Park & Logan (Phys. Plasmas 24, 032505 (2017) §III D) decompose F_k = Q F̄_k Q − P_l†Q − Q P_u + R₁ with R₁ ≠ 0 at Q = 0, and with finite torque det F̄ goes complex, removing the singularity from the solution and the torque integral. Kinetic runs now forcereg_spot = 0and log the override; ideal runs are untouched.Verified in this run's own operator: σ_min(F̄) at the rational is 2.6e-17 ideal vs 4.2e-3 kinetic (never below 3.3e-3 across the window) — which is also why
ksing_findcorrectly reports no kinetic singular surfaces. And in the displacements: ideal unregularized |ξ^α| peaks at 643.8 while kinetic unregularized peaks at 0.059, indistinguishable from the regularized kinetic value (0.055).The two independent torque calculations agree to 0.15% with it off and 20% with it on. Full derivation, equations and figures in the review package; a docs section landed in
docs/src/kinetic_forces.md.Near-singular F̄ guards. The
cond(F̄)scan that locates kinetic singular surfaces (Park & Logan Eq. 70 assembly, so shifted/split resonances are included) already swept 2000 points intoSingularSurfaces/Kinetic/scan_cond, but only reported peaks above the 1e8 singular threshold. Sub-threshold peaks are now reported, and where a peak's FWHM contains fewer than three matrix knots the kernel is evaluated at a few targeted ψ and spliced in — existing values reused, so one kernel call per added knot rather than a re-formation — respectingMIN_KNOT_SPACING, the validity band, and a 24-knot cap.One of the two unresolved peaks coincides with a top NTV torque-density peak. A resolved grid inserts nothing, so this is invisible on healthy cases by construction.
Why (measured, DIII-D kinetic-calculated on the production auto grid)
The kinetic increments diverge toward the axis (~ψ^−0.7; rtol-invariant, identical on develop and the stack — real model output, evaluated outside its validity domain, no clamp anywhere in the kernel), and on the stack 94% of 223,271 EL integration steps land at ψ < 0.01 while contributing nothing physical:
Honest framing (bisected): the 223k-step explosion is a stack regression introduced by #398 (that branch alone reproduces it bit-for-bit; develop does not explode — see #398's disclosure). This PR cures it on physics grounds and finishes below the develop baseline on every axis: fewer steps (4,246 vs 7,972), less than half the kernel evaluations (~258 vs 554 — develop spends 45 of its evals below ψ_c on invalid physics), and et[1] within 1.1e-3 of develop / 2.5e-4 of the stack's converged unsuppressed reference.
A 53× step collapse (also eliminating multi-GB solution dumps) for a 2.5e-4 eigenvalue change, measured against a doubly-converged reference (kernel tolerance ×10: Δ 2.7e-7; denser equilibrium grid: Δ 1.6e-4) and cross-checked against develop (Δ 1.1e-3).
Fortran precedent
ktanh_flag(dcon/fourfit.F:1117) suppressed the same region with four hand-tuned user knobs (ktc,ktw,kinfac1/2) and no physics setting the location. Adopted as supporting evidence for the decision; rejected as a design — here the boundary is profile-derived with zero user parameters.Runtimes
The
cond(F̄)scan sits below the run-to-run noise floor (1000 evaluations of four spline lookups plus a 35×35 condition number); each inserted knot costs ~0.5 s at 28 threads, so the 24-knot cap bounds the worst case near +12 s on a ~275 s run.Regression harness — and an honest coverage gap
regress --cases diiid_n1,solovev_kinetic_calculated,solovev_kinetic_ntv,solovev_kinetic_nuzero --refs performance/decoupled-el-matrix-grid,local:reg_spotentirely reproduces the same numbers — verifying the override picks up the struct default, not only explicit settings.The gap, stated plainly: no current harness case exercises the regularization change.
solovev_kinetic_ntvrunskinetic_factor = 0(not self-consistent), andSolovev_kinetic_calculated_examplehas no[PerturbedEquilibrium]section at all — its PE stage runs in 0.000 s. The only coverage is the DIII-D scratch runs above. #407 (unmerged) adds[ForcingTerms]/[PerturbedEquilibrium]to that example and would give this change real harness coverage; reviewers of the two together should note the interaction. A DIII-D kinetic-calculated FFS-only harness case remains the outstanding test-infrastructure item (also flagged in #398 and #423).Rebased onto develop (349a0c2) — and what the port changed
Merged develop on 2026-09-04, adopting its structures rather than working around them. Three of
this branch's changes became deletions, because develop had solved the same problems
independently:
_compute_fkg_matricesis now idempotent (the ideal-restore workaround is gone),KineticForces/Output.jlalready opens-or-creates its group, and the:statickinetic-threadingfix landed in
9fe8a60c9. What remains is ported ontoMatrixSplines, with the arbitrary-ψ kernelargument type-stable (
psis::Vector, empty meaningmetric.xs) and the orchestration layer gainingtwo lines — the logic lives in
KineticForces.resonance_grid_nodes/axis_validity_boundary/write_validity!andPerturbedEquilibrium.kinetic_regularization_kwargs.Multi-ion ψ_c (recorded for the historical record)
develop's multi-ion NTV means ψ_c is now the widest-orbit species' boundary. Measured per
species on the DIII-D-like H-mode equilibrium:
Because ρ ∝ √(mT)/(Z·e·B₀), impurity orbits are narrower than the main ion's — carbon's ψ_c is
4× smaller — so adding impurities cannot widen the suppressed region on this case; the main ion
sets it. A species with larger √m/Z does move it: the Solovev D-T deck's tritium pushes ψ_c from
0.10 to 0.12, which is what exposed the bug below.
Bug found and fixed during the port
With tritium setting ψ_c = 0.12, the envelope's transition band landed on that equilibrium's
rational surface at ψ = 0.1221. Multiplying near-singular kinetic increments by a rapidly varying,
near-zero envelope is not representable by the matrix splines, and the overshoot propagated NaNs
into the stability solve —
solovev_kinetic_calculatedfailed while develop passed. The boundary isnow moved clear of any rational whose window the band would cut through (ψ_c → 0.127 there), on the
physical grounds that where orbit widths already reach ⟨r⟩ the resonance is not trustworthy anyway,
so suppressing it wholly is more honest than half-suppressing it. The boundary is computed once
per run and threaded to the kernel, the band knots and the
Validityoutput so the three cannotdisagree.
Verification after the port
runtests_kinetic277/277,runtests_multiion52/52,runtests_sing76/76,runtests_h5_schema22/22.solovev_kinetic_calculatedRe(et[1]) 0.94%,Im(et[1]) 6.7%, steps 737 → 792;
solovev_kinetic_ntvand_nuzerosimilar magnitude. These arethe deliberate physics of the suppression and the regularization change.
(0.2%); suppression off 222,489 steps; collisionless + slow rotation 3031 steps — all
reproducing the pre-port values.
Release note
reg_spotis forced to 0 in kinetic runs (harness @ 2abea51)axis_validity_suppression = falsein[KineticForces]to recover the previous behaviour for debuggingCalculated-kinetic runs no longer evaluate the drift-kinetic model where its zero-orbit-width
ordering has failed: the code computes the validity boundary from the profiles themselves and
smoothly suppresses the kinetic terms inside it, which both removes unphysical near-axis structure
and cuts the Euler–Lagrange work that chasing it cost. Every run that uses KineticForces now writes
an orbit-width and gradient-length
Validityprofile so the trustworthy region is visible ratherthan assumed, and self-consistent kinetic runs no longer regularize the displacement — the kinetic
singularity is shifted and split, not divergent, so the ideal-only regularization was inconsistent.
Review round (2026-09-19) — what changed since
@ebursch's reviewReconciled onto the base branch and
develop(#403), then addressed the review plus anindependent pass (
fortran-physics-reviewer,clean-code-reviewer, and a manual read).Reviewer's three comments — all addressed.
GridRefinement.jl's pinned-knot block and thekinetic_forces.mdregularization section are trimmed. The third site(
KineticForcesStructs.jltolerances) turned out not to be this branch's code — it arrived ondevelopwith #392 and only appears in this diff because the merge base makes develop's commitslook like additions. Trimmed anyway.
Six substantive defects the review did not reach (
2dcbbf170,2abea51d2):Validity/psi_cingpec.h5described a different domain than the torque was integrated over; the standalone torque kept near-axis contributions the EL build had already zeroed — the exact cross-check the PE-vs-NTV agreement rests onclear_rational_windowscascaded unboundedq(edge rise, reversed shear) re-triggers the criterion far from the axis and drags the band outwardValiditynever written on FFS-only kinetic runsValiditydatasets carried nolong_name/unitsMAIN_H5_ANNOTATIONS, applied at the end ofwrite_outputs_to_HDF5;write_validity!runs after that, so the entries were deadFixes:
axis_validity_boundaryis now the only entry point, computed once per run and threadedinto the quadrature; the clearing is capped at
VALIDITY_CLEAR_MAX_FACTOR× the orbit-widthboundary with a warning naming the two ways out; the scan stops at the first valid surface;
write_validity!moved to the ForceFreeStates output stage and annotates its own group (matchingHDF5Schema's stated rule that sub-writers keep their tables next to their writers); and
test/runtests_kinetic_validity.jlcovers the envelope's C² join, the cascade cap, the contiguityguard and the
reg_spotoverride (27 tests).Not changed: scoping
reg_spot = 0tokinetic_source == "calculated"was proposed andrejected — the fixed kinetic terms also remove the resonant singularity, so both sources
correctly force it to 0.
Regression report
regress --cases diiid_n1,solovev_n1,solovev_kinetic_calculated,solovev_kinetic_ntv,solovev_kinetic_nuzero,solovev_kinetic_multiion --refs develop,<pre-fix>,<branch>,baseline develop @ f36ecef, branch at 2abea51 (HEAD). Run with three refs so this
round's fixes are separable from the PR as a whole;
<pre-fix>iscf7a40cc4, the reconciled treebefore the review fixes. Deltas vs develop still bundle this PR with its base #398.
The three-ref run was taken at
0f8dd766f; it was then repeated at HEAD (2abea51d2, the HDF5metadata fix) against develop, and every tracked value is identical — that commit changes
attribute strings and a warning message only, with no numerical path.
This round's fixes moved only the NTV quadrature domain, and nothing else:
That the two ideal cases are bit-identical confirms the grid-pinning rework did not leak into
ideal runs. The electron moving most is the clearest evidence defect #1 was real: the electron's
own ψ_c is ~0, so it previously integrated from
psilowwhile the D/T kinetic matrices hadalready been zeroed below their boundary. It now starts where the widest-orbit species says the
model is valid.
Against develop (bundling #398):
Attribution — almost all of the Solovev kinetic movement belongs to the base, not to this PR.
Running #398 alone against the same develop gives
calculatedRe 1.829090 / Im −1.322557 andnuzeroRe 2.290779 / Im −1.557856; adding this PR moves them to Re 1.828561 / Im −1.325305 andRe 2.290643 / Im −1.561707. So this PR's own increment on Solovev is 0.03% / 0.21% (calculated)
and 0.006% / 0.25% (nuzero). That the increment is this small is the expected result, not a
disappointment: Solovev's ψ_c is small and its et[1] barely sees the suppressed region, matching
the 2.5e-4 et[1] change measured on DIII-D. The physics this PR is really being judged on — the
step reduction and the PE-vs-NTV torque agreement — lives on the DIII-D decks in the review
package, which the harness cannot yet reach (see the caveat below).
Unit tests at this state:
runtests_kinetic277/277,runtests_kinetic_validity27/27(new),
runtests_kinetic_profiles9/9,runtests_multiion52/52,runtests_sing76/76,runtests_grid_refinement59/59,runtests_h5_schema16/16 + 6/6,runtests_fullruns19/19.Caveat on coverage: the Solovev calculated example has no
[PerturbedEquilibrium]section andsolovev_kinetic_ntvruns atkinetic_factor = 0, so the harness still cannot exercise theregularization change; #407 adds the missing sections and would give it real coverage. The
DIII-D evidence for it is in the review package, measured before #391/#392 and before this round's
fixes. The boundary change only alters multi-species or rational-adjacent runs (see the regression
table), so the single-species DIII-D headline is not expected to move.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk