KF/EQUIL/PE - NEW FEATURE - Kinetic validity suppression, validity diagnostics, resonance-aware grid, and regularization off for kinetic runs - #414
Conversation
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>
|
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
…rossings are detected (#419)
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
…tories (pure move) (#400)
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
…into ideal and kinetic (#383)
…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>
…escales and widths (#431)
…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>
2225813 to
0c17097
Compare
…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
|
Note for anyone benchmarking from this branch before it is reconciled with develop: this stack forked at |
…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
…from the KineticForces NTV torque (#424)
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
|
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. |
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
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.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LzbLFQKyuRE5DYZmLokKmk