Skip to content

KF/EQUIL/PE - NEW FEATURE - Kinetic validity suppression, validity diagnostics, resonance-aware grid, and regularization off for kinetic runs - #414

Draft
logan-nc wants to merge 121 commits into
performance/consistent-surface-theta-parametrizationfrom
feature/kinetic-axis-validity
Draft

KF/EQUIL/PE - NEW FEATURE - Kinetic validity suppression, validity diagnostics, resonance-aware grid, and regularization off for kinetic runs#414
logan-nc wants to merge 121 commits into
performance/consistent-surface-theta-parametrizationfrom
feature/kinetic-axis-validity

Conversation

@logan-nc

@logan-nc logan-nc commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

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

  1. Near-axis kinetic validity suppression (axis_validity_suppression, default on; a single Bool — 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.

  2. 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.

  3. 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_nodes locator the NTV quadrature panels at) are pinned into the grid as plain knots via merge_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 with toroidal_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).

  4. Regularization off in self-consistent kinetic runs. reg_spot smooths 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 force reg_spot = 0 and 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_find correctly 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).

    configuration max |ξ^α| NTV torque [N·m] EL dissipation [N·m]
    ideal, reg_spot = 0 643.8 6074.3
    ideal, reg_spot = 0.05 0.058 0.554
    kinetic, reg_spot = 0.05 0.055 0.1655 0.1322
    kinetic, reg_spot = 0 0.059 0.1324 0.1322

    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.

  5. 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 into SingularSurfaces/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 — respecting MIN_KNOT_SPACING, the validity band, and a 24-knot cap.

    case qualifying peaks knots inside FWHM inserted result
    nominal none 0 bit-identical (et[1], 3665 steps, torque unchanged)
    low-collisionality none 0 bit-identical
    collisionless + rot 0.2 ψ=0.5296, 0.5072 0 and 0 6 et[1] Δ3e-6, PE-vs-NTV 3.66% → 3.57%

    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:

grid / kernel evals et[1] EL steps wall (FFS, 32 threads)
develop c42d558 (ad-hoc auto grid, no adjustments) 554 / 554 1.006441 − 0.259620i 7,972 427 s
stack unsuppressed 288 / 288 1.005545 − 0.259371i 223,271 905 s
stack suppressed (ψ_c = 0.04) 288 / ~258 1.005298 − 0.259472i (Δ 2.5e-4) 4,246 365 s

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

run Force-Free States KineticForces total insertions
nominal, before 206.3 s 36.9 s 275.7 s
nominal, with guards 205.1 s 37.6 s 274.5 s 0
collisionless + rot 0.2, before 211.0 s 63.8 s 306.6 s
collisionless + rot 0.2, with guards 214.1 s 60.4 s 305.8 s 6

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:

  • diiid_n1 (ideal): 47/47 unchanged — bit-identical.
  • Solovev kinetic cases: energies move ≤ 0.06%, step counts ≤ 1.1%, NTV torque 0.12% (its quadrature domain now starts at ψ_c). These are the axis-validity deltas already reported; the regularization change and the F̄ guards add nothing to them, for the reason below.
  • DIII-D kinetic-calculated, nominal: bit-identical with all guards active (et[1] = 1.005300−0.259473i, 3665 steps), and a deck that omits reg_spot entirely 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_ntv runs kinetic_factor = 0 (not self-consistent), and Solovev_kinetic_calculated_example has 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_matrices is now idempotent (the ideal-restore workaround is gone),
KineticForces/Output.jl already opens-or-creates its group, and the :static kinetic-threading
fix landed in 9fe8a60c9. What remains is ported onto MatrixSplines, with the arbitrary-ψ kernel
argument type-stable (psis::Vector, empty meaning metric.xs) and the orchestration layer gaining
two lines — the logic lives in KineticForces.resonance_grid_nodes / axis_validity_boundary /
write_validity! and PerturbedEquilibrium.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:

species ψ_c ratio to main ion
deuterium (main, z=1 m=2) 0.040 1.00
carbon impurity (z=6 m=12) 0.010 0.25
hydrogen (z=1 m=1) 0.030 0.75
helium (z=2 m=4) 0.030 0.75
electrons 0.000

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_calculated failed while develop passed. The boundary is
now 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 Validity output so the three cannot
disagree.

Verification after the port

  • Unit: runtests_kinetic 277/277, runtests_multiion 52/52, runtests_sing 76/76,
    runtests_h5_schema 22/22.
  • Harness vs develop: all four cases run; solovev_kinetic_calculated Re(et[1]) 0.94%,
    Im(et[1]) 6.7%, steps 737 → 792; solovev_kinetic_ntv and _nuzero similar magnitude. These are
    the deliberate physics of the suppression and the regularization change.
  • DIII-D headline set re-measured on the ported stack: nominal 3665 steps, PE 0.1322 / NTV 0.1324
    (0.2%)
    ; suppression off 222,489 steps; collisionless + slow rotation 3031 steps — all
    reproducing the pre-port values.

⚠️ REQUIRES THIRD-PARTY HUMAN REVIEW BEFORE MERGING — NON-NEGOTIABLE. DO NOT MERGE WITHOUT AN APPROVING HUMAN REVIEW.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk

logan-nc and others added 30 commits July 29, 2026 10:54
The lnLambda formula 17.3 - 0.5*log(ne/1e20) + 1.5*log(Te/1keV) is the NRL Plasma
Formulary electron-ion Coulomb logarithm (lambda_ei = 23 - ln(ne^1/2 * Te^-3/2))
re-normalized to (ne/1e20 m^-3, Te/1keV); its 17.3/-0.5/+1.5 coefficients are
calibrated for natural log. The Julia port used log10, so lnLambda -- and the
collision frequencies nue/nui that scale linearly with it -- ran ~13% low at a
20 keV core (19.2 vs the correct 21.7). Restore natural log, matching both Fortran
PENTRC inputs.f90:238 and the formulary. Regression (diiid_n1): NTV FGAR and dW FGAR
shift within tolerance (~0.06-0.2%; small only because that case sits near the
ne=1e20/Te=1keV reference point where log10 ~= ln).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…ar path)

First increment toward correct multi-main-ion (e.g. D-T) NTV. Adds an ion_fraction
control (default 1.0 = single main ion, bit-for-bit backward compatible) that scales
only this species' resonant density out of the total main-ion n_i. The Coulomb
collisionality and Zeff use the full n_i, so a multi-ion run supplies the TOTAL
main-ion density in the kinetic file and runs each species (D, T) with its fraction;
wdian/wdiat are unaffected (the fraction cancels in T*(dn/dpsi)/n), and nu_s stays the
full-composition collisionality. Vetted against Logan-Park PoP 2013 (fortran-physics-reviewer).

Wired through the fgar NTV torque path (Compute.integrate_psi_quadgk -> tpsi!) and
auto-exposed via the [KineticForces] TOML splat.

Not yet threaded (tracked in the feature plan): the matrix-method (_setup_surface_state)
and self-consistent kinetic-DCON paths use the implicit 1.0; and an internal
multi-species loop (D/T summation is still external for now, but with the correct Zeff).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…arbitrary ion mix)

Physics core for correct multi-main-ion NTV: full-composition Zeff and momentum-restoring
pitch-angle enhancement from an arbitrary list of main-ion (z_s, n_s) plus one impurity that
closes quasineutrality (n_imp = (ne - Sum z_s n_s)/zimp; Zeff = (Sum z_s^2 n_s + zimp^2 n_imp)/ne).

Verified: reduces EXACTLY to the current single-ion formula Zeff = zimp - (ni/ne) zi (zimp-zi)
(Zeff 1.44355 both ways); gives the correct 50/50 D-T Zeff=1.44 vs the wrong 3.72 that each
split single-ion run currently sees (the ni double-duty bug). Per-species collisionality built
on this shares zpitch/n_main/Zeff/lnLambda and varies only z_s^2, m_s, T_s (Logan-Park PoP 2013,
fortran-physics-reviewer). Pure function, not yet wired into the loader/driver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…uilder

Replaces the scalar ion_fraction stopgap with the species-list design. Adds:
- IonSpecies(z, m, fraction|density) + ion_species::Vector on KineticForcesControl,
  auto-converted from [[KineticForces.ion_species]] TOML tables (Vector{Dict}->Vector{IonSpecies}).
  Empty default => single ion from zi/mi (unchanged).
- Equilibrium.build_species_profiles(file, ion_species; zimp, mimp) -> Vector{KineticProfileSplines},
  one per species: ni_spline = that species' resonant density (fraction * total n_i), nui_spline =
  its full-composition collisionality (shared Zeff/zpitch/lnLambda via multi_ion_composition, per-species
  z_s^2, m_s, T_i); ne/Te/omegaE/zeff/nue shared. tpsi! consumes each view unchanged.

Verified: single-species [z=1,m=2,fraction=1] reproduces load_kinetic_profiles exactly (ni/nui/zeff);
50/50 D-T gives n_D=n_T=0.5 n_i, shared Zeff=1.46 (not the buggy 3.7), nu_D/nu_T=sqrt(3/2).
Reverts the ion_fraction wiring in tpsi!/Compute. Explicit per-species profiles (density=) validated
but not yet wired (fraction path only) -- next increment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…d output

Wires the ion_species list end to end for the fgar NTV (post-PE diagnostic): when ctrl.ion_species
is non-empty, build per-species profiles (build_species_profiles), run compute_torque_all_methods!
once per species (+ electron if enabled) sharing the perturbed field and the full-composition Zeff,
and sum via combine_species_states (exact scalar totals; dT/dpsi summed on the union grid via the
per-species torque_profile interpolants; T(psi) re-integrated). Output: per-species groups
kinetic_forces_ion<i>_z<z>_m<m> / kinetic_forces_electron, plus the summed total in the standard
kinetic_forces group (so existing analysis reads the total). Empty ion_species => unchanged single
path. write_to_hdf5! gains a group_name kwarg. The self-consistent kinetic-DCON path stays single-ion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…version

TOML.jl parses [[KineticForces.ion_species]] as Vector{Any} (not Vector{Dict}), so the
AbstractVector{<:AbstractDict} convert did not match. Broaden to AbstractVector with per-element
construction (pass IonSpecies through).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…e + kinetic matrices) incl impurity

Both NTV paths now loop over and sum the SAME full species set. resolve_ntv_species (was
build_species_profiles) returns descriptors (z, m, profiles, electron, label) for: the main ions,
the neutrality-closing impurity (zimp/mimp, density = the quasineutrality n_imp) as its own resonant
species, and (optionally) electrons -- all on one shared full-composition Zeff/zpitch/lnLambda.
Resolved once in main() and passed to:
- the KineticForces quadrature path (per-species compute + combine_species_states), and
- compute_calculated_kinetic_matrices (self-consistent DCON Mode A): the kinetic W/torque matrices
  are additive over species, so the threaded psi-loop is wrapped in a species loop that accumulates
  (+=) each species' block-diagonal contribution (species=nothing => single-species, unchanged).
Empty ion_species => single-species from zi/mi (bit-for-bit unchanged). Verified: D-T-e config
resolves to 4 summed species (D, T, C impurity, e).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
… kinetic EL crossing

cross_kinetic_singular_surf! built its placeholder IntegrationChunk with a stale
4-arg signature (psi_start, psi_end, needs_crossing, ising), but the struct's
@kwdef positional constructor requires the direction field added later for
bidirectional parallel FM. The 4-arg call throws MethodError. This path is only
reached on the serial Euler-Lagrange shooting solver (use_parallel=false) with a
kinetic singular surface (kinetic_factor>0), which previously errored earlier in
the Riccati branch, so the defect stayed latent. Now passes direction=1, matching
the sibling placeholder call sites in cross_ideal_singular_surf! and the Riccati
crossing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…HDF5)

Completes the multi-ion IO: KineticProfileData gains a species_densities dict populated by
_read_kinetic_h5 from any non-standard HDF5 datasets (e.g. "n_D", "n_T"). resolve_ntv_species'
`density=` branch resolves a species from its named profile (resampled), the same downstream path
as the `fraction` shorthand -- so measured, differently-shaped per-species profiles are supported
via the HDF5 container while ASCII+fraction stays the simple case. One-of-{fraction,density} per
species is validated; a missing named profile errors with the available names. Verified: n_D=0.6*Ni,
n_T=0.4*Ni explicit datasets resolve to the correct per-species densities, impurity closes
quasineutrality from them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
Covers: multi_ion_composition (exact single-ion reduction for z=1,2; 50/50 D-T Zeff; quasineutrality
closes the impurity), IonSpecies + TOML Vector{Any}->Vector{IonSpecies} conversion, resolve_ntv_species
(single-species reduces to load_kinetic_profiles; D-T-e set = {D,T,impurity,electron}; nu ~ 1/sqrt(m);
one-of-{fraction,density} validation), and explicit per-species HDF5 density profiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…_multiion)

New example examples/Solovev_kinetic_multiion_example (Solovev main ion modelled as 50/50 D-T) and
regression case solovev_kinetic_multiion tracking the summed total plus each per-species NTV
contribution (D, T, electron; pure fixture so no impurity), pinning the multi-species
resolve_ntv_species + combine_species_states loop end to end. Verified: total 1.318e-4 = D+T+e sum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…ity (per physics review)

Per fortran-physics-reviewer sign-off of the multi-species NTV: (1) flag that zpitch is a main-ion
momentum-restoring closure applied approximately to the impurity/electron test species; (2) the
electron descriptor now carries ne (not ns[1]) in the unused ni_spline slot for clarity. No numerical
change. Reviewer verdict: collision-freq z^2/n_main form CORRECT (n_main is the faithful Fortran
choice, not ne*Zeff); the self-consistent dW +0.066->-0.10 shift is additive-channel physics, not a
summation bug (ideal F/K/G added once downstream, kw/kt purely kinetic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…ll zeros

The multi-species combined dT/dψ profile (written to the "kinetic_forces" total group) was identically
zero: combine summed via each MethodResult.torque_profile interpolant, but that field is not populated
by the per-species compute (only the dtdpsi array is). total_torque was correct (summed scalar), so the
bug was invisible until the combined mid-radius profile was integrated. Fix: linear-interpolate each
species' (psi_grid, dtdpsi) arrays onto the union grid and sum (zero outside a species' range). Unit
test added (overlap sum + nonzero + total). Per-species output groups were always correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…omment rule)

Collapse the 3-line lnLambda comment to one self-contained line: drop the 'not log10'
bug-history note and the stale Fortran file:line, per the keep-comments-concise rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…nChunk (PR #336 review)

Match the sibling call sites (cross_ideal_singular_surf!, Riccati crossing): pass the in-scope
ising instead of 0. Functionally inert (sing_der! discards the chunk); keeps the cherry-picked
IntegrationChunk fix in sync with the amended bugfix branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
…icle z², density hard-stop, typed species

- ν_s carries the test-particle z² (Krook deflection, Logan & Park 2013 Eq. 6);
  the single-ion path now equals the multi-ion limit (identical at z=1).
- Hard error on any negative density (bad input or cubic-resample overshoot)
  instead of silently clamping the neutrality-closing impurity.
- resolve_ntv_species returns concrete-eltype ResolvedNTVSpecies structs, so the
  self-consistent kinetic-matrix species loop is type-stable.
- Share physical constants and the Coulomb-log helper across load_kinetic_profiles
  and resolve_ntv_species; drop duplicate local literals.
- Fix combine_species_states docstring (linear interp of dtdpsi, dropped fields);
  move the multi-ion contract into the KineticForcesControl / KineticProfileData
  docstrings; trim inline field comments.
- Make runtests_multiion self-contained (using HDF5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
… work

- kinetic_ntv_map.md: multi-ion NTV audit — test-particle z² justification,
  field-density (zpitch·n_main vs n_e·Zeff) reconciliation, and the electron-
  driven self-consistent δW sign-flip analysis (physics, not a double-count).
- reg_spot_regularization.md: verdict that the reg_spot field-reconstruction
  smoothing port is correct and distinct from the singfac_min ODE crossing gate.
- MEMORY.md: index pointer to the new reg_spot audit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1
Two conflicts, both from develop moving under this branch.

The Coulomb logarithm was corrected to a natural log on both sides
independently: develop in #337, this branch in its own refactor. They are
numerically identical, so the helper is kept and develop's annotation already
sits on its definition.

The kinetic-matrix block conflicted because develop hoisted it and this branch
still carried the older copy, which git left duplicated. One copy is kept, in
develop's position, with this branch's `species=kf_species` argument carried
into it. The branch's guard on `ctrl.ode_flag` is dropped because develop
deprecated and removed that flag; the surviving guard is `singfac_min > 0`.

The module loads against the merged tree.
SingularSurfaces/ca_left and ca_right were written to gpec.h5 unconditionally,
but only ideal singular-surface crossings populate odet.ca_l/ca_r — kinetic and
galerkin-matched runs dumped uninitialized heap memory (non-reproducible NaN
counts between identical runs; subnormal garbage values).

- Allocate ca_l/ca_r with zeros instead of undef (kills in-memory
  nondeterminism at the root).
- New OdeState.ca_populated flag (mirrors du_store_populated), set by the two
  ideal crossing routines (EulerLagrange + Riccati) and carried through the
  Riccati dense-xi save/restore; galerkin-matched OdeStates keep the default
  false.
- The writer emits rank-4 zero-extent sentinels when the flag is false —
  datasets stay always-present (no reader KeyErrors, metadata annotations
  unchanged), matching the established empty-sentinel idiom.
- Guard the two readers that index ca: Analysis.plot_delta_prime returns a
  placeholder on empty ca, and the gal_{epsilon,beta}_scan benchmarks record
  NaN for the ca-jump diagnostic (they were consuming garbage on galerkin runs
  already); also fix a leftover legacy haskey(f["singular"], ...) in both.
- Zero-extent not-computed sentinel codified in hdf5-conventions.md; stale
  stability.md claim about SingularCoupling reading ca_l/ca_r corrected.
- Tests: kinetic fullrun asserts empty ca datasets; ideal schema run asserts
  populated + finite. New solovev_n1 harness quantity checksums ca_left to
  lock bitwise reproducibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
…z<zimp, zpitch pole, dup-node interp

Review response (PR #339): fractions are shares of n_i so any list summing
above 1 is a hard error, and an all-fraction list must sum to 1 (the impurity
is set by the file's n_i/n_e deficit, not a fraction shortfall). Main-ion
z >= zimp now errors (zpitch closure pole at Zeff=zimp) and a near-pole Zeff
warns. multi_ion_composition returns a neutral no-impurity composition at
ne<=0 instead of landing on the pole. combine_species_states guards
duplicate psi nodes in the union-grid interpolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ies; reader harvests only n_* datasets

Review response (PR #339): the documented authoring path (write_kinetic_h5)
silently dropped species_densities, so it could not produce a file the
explicit-density multi-ion input accepts. Per-species densities now write with
units attributes and must be named n_* — the reader correspondingly harvests
only non-standard n_* datasets, reserving other names for future schema fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…addition to ions

Unify the electron flag semantics: on the single-ion path electron=true used to
compute the electron torque INSTEAD of the ion torque (a Fortran PENTRC IO
limitation), while the multi-ion path appended electrons to the species sum. A
single-ion + electron run now routes through resolve_ntv_species as the species
set {main ion from zi/mi, quasineutrality impurity, electron}, summed like any
multi-ion run (per-species output groups + summed total). electron=false with an
empty ion_species list is bit-for-bit unchanged. No shipped example or
regression case used the old electron-only behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, add docs section, annotate example TOML

Review response (PR #339): drop the stale 'not yet wired' clause (explicit
per-species densities are implemented); state the z=1 validity domain of the
zpitch*n_main field term; cite the Krook nu numerical form to PENTRC
inputs.f90:238-244 rather than implying Logan & Park Eq. 6 carries the
prefactor; document t_cumulative as a diagnostic (endpoint need not equal the
exact summed total); per-point signature note on multi_ion_composition; new
'Multi-ion runs' docs section (ion_species table, fraction vs density, n_*
schema, per-species output groups, unified electron flag); inline annotations
on every multi-ion example TOML key per convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d species + shared constants

Review response (PR #339): the kinetic-matrix psi loop indexed per-thread
buffers by threadid() under default dynamic scheduling — a task-migration
hazard multiplied by the new species loop (the sibling quadrature loop already
uses :static). Pin with :static and hoist the per-species scalars so the
closure captures concrete values instead of a union-typed sp. Type the species
kwarg as AbstractVector{<:ResolvedNTVSpecies}. Distinct _E_CHARGE constant
where the physics wants the elementary charge (same PENTRC-parity value as
_EV_J), single _NKIN resample-grid constant shared by both loaders, and
combine_species_states unions method names over all states instead of taking
the first state's list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Mode A matrices

Review response (PR #339): the self-consistent kinetic-matrix species loop was
only verified by a manual ITER TC-24 run. The existing calculated-path fixture
now declares the main ion as a 50/50 D-T mix, pinning the multi-ion matrix
accumulation end-to-end in the regression harness. Deliberate baseline change:
et[1] moves from ~1.894-1.525i (single D) to ~1.874-1.434i (D-T summed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…physics-reviewer memory

The recorded example arithmetic was wrong (Zeff=1.5 with C6 gives ~18% gap, not
1.55 vs 1.5); the conclusion (zpitch·n_main is PENTRC's deliberate design)
stands and is now stated without the false near-equality.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings the remediated stack base up (second develop sync, review fixes,
reviewer dispositions, Tearing metadata remediation). Two modify/delete
conflicts: benchmarks/gal_validation/gal_{beta,epsilon}_scan.jl were untracked
on develop while this branch had guarded their ca-jump reads — accepted the
deletion; those guard hunks are moot for files that no longer exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz
…ero case annotations

The nuzero case reuses the calculated example deck via [overrides], so it
inherits the 50/50 D-T multi-ion fixture change (same documented baseline
shift, collisionless variant); drop the stale et[1] quote.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@logan-nc logan-nc changed the title KF/EQUIL - NEW FEATURE - Near-axis kinetic validity suppression, validity diagnostics, and resonance-aware auto grid KF/EQUIL/PE - NEW FEATURE - Kinetic validity suppression, validity diagnostics, resonance-aware grid, and regularization off for kinetic runs Aug 22, 2026
@logan-nc

Copy link
Copy Markdown
Collaborator Author

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 reg_spot = 0.05): the override logs, PE/fgar agree to 0.2%, and the EL solve is bit-identical (et[1] and 3665 steps unchanged) — reg_spot only ever entered the post-processing that builds the NTV input.

logan-nc and others added 2 commits August 22, 2026 16:35
…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
@logan-nc
logan-nc force-pushed the feature/kinetic-axis-validity branch from 530ade4 to b3d7ab9 Compare August 22, 2026 22:03
logan-nc and others added 11 commits August 22, 2026 18:31
…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
Resolve the two modify/delete conflicts by porting develop's edits into the
new file locations:

- OdeState.ca_populated (+ zeros-initialized ca_l/ca_r) from
  ForceFreeStatesStructs.jl into EulerLagrange.jl.
- ca_populated flag in _capture_right_crossing_data! from Riccati.jl into
  Riccati/Crossings.jl.
- Per-column abstol in integrate_fm_with_ua_ic from Riccati.jl into
  Riccati/Propagators.jl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAMWNadfK6utwJz1AJrmKz
Resolve conflicts between this branch's FourFitVars -> MatrixSplines rename
and develop's multi-ion species plumbing by combining both:

- prepare_force_free_states! / build_kinetic_matrix_splines calls keep the
  `mats` naming and immutable-reassignment form while gaining develop's
  `species=` argument.
- compute_calculated_kinetic_matrices keeps the `mats::MatrixSplines`
  parameter and gains develop's `species` keyword.
- The kinetic psi loop takes develop's multi-species restructure wholesale;
  this branch's only change there was formatter whitespace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAMWNadfK6utwJz1AJrmKz
…escales and widths

slayer_parameters took sval_r with its sign in five places that are magnitudes: the
parallel diffusivity chi_par_lmfp (twice), the island width Wd, the :lar critical-Delta,
and the Alfven time tau_h. On a reverse-shear surface tau_h came out negative, hence a
negative Lundquist number, and tauk = lu^(1/3)*tau_h threw a DomainError before any layer
quantity was produced. The two sqrt() sites would have thrown on the same surface.

Found while scanning DIII-D reconstructions: shot 153072_3415 has q dipping to 1.855
off-axis, giving two q = 2 surfaces, and the inner one is on the negative-shear branch.
Rational-surface finding is already reverse-shear-safe, so the layer build is handed
exactly those surfaces.

The layer timescales and widths depend on |dq/dr|, not its sign -- the module's own
diffusive-resistive width derivation states the convention as (n|s|). abs(s) == s wherever
the shear is positive, so no currently-working surface changes value; this only turns a
crash into a result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…yer magnitudes

The shear-magnitude fix has no test coverage: every SLAYER testset builds its
parameters at sval_r = 1.0, and no shipped deck has a reverse-shear rational
surface, so a regression would be silent.

Assert that a negative-shear surface reproduces its positive-shear mirror
bit-identically across every normalized layer quantity, that sval_r keeps its
sign in the diagnostic, and that the Lundquist number stays positive so the
S^(1/3) that used to raise a DomainError evaluates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…invariants

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@logan-nc
logan-nc force-pushed the performance/decoupled-el-matrix-grid branch from 2225813 to 0c17097 Compare September 4, 2026 16:15
logan-nc and others added 2 commits September 4, 2026 12:16
…m the torque annotation commit

The previous commit ran through JuliaFormatter, collapsing the hand-maintained
alignment in Utils.jl and PerturbedEquilibriumStructs.jl and re-indenting two
docstring bullet lists. None of that is part of the torque delineation, and it
buries the three substance hunks in ~120 lines of whitespace noise. Restore the
pre-commit formatting so the PR diff is exactly the docstring, the HDF5
long_name, and the computation-site comment. A broader reformatting pass is
planned separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CV1jBWtEgxA1aQvjB9SBhs
…nnotation

The eight-line bullet added to PerturbedEquilibriumState read as session notes
rather than a field description, against the concise-comment standard in
CLAUDE.md. Cut it to three lines carrying the three facts a reader needs: it is
the boundary-response torque, it matches the volume-integrated Euler-Lagrange
kinetic torque only for converged self-consistent solutions, and it is a
distinct construction from the KineticForces NTV torque. The δW surface-term
derivation and the grid-quality-diagnostic commentary belong in the docs, not
the struct docstring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CV1jBWtEgxA1aQvjB9SBhs
@logan-nc

logan-nc commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

Note for anyone benchmarking from this branch before it is reconciled with develop: this stack forked at 9491f893d (2026-08-16), which predates develop's :static kinetic-threading fix (9fe8a60c9). Runs launched from feature/kinetic-axis-validity as it stands are therefore nondeterministic at rare-task-migration frequency — identical decks were measured at et[1] = 1.0053 / 1.1355 / 0.9895. Reconciliation with develop brings the fix; until then, treat single runs off this branch as indicative rather than exact. All numbers reported in this PR were taken on a stack that carried the fix locally.

logan-nc and others added 3 commits September 4, 2026 12:33
…gies docstring

The `plasma_energy` bullet in the PerturbedEquilibriumState docstring carried a
trailing "# Response fields in mode space [npsi, mpert]" — a struct-body comment
that leaked into the docstring, where it duplicates the real comment above the
xi_modes/b_modes fields and renders as part of the energy description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CV1jBWtEgxA1aQvjB9SBhs
Base automatically changed from performance/decoupled-el-matrix-grid to performance/consistent-surface-theta-parametrization September 4, 2026 17:04
logan-nc and others added 2 commits September 4, 2026 13:48
Port the EL coefficient-spline core density cap onto develop's MatrixSplines API:
the cap logic moves into a named helper, core_capped_knots(xs, rationals), and
build_matrix_splines constructs IdealMatrices on the capped subset. Cap behaviour
is unchanged; only its plumbing follows develop's immutable ideal/kinetic split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk
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_01LzbLFQKyuRE5DYZmLokKmk
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

This pull request is missing an assignee and a reviewer.

If you are not ready to name them, mark this pull request as a draft.
docs/development/contributors.md suggests lead developers to ask.
Merging is not blocked here, but no pull request may be merged without human review.

logan-nc and others added 2 commits September 4, 2026 15:03
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants