KineticForces - FEATURE! - Multi-main-ion NTV (summed ions + impurity + electrons, both paths) - #339
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 (
|
|
@krystophny could you review this (for TC24)? |
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.
krystophny
left a comment
There was a problem hiding this comment.
@logan-nc Three things before this goes in.
The fraction path in resolve_ntv_species never checks that the fractions sum to one. If they don't, the shortfall ends up in the quasineutrality impurity, which pushes zeff toward zimp, and zpitch has a pole there. A typo like 0.5/0.4 gives no error, just a collisionality that runs away. Either require sum == 1 within a tolerance, or say what a partial sum means.
write_kinetic_h5 doesn't write species_densities, but read_kinetic_h5 reads it, and docs/src/kinetic_forces.md tells people to author files with write_kinetic_h5. So the documented way of making a kinetic file can't produce one the new explicit-profile input accepts. Needs the put() loop.
The resolve_ntv_species docstring still says explicit per-species profiles are "not yet wired". They are, a few lines below.
The hard error on the scaling factors is fine.
|
@logan-nc I am going to refrain from reviewing this until we have our group sit-downs next week. Unfortunately I have a lot of these PR review requests building up and cannot get them done fast enough with my thesis writing right now. |
…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>
…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 round 2 applied (six commits) + independent 3-agent auditAll three of @krystophny's items, the remaining claude-bot items, and the findings of a fresh independent review (physics / clean-code / performance agents over the full diff) are addressed. Point-by-point: @krystophny's items1. Fraction-sum validation — implemented as two hard errors (
A blanket Σ==1 check would have broken supported mixed 2. 3. Stale "not yet wired" docstring — fixed; the docstring now describes the implemented named-dataset lookup + resample behavior. claude-bot items
Independent audit findings applied
RegressionFull run:
The two changed cases are the one intended baseline change (the calculated deck now declares a 50/50 D-T main-ion mix to pin the Mode A species loop): Attribution was verified by isolation: re-running |
…orms to the h5 schema Merge origin/develop (b3abe07), which brought the repo-wide gpec.h5 naming and metadata standard of docs/development/hdf5-conventions.md. The per-species groups this branch added predated that standard: flat lowercase top-level kinetic_forces_<label> siblings violated both the CamelCase group rule and the physics-first organization principle. Per-species results now nest under KineticForces/PerSpecies/<label>/<method>/, parallel to Tearing/PerSurface/, with the summed total staying at KineticForces/<method>/. write_to_hdf5! takes a species_label instead of a free-form group_name, so callers can no longer name arbitrary top-level groups; the top group is opened-or-created because a multi-species run writes it once per species plus once for the total. The metadata pass now annotates the method groups actually written rather than walking keys(g), which would otherwise annotate the PerSpecies container and leave every per-species dataset without long_name/units. Species labels drop the ordinal (ion_z1_m2, not ion1_z1_m2) since z and m identify the species; a repeated (z, m) pair gets a numeric discriminator so group creation cannot collide. The schema-test group whitelist and the conventions doc cover the species token, and the multi-ion regression case moves to the new paths (including SingularSurfaces/rational_count and the ForceFreeStates-prefixed eigenmode path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins KineticForces/PerSpecies/<label>/<method>/ with the summed total at KineticForces/<method>/, and asserts long_name/units reach the per-species method level — the failure mode if the annotation pass stops at the PerSpecies container instead of walking to the method groups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tly two token levels The whitelist admitted anything at any depth below KineticForces/PerSpecies/. Only the species label and the method token are data-driven; EnergyIntegrals/ and KineticMatrices/ below them are ordinary groups and must still be CamelCase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@matt-pharr has been assigned to many reviews, so I'll hand this off to @ebursch since he is already developing things related to torques. @ebursch this needs a human sanity check of the diff - sorry it's a bit lengthy but luckily the physics isn't hard. Ultimately, we are just trying to loop over multiple species in the NTV calculation. |
Regression re-baselined against current develop (
|
| Case | Result |
|---|---|
diiid_n1 |
48 unchanged |
solovev_n1 |
21 unchanged |
solovev_kinetic_ntv |
6 unchanged (ψ-quadrature NTV bit-identical) |
solovev_kinetic_calculated |
6 changed — deliberate D-T fixture change |
solovev_kinetic_nuzero |
7 changed — same fixture (reuses the calculated deck via [overrides]) |
solovev_kinetic_multiion |
new case; develop ref fails as expected (Example directory not found), local values recorded |
Every changed row is confined to the two cases that share the calculated deck, which this PR deliberately switched to a 50/50 D-T main-ion mix so the multi-ion kinetic-matrix species loop is pinned end-to-end:
solovev_kinetic_calculated develop local Diff
total energy Re(et[1]) 1.845905e+00 1.873742e+00 2.784e-02 (1.51%)
total energy Im(et[1]) -1.421059e+00 -1.434284e+00 1.323e-02 (0.93%)
plasma energy Re(ep[1]) -8.593964e+00 -8.563835e+00 3.013e-02 (0.35%)
vacuum energy Re(ev[1]) 1.043987e+01 1.043758e+01 2.293e-03 (0.02%)
ODE steps (total) 1800 1801 1 (0.06%)
solovev_kinetic_nuzero (collisionless override of the same deck)
total energy Re(et[1]) 2.245439e+00 2.243336e+00 2.103e-03 (0.09%)
total energy Im(et[1]) -2.034869e+00 -2.026028e+00 8.841e-03 (0.43%)
plasma energy Re(ep[1]) -8.485145e+00 -8.479030e+00 6.115e-03 (0.07%)
Attribution is pinned: diiid_n1's NTV torque FGAR and dW FGAR rows are exactly 0.0e+00, and solovev_kinetic_ntv is entirely unchanged, so the single-ion paths are untouched and the movement is the fixture change alone.
HDF5 schema conformance
Per-species NTV output now nests under KineticForces/PerSpecies/<label>/<method>/ per docs/development/hdf5-conventions.md (the flat kinetic_forces_<label> top-level groups violated the CamelCase group rule). Verified on a real 3-species run:
KineticForces/
fgar/ total_torque = 1.317868e-04
PerSpecies/
ion_z1_m2/fgar/ +7.794827e-05
ion_z1_m3/fgar/ +8.206067e-05
electron/fgar/ -2.822214e-05
Species sum to the total bitwise; every per-species dataset carries long_name + units; dTdpsi is native ComplexF64. runtests_h5_schema.jl 14/14, runtests_kinetic.jl 277/277, runtests_multiion.jl 52/52.
# Conflicts: # src/GeneralizedPerturbedEquilibrium.jl # src/KineticForces/KineticForcesStructs.jl
This reverts commit 0188f80.
krystophny
left a comment
There was a problem hiding this comment.
Thanks, Nik. I rechecked the requested points and the multi-ion regression. We accidentally included an unnecessary CI hash refresh, reverted it, and everything is now fine to merge from my side.
…d drop dead deck keys A develop merge into this branch silently reverted PR #367's immutability freeze on KineticForcesControl, because the multi-species loop mutated a deepcopy of the run control. That made the branch carry an undocumented revert of an upstream refactor and depend on the mutability upstream removed ("KineticForcesControl had no writers in src/ at all" — this loop was exactly such a writer, and the only one left). Build each species' control with the @kwdef keyword constructor instead, the idiom the freeze commit documents and that check_psi_quadrature_convergence already uses, and restore @kwdef struct. Threading species identity through explicit arguments was rejected: ctrl is passed whole into integrate_psi_quadgk and onward, so that would touch several nested signatures. Also drop six keys from the multi-ion deck that develop removed when it replaced the integrator-selection axis with integrator= (mat_flag, ode_flag, force_wv_symmetry, use_parallel, parallel_threads, populate_dense_xi). They only emitted deprecation warnings, but populate_dense_xi in particular implied a guarantee it no longer provides — had integrator= been dropped on that belief, the run would have fallen back to the riccati default, which does not populate dense xi, and PE/NTV would have gone quietly wrong. Document the PerSpecies layout in the h5 schema table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Release note
solovev_kinetic_calculatedand its collisionlessnuzerovariant are re-baselined because their shared deck now declares a 50/50 D-T mix (Re(et[1]) +1.51%, Im(et[1]) +0.93%);diiid_n1,solovev_n1andsolovev_kinetic_ntvare unchanged (harness @ 0e21828)electron = truenow adds electrons in addition to the ion species rather than computing them instead of the ions, and per-species NTV output moved from flat top-levelkinetic_forces_groups into theKineticForces/PerSpecies/subtree.A plasma can now declare an arbitrary list of main-ion species, each with its own charge, mass and density, and the NTV is computed per species on one shared full-composition Zeff and summed over the main ions, the quasineutrality-closing impurity and the electrons. This applies to both the post-PE ψ-quadrature diagnostic and the self-consistent kinetic matrices, and replaces the previous workaround of running each ion separately, which gave every run the wrong Zeff.
Regression report
regress --cases diiid_n1,solovev_n1,solovev_kinetic_ntv,solovev_kinetic_nuzero,solovev_kinetic_calculated,solovev_kinetic_multiion --refs develop,local(develop @14a3c9d6)Every changed row is confined to the two cases sharing the calculated deck, which this PR deliberately switched to a 50/50 D-T mix so the multi-ion kinetic-matrix species loop is pinned end-to-end.
diiid_n1's NTV torque and dW rows are exactly0.0e+00, so the single-ion paths are untouched and the movement is the fixture change alone.Notes for reviewers
Since the last review round this branch also merged develop and restored develop's #367 immutability freeze on
KineticForcesControl, which an earlier develop merge had silently reverted here because the species loop mutated adeepcopyof the run control. Each species' control is now built with the@kwdefkeyword constructor — the idiom that freeze commit documents. The multi-ion example produces bit-identical per-species torques before and after that rewrite.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.electronflag:electron = truealways adds the electron NTV in addition to the ion species, on both the single-ion and multi-ion paths (a single-ion + electron run resolves as {main ion, impurity, electron} summed). The Fortran PENTRC electron-instead-of-ions behavior was an IO limitation and is deliberately not reproduced.electron = falsewith an emptyion_specieslist is bit-for-bit unchanged.n_i); an all-fraction list must sum to 1 (in mixed fraction/density lists the impurity content is set by the file'sn_i/n_edeficit); every main-ionz < zimp(zpitch-closure pole atZeff = zimp, with a near-pole@warn); hard error on any negative density.API
Empty
ion_species+electron = false⇒ single main ion fromzi/mi— bit-for-bit unchanged. Explicit measured profiles via named HDF5 datasets (density = "n_D"; datasets must be namedn_*and round-trip throughwrite_kinetic_h5). Docs: new "Multi-ion runs" section indocs/src/kinetic_forces.md.Validation
test/runtests_multiion.jl, 46): composition (exact single-ion reduction; 50/50 D-T Zeff; quasineutrality), TOML conversion, resolver (single-species reduction, D-T-e set, ν∝1/√m, one-of-{fraction,density}, unified electron semantics), explicit HDF5 profiles + write/read roundtrip, all validation guards, union-grid summing incl. duplicate-node guard.solovev_kinetic_multiion(ψ-quadrature path: summed total + per-species NTV) andsolovev_kinetic_calculatednow runs 50/50 D-T (self-consistent kinetic-matrix species loop; deliberate baseline changeet[1]: 1.846−1.421i → 1.874−1.434i, with the collisionlessnuzerooverride variant shifting accordingly).:staticthreading on the kinetic-matrix ψ-loop.Behavior changes (deliberate)
electron = truewith emptyion_speciesnow computes ion + impurity + electron NTV summed (was: electron only). No shipped example or regression case used the old mode.solovev_kinetic_calculatedbaseline re-blessed for the D-T fixture change.🤖 Generated with Claude Code