Uh oh!
There was an error while loading. Please reload this page.
KineticForces - FEATURE! - Suppress kinetic terms inside the near-axis validity limit and report validity profiles - #414
Conversation
The bounce-point spline samples 1-(lmda/bo)*B(theta), which is periodic on the closed poloidal interval, but was fitted with non-periodic endpoint conditions while the adjacent equilibrium B spline (tspl) was already periodic. The resulting fit is only C0 at the theta=0/1 seam, so it can manufacture false near-seam extrema and bounce-root pairs. On a synthetic shaped field the seam derivative mismatch drops ~5x (1.4e-4 -> 2.6e-5) when the fit is made periodic. Fixed at both construction sites (the tpsi! quadrature path and the _setup_surface_state kinetic-matrix path). Renamed B_extrap -> B_vpar and _vpar_from_extrap -> _vpar_from_spline, since after this change the old names describe a fit that no longer exists. Julia analog of Fortran GPEC PR #284, which fixed the same spline_fit(vspl, "extrap") -> "periodic" defect in pentrc/torque.F90 after it produced non-finite omega_b/omega_D and an LSODE failure on an ITER case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ga_D prefactor The general-aspect-ratio precession prefactor carried ro^2 where it should carry ro. wbbar = ro*2pi/((2-sigma)*I1) already contains one factor of ro that its own normalization bhat = sqrt(2T/m)/ro cancels; reusing wbbar inside wdbar imports that ro a third time, while dhat = (T/q)/(bo*ro^2) removes only the two written explicitly. Dimensionally, both bounce integrals carry the J*b*dtheta = dl length, so I1 is a length and I2/I1 is 1/Wb. With T/q in volts and V/Wb = 1/s, the prefactor 4*pi*wdfac*(I2/I1)*(T/q) is already a frequency, and the surviving ro left omega_D in m/s. Verified by holding the physics fixed and varying only the machine size: the old form scales as ro (ratio 2.0 when ro is doubled), the corrected form is ro-invariant (ratio 1.0). omega_b, built from the same I1, is untouched and keeps its correct v_th/L scaling. Julia analog of Fortran GPEC PR #281, which measured the same correction against an independently validated guiding-centre precession operator: the least-squares slope of omega_D against the reference moves from -6.49 (= -ro on that ITER equilibrium) to -1.02, with omega_b agreeing to within [0.9964, 1.0011] both before and after as the control. This changes omega_D by a factor of ro on every machine, so it moves the resonance denominator and all NTV torque. Only the magnitude is addressed here; the overall sign convention is a separate question and is not touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erances The energy (x) integration was handed the same atol_xlmda/rtol_xlmda pair as the pitch (lambda) integration that encloses it. Since the pitch integrand IS the energy integral, the outer integrator was asked to resolve its integrand to the same tolerance to which that integrand was itself computed, so it chases the inner integrator's quadrature noise instead of converging. Adds atol_x/rtol_x, defaulting (NaN sentinel) to nested_tolerance_margin = 1e-2 times the pitch tolerances, which extends the nesting rule the struct already documents one level up for rtol_psi vs rtol_xlmda. Wired through both the psi-quadrature path (tpsi!) and the kinetic-matrix path. This deliberately changes default numerical behaviour: shipped decks now integrate the energy variable to atol 1e-10 / rtol 1e-7 rather than 1e-8 / 1e-5, and pay for it in runtime. A deck can set atol_x/rtol_x explicitly to override the derived values, or widen nested_tolerance_margin to recover the old cost. Julia analog of the second commit of Fortran GPEC PR #280. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ergy-pitch-tolerances
# Conflicts: # src/KineticForces/CalculatedKineticMatrices.jl # src/KineticForces/Compute.jl
…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
logan-nc
commented
Aug 20, 2026
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
logan-nc
commented
Aug 22, 2026
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
b3d7ab9Compare…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
2225813 to
0c17097Comparelogan-nc
commented
Sep 4, 2026
Note for anyone benchmarking from this branch before it is reconciled with develop: this stack forked at |
Port the kinetic guards onto develop's structures rather than fighting the reorg:
- MatrixSplines replaces FourFitVars: kinetic matrices read mats.ideal and return
a fresh MatrixSplines, so the ideal-restore workaround is gone (develop's
_compute_fkg_matrices is idempotent by construction).
- Arbitrary-psi kernel evaluation returns as a type-stable psis::Vector{Float64}
(empty = metric.xs), no Union sentinel.
- Multi-ion: the near-axis boundary is now the widest-orbit species' psi_c.
- Sub-threshold cond(F-bar) reporting follows find_kinetic_singular_surfaces!
into Surfaces/Finding.jl.
- main keeps develop's shape: resonance pinning, the validity boundary, the
regularization policy and the Validity output move into named helpers in the
owning modules (KineticForces.resonance_grid_nodes / axis_validity_boundary /
write_validity!, and kinetic_regularization_kwargs beside perturbed_equilibrium).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmkThis pull request is missing an assignee and a reviewer. If you are not ready to name them, mark this pull request as a draft. |
A rational surface inside the envelope's transition band gets its near-singular kinetic increments multiplied by a rapidly varying, near-zero envelope, which the matrix splines cannot represent; the overshoot propagates NaNs into the stability solve. Caught by the Solovev D-T deck, where the widest-orbit species (tritium) puts psi_c at 0.12 and the rational sits at 0.1221. Push the boundary past any rational whose window the band would cut through -- where orbit widths already reach <r> the resonance is not trustworthy anyway -- and compute that boundary once per run, threading it to the kernel, the band knots and the Validity output so all three agree. Also from the clean-code review: single orbit_widths helper feeding both the boundary and the diagnostic profiles, and Validity's psi_c/envelope/is_valid now describe one species (the widest-orbit one) instead of contradicting each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
Single named constants for the kinetic singular threshold and its relaxed reporting fraction (they were duplicated literals in two files that could drift), one local-maxima pass instead of two, the regularization policy moved into PerturbedEquilibrium which owns reg_spot, the band-knot count named and justified, the struct field documented in the docstring per convention, and two allocating minimum(abs.(...)) checks replaced with any(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
logan-nc
commented
Sep 5, 2026
Review package (self-contained, private artifact): https://claude.ai/code/artifact/09745676-ff9b-49c6-a14b-74f093427d00 Covers the validity boundary and where it comes from, what the invalid region was costing, the resonance-aware grid, the near-singular F̄ guards, and the regularization-off argument — plus, new since the port onto develop, the per-species ψ_c table (deuterium sets it; carbon's is 4× smaller) and the rational-inside-the-transition-band bug the port exposed and how it is now guarded. |
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.KineticForces/Validity/output group, written whenever kinetic profiles are used: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 self-consistent kinetic runs (harness @ 14254f5)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.
Regression report
regress --cases diiid_n1,solovev_n1,solovev_kinetic_calculated,solovev_kinetic_ntv,solovev_kinetic_nuzero --refs develop,local,baseline develop @ 349a0c2, working tree at 14254f5. Deltas below bundle this PR with
its base #398 (this branch is stacked on it); #398's own table is in that PR.
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), with ODE steps 759 → 789 and 754 → 794.
solovev_kinetic_ntv'storque move is likewise inherited from #398. 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 61× 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).
Both sides were measured against a develop that includes its own kinetic bugfixes (#391 ω_D
prefactor, #392 energy-integration tolerances, both merged 2026-09-04).
Caveat on coverage: the Solovev calculated example has no
[PerturbedEquilibrium]section andsolovev_kinetic_ntvruns atkinetic_factor = 0, so the harness cannot yet 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.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk