KineticForces - NEW FEATURE - Multi-main-ion NTV (summed ions + impurity + electrons, both paths) - #339
KineticForces - NEW FEATURE - Multi-main-ion NTV (summed ions + impurity + electrons, both paths)#339logan-nc wants to merge 17 commits into
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
|
@claude review this pr |
Review: Multi-main-ion NTVReviewed the diff (18 commits: Potential physics inconsistency
_nu(zsp, msp) = [... (zpitch[i]/3.5e17) * zsp^2 * n_main[i] * loglam[i] / (sqrt(Float64(msp)) * ...) ...]but the pre-existing single-ion path in nui[i] = T_i > 0 ? (zpitch/3.5e17) * n_i * ll / (sqrt(1.0*mi) * (T_i/1.602e-16)^1.5) : 0.0For the flagship D/T case ( Test coverage gapThe new regression case ( Minor style
What looks solid
Process notePer |
…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
Review changes applied (
|
Summary
Adds correct multi-main-ion NTV to KineticForces. A plasma can now declare an arbitrary list of main-ion species (each with its own z, m, and density), and the NTV is computed per species on one shared full-composition Zeff and summed —
τ = Σ_s τ_s— over the main ions, the neutrality-closing impurity, and the electrons. This applies identically to both NTV paths:Previously only a single main ion was supported (D-T had to be run per-species externally and summed, which double-used the
n_icolumn as both the species density and the Zeff source — giving each ion run the wrong Zeff ≈ 3.7 instead of the composition Zeff ≈ 1.4). This is an extension beyond Fortran PENTRC (also single main ion), consistent with Logan & Park PoP 2013.Design (physics-reviewed)
n_s(ψ);fraction(share of the totaln_i) and explicit HDF5densityarrays both feed it (normalize-at-the-boundary).n_s(per-species torque prefactor) vs the collision field density (totaln_main+ shared composition Zeff + speciesz_s²,m_s).multi_ion_composition:Zeff = (Σ z_s² n_s + z_imp² n_imp)/n_e, impurity closes quasineutrality; reduces exactly to the single-ion formula for one z=1 ion.API
Empty
ion_species⇒ single main ion fromzi/mi— bit-for-bit unchanged. Explicit measured profiles via named HDF5 datasets (density = "n_D").Validation
test/runtests_multiion.jl): composition (exact single-ion reduction; 50/50 D-T Zeff; quasineutrality), TOMLVector{Any}→IonSpeciesconversion, resolver (single-species reduction, D-T-e set, ν∝1/√m, one-of-{fraction,density}), explicit HDF5 profiles.solovev_kinetic_multiion: tracks the summed total + each per-species NTV end-to-end.z_s²/n_mainform CORRECT (n_main is the faithful Fortran-matching choice, not n_e·Zeff); the self-consistent δW shift is additive-channel physics (ideal F/K/G added once downstream — no per-species double-count), not a summation bug.Stacking note
This branch includes two bugfix commits that are their own PRs and should merge first; they deduplicate on rebase:
#337— Coulomb log natural-log fix (prerequisite for correct collisionality).#336— IntegrationChunk arity fix (prerequisite for any Mode A / self-consistent kinetic run).Not included (follow-ups)
density_factor, …) with a multi-ion list error out (documented).zpitchis a main-ion closure applied approximately to impurity/electron test species (annotated).🤖 Generated with Claude Code
https://claude.ai/code/session_01SsLUydP2gJbE1tGoaHDiS1