KineticForces Module
Kinetic torque and energy calculations for perturbed equilibria. Implements neoclassical toroidal viscosity (NTV) from the PENTRC formulation (Logan & Park, 2013; Logan, 2015).
Kinetic profile file formats
Kinetic profiles are read by read_kinetic_file, which dispatches on the file extension:
- HDF5 (
.h5/.hdf5) — the GPEC kinetic schema (recommended). Datasets at the file root: requiredpsi(normalized poloidal flux),n_e,T_i,T_e,omega_E(andn_i, defaulting ton_eif omitted); optionalomega_tor,chi_e(perpendicular heat diffusivity $\chi_\perp$), andchi_phi(toroidal momentum diffusivity $\chi_\phi$). Each dataset carries aunitsattribute; the root carriesschema_versionandprovenance. Densities are m⁻³, temperatures eV, frequencies rad/s, diffusivities m²/s. Additional datasets namedn_*(e.g.n_D,n_T) are named per-species densities for multi-ion runs; names outsiden_*are reserved for future schema fields and ignored. Write files withwrite_kinetic_h5(which round-trips the per-species densities). - ASCII (
.gpeckf/.kin/.dat) — legacy six-column whitespace tablepsi_n n_i n_e T_i[eV] T_e[eV] omega_E, retained for backward compatibility. Header rows are skipped.
The NTV calculation consumes n_i, n_e, T_i, T_e, omega_E; chi_e/chi_phi, when present, are carried for the resistive-layer (SLAYER) analysis and ignored here.
Multi-ion runs
A plasma may declare an arbitrary list of main-ion species; the NTV is computed per species under one shared full-composition $Z_\mathrm{eff}$ and summed ($\tau = \sum_s \tau_s$) over the main ions, the quasineutrality-closing impurity, and (with electron = true) the electrons. This applies to both NTV paths: the post-PE ψ-quadrature diagnostic and the self-consistent kinetic_source = "calculated" matrices.
[KineticForces]
kinetic_file = "kinetic.h5" # n_i column/dataset = TOTAL main-ion density
electron = true # add electron NTV in addition to the ion species
zimp = 6 # impurity charge (closes quasineutrality)
mimp = 12 # impurity mass
[[KineticForces.ion_species]]
z = 1
m = 2
fraction = 0.5 # this species' share of the total n_i
[[KineticForces.ion_species]]
z = 1
m = 3
density = "n_T" # or: explicit n_* dataset from the HDF5 kinetic fileEach species sets exactly one of fraction (share of the file's total n_i) or density (a named n_* dataset). An all-fraction list must sum to 1; in a mixed list the impurity content is set by the file's n_i/n_e deficit, not by a fraction shortfall, and fractions may sum below (never above) 1. Every main-ion charge must satisfy z < zimp. An empty ion_species list runs the single main ion from zi/mi (with electron = true still adding the electron species — the electron flag always means in addition to the ions).
The summed total is written to KineticForces/<method>/ exactly as in a single-ion run, and each species' own contribution to KineticForces/PerSpecies/<label>/<method>/ — labels are ion_z<z>_m<m>, impurity_z<z>_m<m>, and electron (a numeric suffix is appended only if a run repeats a (z, m) pair). Every per-species group carries the same datasets and metadata as the total, so dTdpsi, T, and EnergyIntegrals/ are available per species. The summed cumulative torque profile is a diagnostic (linear interpolation onto the union grid); the summed total_torque scalar is the exact Gauss-Kronrod value.
The profile-scaling knobs below are not supported together with a multi-ion ion_species list (they error).
Profile Scaling Knobs
Seven scaling factors are available on KineticForcesControl to modify kinetic profiles and physics parameters for sensitivity studies:
| Knob | Default | Stage | Description |
|---|---|---|---|
density_factor | 1.0 | Profile loader | Density scaling (ni, ne) |
temperature_factor | 1.0 | Profile loader | Temperature scaling (Ti, Te) |
ExB_rotation_factor | 1.0 | Profile loader | ExB rotation scaling (omegaE) |
toroidal_rotation_factor | 1.0 | Profile loader | Total toroidal rotation scaling (wphi) |
wdfac | 1.0 | Evaluation time | Magnetic drift frequency scaling |
nufac | 1.0 | Evaluation time | Collisionality scaling |
divxfac | 1.0 | Evaluation time | $\nabla \cdot \xi_\perp$ scaling |
Profile-loader knobs (density_factor, temperature_factor, ExB_rotation_factor, toroidal_rotation_factor)
These four knobs are applied during kinetic profile loading in load_kinetic_profiles, before any physics evaluation. The physical model is:
\[\omega_\phi = \omega_E + \omega_{*n,i} + \omega_{*T,i}\]
where $\omega_\phi$ is the user's measured total toroidal rotation, $\omega_E$ is the ExB rotation (the input profile), and the diamagnetic frequencies are computed from cubic spline derivatives of the unscaled profiles:
\[\omega_{*n,i} = -\frac{2\pi T_i}{\chi_1 Z_i e} \frac{1}{n_i}\frac{dn_i}{d\psi}, \qquad \omega_{*T,i} = -\frac{2\pi}{\chi_1 Z_i e} \frac{dT_i}{d\psi}\]
The scaling sequence is:
- Build first-pass cubic splines from original (unscaled) profiles
- Compute $\omega_{*n,i}$, $\omega_{*T,i}$, and $\omega_\phi$ at each grid point
- Apply
density_factorto density,temperature_factorto temperature, and update diamagnetic terms: $\omega_{*n,\text{new}} = \texttt{temperature\_factor} \cdot \omega_{*n,i}$ (density_factorcancels in $T \cdot (dn/d\psi)/n$), $\omega_{*T,\text{new}} = \texttt{temperature\_factor} \cdot \omega_{*T,i}$ - Reform ExB rotation: $\omega_E = \texttt{toroidal\_rotation\_factor} \cdot \omega_\phi - \omega_{*n,\text{new}} - \omega_{*T,\text{new}}$
- Apply ExB scaling: $\omega_E \mathrel{*}= \texttt{ExB\_rotation\_factor}$
- Recompute collisionality from scaled density and temperature
- Build final splines from scaled arrays
Evaluation-time knobs (wdfac, nufac, divxfac)
These three knobs are applied during the bounce-averaged kinetic matrix and torque calculations in KineticForces/Torque.jl and related modules. They multiply the magnetic drift, collisionality, and $\nabla \cdot \xi_\perp$ terms respectively, and do not modify the stored kinetic profile splines.
Collisionality from scaled profiles: Julia recomputes collisionality ($\nu_i$, $\nu_e$) from the scaled density and temperature arrays. Fortran PENTRC (
inputs.f90:237-246) computes collisionality from unscaled profiles. If you need independent collisionality scaling without changing the density/temperature profiles, usenufac.Consistent derivative ordering: Fortran's
inputs.f90:269-272mixes pre-scaling spline derivatives with post-scaling array values when computing thetoroidal_rotation_factorback-solve. Julia uses a clean reimplementation with consistent pre-scaling derivatives throughout.
HDF5 outputs: complex torque convention and the EnergyIntegrals layout
The method level of KineticForces/<method>/ reports the two physical scalars a user wants first: total_torque = $T_\phi$ (N·m) and total_energy = $\delta W_k$ (J). Internally both are halves of one complex quantity $T = T_\phi + 2in\,\delta W_k$ (Logan 2013), and the ψ-profiles dTdpsi and T store that complex $T$ directly — so imag(T) carries the $2n$ factor while total_energy has it divided out. The per-record EnergyIntegrals/torque and EnergyIntegrals/kinetic_energy are separate complex diagnostics of the two integrand halves at each $(\psi, \lambda, \ell)$ evaluation, which is why they are not packed into one number there.
EnergyIntegrals/ stores the variable-length integration trajectories in the flat-plus-offsets ragged layout (chosen over HDF5 VLEN types for cross-language support; Tearing/Diagnostics/* uses the same pattern). Record k spans offsets[k]+1 : offsets[k+1] (Julia, 1-based) of each *_all array:
h5open("gpec.h5", "r") do f
g = f["KineticForces/fgar/EnergyIntegrals"]
off = read(g["trajectory_offsets"])
x_k = read(g["x_all"])[off[k]+1:off[k+1]] # record k's abscissae
I_k = read(g["integrand_all"])[off[k]+1:off[k+1]] # its complex integrand
endg = f["KineticForces/fgar/EnergyIntegrals"] # h5py, 0-based
x_k = g["x_all"][g["trajectory_offsets"][k]:g["trajectory_offsets"][k + 1]]GeneralizedPerturbedEquilibrium.KineticForces.METHOD_REGISTRY — Constant
METHOD_REGISTRYSingle source of truth for the NTV calculation methods. Each entry is a NamedTuple (name, flag, kind, doc):
name— short method identifier used as the HDF5 group key and inintr.methodflag— theKineticForcesControlfield symbol that enables the methodkind— dispatch routing tag consumed bymethod_kind/Torque.jl(:garfor the GAR/matrix family,:fcgl/:rlar/:clarfor the three special-cased methods)doc— one-line description printed in verbose output
The method names/docs and the Compute.jl enable list are all derived from this tuple, and Torque.jl routes on kind, so the methods are enumerated in one place. To add a method: append an entry here and add the matching *_flag field to KineticForcesControl.
GeneralizedPerturbedEquilibrium.KineticForces.BounceData — Type
BounceDataBounce-averaged quantities as functions of pitch angle λ. Produced by compute_bounce_data(), consumed by pitch integration.
GeneralizedPerturbedEquilibrium.KineticForces.BounceScratch — Type
BounceScratch(ntheta, mpert)Per-surface scratch for the bounce-averaging inner loops, allocated once in compute_bounce_data and reused across every λ. Sizes are fixed for a flux surface (ntheta sub-grid points, mpert Fourier modes). Buffers the loops populate only partially are fill!-reset per λ.
Fields
g_wb::Vector{Float64}: lengthntheta— bounce-action integrand samplesg_wd::Vector{Float64}: lengthntheta— drift integrand samplescum_wb_arr::Vector{Float64}: lengthntheta— cumulative bounce-action integraljvtheta::Vector{ComplexF64}: lengthntheta— action integrandbj_samples::Vector{ComplexF64}: lengthntheta— action bounce-integral sampleswsamp::Vector{ComplexF64}: lengthntheta— per-mode W bounce-integral sampleswmu_mt::Matrix{ComplexF64}:mpert × ntheta— W_μ per θwen_mt::Matrix{ComplexF64}:mpert × ntheta— W_E per θexpm::Vector{ComplexF64}: lengthmpert— Fourier basis at a θpl::Vector{ComplexF64}: lengthntheta— bounce phase factorwmu_ba::Vector{ComplexF64}: lengthmpert— bounce-averaged W_μwen_ba::Vector{ComplexF64}: lengthmpert— bounce-averaged W_Ewmats_lmda::Vector{ComplexF64}: lengthnqty_matrix(mpert)— packed W outer productstspl_f::Vector{Float64}: length 5 — in-place tspl(θ) evaluationint_w::Vector{Float64},cumint_W::Matrix{Float64}: precomputed exact-cubic quadrature weights on the fixed unit θ-grid (∫ = int_w·y,cumulative = cumint_W·y); shared read-only across surfaces, see_quadrature_weights
GeneralizedPerturbedEquilibrium.KineticForces.EnergyIntegrationResult — Type
EnergyIntegrationResultResults from a single energy-space integration at one (ψ, λ, ℓ) point. Trajectory fields are only populated when ctrl.save_records=true.
GeneralizedPerturbedEquilibrium.KineticForces.EnergyParams — Type
EnergyParamsParameters for the energy integrand evaluation.
GeneralizedPerturbedEquilibrium.KineticForces.IonSpecies — Type
IonSpecies(; z, m, fraction=NaN, density="")One main-ion species in a multi-ion NTV run. z/m are the charge (e) and mass (proton masses). The density is given by exactly one of fraction or density, which select a fraction of the total n_i profile or an explicit per-species profile in the kinetic file.
GeneralizedPerturbedEquilibrium.KineticForces.KineticForcesControl — Type
KineticForcesControlUser-facing control parameters from the TOML [KineticForces] section. Configures which NTV methods to run, species parameters, tolerances, and output options.
Constructed via keyword arguments or from a TOML dict:
ctrl = KineticForcesControl(; (Symbol(k) => v for (k, v) in inputs["KineticForces"])...)Immutable: vary a field by building a new control rather than assigning to one (the multi-species loop does this per species, and check_psi_quadrature_convergence's test builds a second control for its differing tolerance).
GeneralizedPerturbedEquilibrium.KineticForces.KineticForcesInternal — Type
KineticForcesInternalInternal working state for KineticForces calculations. Holds equilibrium-derived quantities, profile interpolants, and integration results.
Fields replacing former module-level globals:
ro,bo,chi1: Equilibrium geometry parametersmthsurf,mfac: Poloidal grid infodbob_m,divx_m: Perturbation mode interpolantssing_psis: Rational-surface ψ locations (sorted, from the stability analysis), used as panel boundaries for the outer ψ torque quadrature so the resonant peaks fall on Gauss-Kronrod interval endpoints instead of driving deep adaptive bisection
Equilibrium and kinetic profile data are read directly from the PlasmaEquilibrium (equil.profiles, equil.geometry) and the externally-loaded KineticProfileSplines — no shadow copies are kept on this struct.
GeneralizedPerturbedEquilibrium.KineticForces.KineticForcesInternal — Method
KineticForcesInternal(equil; verbose=false)Construct KineticForcesInternal from a PlasmaEquilibrium, extracting the equilibrium geometry parameters needed for NTV calculations.
GeneralizedPerturbedEquilibrium.KineticForces.KineticForcesState — Type
KineticForcesStateAccumulated results from all KineticForces computations. Written to gpec.h5 under the "KineticForces" group.
GeneralizedPerturbedEquilibrium.KineticForces.MethodResult — Type
MethodResultResults for one NTV computation method across all flux surfaces.
GeneralizedPerturbedEquilibrium.KineticForces.PitchGARParams — Type
PitchGARParamsParameters for the GAR pitch-angle integrand. Uses a unified fbnce interpolant that returns [ωb, ωd, f₁, f₂, ...] at each λ, matching Fortran lambdaintgrl_lsode.
GeneralizedPerturbedEquilibrium.KineticForces._bounce_integrate — Method
_bounce_integrate(...)Perform bounce integrals over θ sub-grid. Computes ωbbar, ωdbar, |δJ|², and optionally W matrix outer products. Ports Fortran torque.F90 lines 674-793.
GeneralizedPerturbedEquilibrium.KineticForces._build_lambda_grid — Method
Build λ grid based on method character (f/t/p). Returns (lambda, dlambda) with endpoints excluded.
GeneralizedPerturbedEquilibrium.KineticForces._energy_collision_frequency — Method
_energy_collision_frequency(x::Float64, p::EnergyParams) → Float64Collision frequency ν(x) at normalized energy x = E/T.
"zero": collisionless (ν = 0)"small": 1e-5 * we"krook": unmodified Krook operator"harmonic": (1 + (l/2)²) * krook * x^(-3/2)
GeneralizedPerturbedEquilibrium.KineticForces._energy_integrand_real — Method
_energy_integrand_real(x::Float64, p::EnergyParams) → ComplexF64Physical energy integrand in x-space, N(x)·exp(-x)/denom(x), evaluated on the real axis. Used both by the production integral (integrate_energy via _integrate_energy_resonant) and for diagnostics (evaluate_energy_integrand).
GeneralizedPerturbedEquilibrium.KineticForces._energy_numerator — Method
_energy_numerator(x::Float64, p::EnergyParams) → ComplexF64Numerator N(x) of the energy integrand, without the resonance denominator and without the Maxwellian weight exp(-x). The physical x-space integrand is N(x)·exp(-x)/denom(x); the residue at a pole uses N(xres)·exp(-xres).
For CGL there is no resonance denominator: N_cgl = x^2.5 / (i·n).
GeneralizedPerturbedEquilibrium.KineticForces._energy_numerator_deriv — Method
_energy_numerator_deriv(x::Float64, p::EnergyParams) → ComplexF64x-derivative dN/dx of _energy_numerator, used for the analytic regular-part limit at a real-axis (ν=0) resonance pole. CGL is excluded — the real-pole path never carries a CGL numerator (CGL has no pole).
GeneralizedPerturbedEquilibrium.KineticForces._find_bounce_points_and_grid — Method
Find bounce points for trapped/passing particles and build θ sub-grid. Returns (t1, t2, thetapoints, thetaweights).
GeneralizedPerturbedEquilibrium.KineticForces._find_deepest_well — Method
Find the deepest potential well (largest midpoint v_par) among bounce-point pairs, handling pairs that wrap through θ = 0/1.
GeneralizedPerturbedEquilibrium.KineticForces._full_idx — Method
Full-block index (column-major) within a non-Hermitian block.
GeneralizedPerturbedEquilibrium.KineticForces._jbb_deweight! — Method
_jbb_deweight!(out, jbb_modes, ft, psi, equil, mthsurf, theta_buf)JBB deweighting step: inverse DFT → divide by J(ψ,θ)·B(ψ,θ)² → forward DFT.
Matches Fortran set_peq lines 859-868: transforms JBB-weighted m-space data to θ-space, removes the J·B² weighting at each poloidal angle, and transforms back.
GeneralizedPerturbedEquilibrium.KineticForces._pitch_gar_kernel_quadgk! — Method
_pitch_gar_kernel_quadgk!(out::Vector{ComplexF64}, lambda, p::PitchGARParams)In-place complex-valued kernel for quadgk!. Writes out[i] = fvals[i+2] * xint_decomposed for i in 1..nqty. QuadGK natively handles ComplexF64.
GeneralizedPerturbedEquilibrium.KineticForces._pitch_gar_kernel_quadgk_wt! — Method
_pitch_gar_kernel_quadgk_wt!(out::Vector{ComplexF64}, lambda, p::PitchGARParams)Dual-output pitch kernel. Fills a length-2*nqty buffer: out[1:nqty] — fwmm half: fvals * complex(0, imag(xint)) out[nqty+1:2*nqty] — ftmm half: fvals * complex(real(xint), 0)
One energy integration per λ; both halves share it.
GeneralizedPerturbedEquilibrium.KineticForces._powspace_antideriv — Method
_powspace_antideriv(x, pow)Analytic antiderivative of |(1-x²)|^pow for pow 1-9. Matches Fortran powspace_sub cases exactly.
GeneralizedPerturbedEquilibrium.KineticForces._quadrature_weights — Method
_quadrature_weights(ntheta) → (int_w, cumint_W)Exact integral of the CubicFit-endpoint spline on the fixed grid range(0,1,ntheta) is a constant linear functional of the node samples, so ∫ = int_w·y and the cumulative integral is cumint_W·y. The weights are obtained once per ntheta by evaluating the public integrate / cumulative_integrate! on the unit basis vectors — bit-faithful to fitting and integrating each sample vector directly, but reducing the per-λ hot loop to a dot/mul!. Cached (build guarded by a lock); the returned arrays are read-only.
GeneralizedPerturbedEquilibrium.KineticForces._real_pole_regular_part — Method
_real_pole_regular_part(xr, p, leff, wb, n, wd) → ComplexF64Laurent regular part (finite limit at x → x_res) of the pole-subtracted real-axis (ν=0) integrand N(x)·exp(-x)/(i·Ω(x)) − R/(x − x_res), with Ω(x) = leff·wb·√x + n·(we+wd·x) and R = N(x_res)·exp(-x_res)/(i·Ω′).
Writing h(x) = N(x)·exp(-x), the limit is [h′(x_res) − h(x_res)·Ω″/(2Ω′)] / (i·Ω′), with h′ = (N′ − N)·exp(-x), Ω′ = leff·wb/(2√x) + n·wd, Ω″ = −leff·wb/(4·x^{3/2}).
GeneralizedPerturbedEquilibrium.KineticForces._resonance_nodes_from_frequencies — Method
_resonance_nodes_from_frequencies(wbhat_f, welec_f, wdhat_f, grid; n, nl, xeval=2.5) → Vector{Float64}Scan grid for the zeros of the resonance operator Ω_ℓ(x; ψ) = ℓ·ω_b(ψ)·√x + n·(ω_E(ψ) + ω_d(ψ)·x) at energy x = xeval, for every bounce harmonic ℓ ∈ −nl:nl, given per-ψ frequency callables. xeval defaults to 2.5 — the peak of the Maxwellian-weighted drive x^2.5·e^−x, where the resonance overlaps the most particles, so the located ψ surfaces sit on the NTV torque-density peaks (on the DIII-D case the x=2.5 nodes land ~3× closer to the measured dT/dψ spikes than the thermal-energy x=1 estimate). Roots from all harmonics are concatenated (deduplication against coincident surfaces happens in psi_panel_points).
GeneralizedPerturbedEquilibrium.KineticForces._setup_surface_state — Method
_setup_surface_state(psi, n, l, zi, mi, wdfac, electron,
equil, intr, kinetic_profiles) → NamedTuplePrivate helper for the calculated-matrix path. Reproduces the per-surface setup in tpsi! (theta-grid sampling, bounce-extremum finding, flux-function evaluation, diamagnetic/drift frequencies) without any of the perturbation- dependent bookkeeping or method dispatch.
This keeps compute_kinetic_matrices_at_psi! structurally independent of tpsi! so the matrix-only path can be evolved (e.g. QuadGK pitch in Phase C) without perturbing the perturbative torque pipeline.
GeneralizedPerturbedEquilibrium.KineticForces._tri_idx — Method
Upper-triangle index (1 ≤ i ≤ j ≤ mpert) within a triangular block.
GeneralizedPerturbedEquilibrium.KineticForces._vpar_from_extrap — Method
Parallel-velocity factor v_par = 1 − (λ/bo)·B(θ) from the endpoint-fit cubic of B (B_extrap, built where the surface interpolants are constructed), keeping v_par consistent with the bounce-point roots as in Fortran's vspl.
GeneralizedPerturbedEquilibrium.KineticForces.calculate_clar — Method
calculate_clar(psi, n, l, q, epsr, wdian, wdiat, welec, nuk, bo,
bmax, bmin, n_s, T_s, mass, chrg, tspl, dbob_m_f, divx_m_f,
divxfac, wdfac)::ComplexF64Calculate CLAR (Circular Large Aspect Ratio) torque. Uses pitch-angle resolved calculations for trapped and passing particles. Includes bounce-averaged integrals over lambda (pitch angle).
Status: Partially implemented (stub for full calculation)
GeneralizedPerturbedEquilibrium.KineticForces.calculate_fcgl — Method
calculate_fcgl(psi, n, l, tspl, dbob_m_f, divx_m_f, divxfac, n_s, T_s,
equil, intr)::ComplexF64Calculate FCGL (Full Circular Gyrokinetic Landau) torque. Implements simplified energy balance equation. Only valid for bounce harmonic l=0.
Based on: [Logan et al., Phys. Plasmas 2013]
GeneralizedPerturbedEquilibrium.KineticForces.calculate_gar — Method
calculate_gar(psi, n, l, q, epsr, wdian, wdiat, welec, nuk, bo, bmax,
bmin, n_s, T_s, mass, chrg, tspl, dbob_m_f, divx_m_f,
divxfac, wdfac, method, op_wmats; kwargs...)::ComplexF64Calculate GAR (General Aspect Ratio) torque. Fully general method without aspect ratio expansion. Handles variants: FGAR (full), TGAR (trapped), PGAR (passing). Can compute torque (TMM), energy (WMM), or matrix elements (KMM/RMM).
Ports Fortran torque.F90 GAR branch (lines 529-932).
Steps
- Compute bounce-averaged quantities via
compute_bounce_data() - Build fbnce interpolant over λ, normalize for numerical stability
- Integrate over pitch angle via
integrate_pitch_gar_quadgk() - Apply torque normalization (Eq. 19, Logan et al. 2013)
- If matrix path: assemble and normalize kinetic matrices
Keyword Arguments (rex/imx override)
rex_override::Union{Nothing,Float64}: Override real-part multiplier for resonance operator. When both overrides are provided, bypasses method-string derivation.imx_override::Union{Nothing,Float64}: Override imaginary-part multiplier. Userex_override=1.0, imx_override=1.0to get full complex result for simultaneous kwmat/ktmat extraction viacompute_kinetic_matrices_at_psi!.
Reference: [Logan et al., Phys. Plasmas 20, 122507 (2013)]
GeneralizedPerturbedEquilibrium.KineticForces.calculate_rlar — Function
calculate_rlar(psi, n, l, q, epsr, wdian, wdiat, welec, wdhat, wbhat,
nueff, dVdpsi, n_s, T_s, dbob_m_f, bo, bmin)::ComplexF64Calculate RLAR (Reduced Large Aspect Ratio) torque. Uses energy space integration with pitch angle averaging. Valid for low aspect ratio tokamaks (ε << 1).
Reference: [Logan et al., Phys. Plasmas, 2013]
GeneralizedPerturbedEquilibrium.KineticForces.check_psi_quadrature_convergence — Method
check_psi_quadrature_convergence(total, quad_err, ctrl, method)Warn when the ψ torque quadrature terminated without satisfying the requested tolerances (hit maxevals_psi), or when a nonzero user-set atol_psi dominated termination — the silent-garbage scenario for weak applied fields, since NTV scales as δB².
GeneralizedPerturbedEquilibrium.KineticForces.combine_species_states — Method
combine_species_states(states) -> KineticForcesStateSum per-species KineticForcesState results into a single total (τ = Σs τs). Per method: the scalar total_torque/total_energy are summed exactly; the dT/dψ profile is summed by linearly interpolating each species' own (psi_grid, dtdpsi) arrays onto the sorted union of the species ψ grids (zero outside a species' range), and the cumulative T(ψ) is re-integrated (trapezoid) from that summed profile. All species share the same ψ-integration range (ctrl.psilims), so the grids differ only in adaptive nodes. The interpolated/trapezoid t_cumulative is a diagnostic profile — its endpoint need not equal the exactly-summed Gauss-Kronrod total_torque, especially near sharp resonances. The combined MethodResult carries only the summed scalars and profile; per-species diagnostics (torque_profile, records, panel_psis, resonance_psis) are not aggregated and are left at their defaults.
GeneralizedPerturbedEquilibrium.KineticForces.compute_bounce_data — Method
compute_bounce_data(psi, n, l, q, bo, bmax, bmin, theta_bmax,
tspl, B_extrap, mfac, chi1, ro, dbob_m_f, divx_m_f,
divxfac, wdfac, mass, chrg, T_s, method;
nlmda=128, ntheta=128,
smat=nothing, tmat=nothing, xmat=nothing,
ymat=nothing, zmat=nothing) → BounceDataCompute bounce-averaged quantities as functions of pitch angle λ. This is the core function that sets up all λ-dependent quantities needed by the pitch-angle quadrature.
Ports Fortran torque.F90 lines 530-816 (GAR branch).
Arguments
psi: Normalized poloidal fluxn: Toroidal mode numberl: Bounce harmonic numberq: Safety factor at this ψbo: On-axis toroidal field [T]bmax, bmin: Max/min of B(θ) at this ψtheta_bmax: θ location of Bmax (nodal knot; the passing-transit start)tspl: Periodic poloidal interpolant: tspl(θ) → [B, dB/dψ, dB/dθ, J, dJ/dψ]B_extrap: Endpoint-fit (non-periodic) cubic of B(θ) used for v_par and the bounce-point roots (the Fortranvsplequivalent)mfac: Poloidal mode numbers [mlow:mhigh]chi1: 2π·ψ₀ flux normalizationro: Major radius [m]dbob_m_f: δB/B Fourier modes at this ψ (ComplexF64 vector, length mpert)divx_m_f: ∇·ξ⊥ Fourier modes at this ψ (ComplexF64 vector, length mpert)divxfac, wdfac: Scaling factorsmass: Particle mass [kg]chrg: Particle charge [C]T_s: Species temperature at this ψ [J]method: Method string (first char: f/t/p determines λ range)
Keyword Arguments
nlmda: Number of pitch angle grid points (default 128, matching Fortran pentrc nlmda)ntheta: Number of poloidal grid points per bounce (default 128)smat, tmat, xmat, ymat, zmat: Geometric matrices (mpert×mpert) for kinetic matrix path
GeneralizedPerturbedEquilibrium.KineticForces.compute_calculated_kinetic_matrices — Method
compute_calculated_kinetic_matrices(ffs_ctrl, equil, ffs_intr, metric, ffit;
kf_ctrl=KineticForcesControl(),
kinetic_profiles)
→ (kw_flat, kt_flat)Drive the KineticForces matrix kernel over the ψ grid stored in metric.xs and return (kw_flat, kt_flat) arrays of shape (mpsi, np^2, 6) matching the contract that ForceFreeStates._compute_fkg_matrices! consumes.
The arrays carry the six bounce-averaged kinetic energy / torque matrices (Logan 2015 Eqs 7.30–7.35) for every ψ on the equilibrium grid, packed as block-diagonal matrices over toroidal mode number n ∈ [nlow, nhigh] and flattened to (np = mpert·npert)².
This routine reads equilibrium-derived profiles (q, dV/dψ, ⟨r⟩, ⟨R⟩) directly from named splines on equil.profiles and equil.geometry, and kinetic profiles (n, T, ωE, ν) from the `kineticprofilesargument, avoiding the former shadow-copy pattern in KineticForcesInternal. The perturbation-mode interpolants (kfintr.dbobm,kfintr.divxm`) remain unwired and are tracked as follow-up work blocked on PR #196 — see the plan's "Out of scope" section.
Arguments
ffs_ctrl: ForceFreeStatesControl (carrieskinetic_factor,kinetic_source)equil: PlasmaEquilibrium with 2D interpolants and named profile/geometry splinesffs_intr: ForceFreeStatesInternal (mode indexing)metric: MetricData (provides ψ grid viametric.xs)ffit: FourFitVars (used only fornumpert_totalcross-check)
Keyword arguments
kf_ctrl: KineticForcesControl, defaults toKineticForcesControl(). Used to carry NTV-specific knobs (nl, zi, mi, wdfac, divxfac, electron) that the KineticForces kernel needs but ForceFreeStatesControl does not expose.kinetic_profiles::Equilibrium.KineticProfileSplines: Required. Named kinetic- profile splines loaded viaEquilibrium.load_kinetic_profiles.
Returns
kw_flat::Array{ComplexF64,3}: Energy matrices, shape(mpsi, np^2, 6)kt_flat::Array{ComplexF64,3}: Torque matrices, shape(mpsi, np^2, 6)
GeneralizedPerturbedEquilibrium.KineticForces.compute_kinetic_matrices_at_psi! — Method
compute_kinetic_matrices_at_psi!(kwmat, ktmat, psi, n, l, zi, mi,
wdfac, divxfac, electron, equil, intr, kinetic_profiles)Compute the six kinetic Euler-Lagrange coefficient matrices at a single flux surface and split them into kwmat and ktmat in the convention used by the DCON matrix-assembly path (Logan 2015 Eqs 7.30–7.35).
Rather than running two integrations like the reference Fortran PENTRC (one with rex=0, imx=1 for kwmat and another with rex=1, imx=0 for ktmat), this path integrates once with rex=imx=1 to get the full complex response, then decomposes by real/imag parts — equivalent math at half the work. After the -i/(2n) normalization inside kinetic_energy_matrices_for_euler_lagrange!, the full complex response splits cleanly:
kwmat← fwmm half (Fortran rex=0, imx=1 pass)ktmat← ftmm half (Fortran rex=1, imx=0 pass)
Each half is complex (not pure real / pure imag), matching Fortran's two independent integration passes at torque.F90:842-847. Per-surface matrix dumps confirm element-by-element agreement with Fortran fourfit.F:1080-1082 (kwmat_l, ktmat_l). This is the Fortran convention required by the adjoint combinations kwmat ± ktmat in ForceFreeStates/Kinetic.jl / Fortran dcon/sing.f:967-1075 for non-Hermitian Bk, Ck, E_k.
Arguments
kwmat::Array{ComplexF64,3}: Output (mpert×mpert×6), fwmm half, zeroed on entryktmat::Array{ComplexF64,3}: Output (mpert×mpert×6), ftmm half, zeroed on entrypsi, n, l, zi, mi, wdfac, divxfac, electron: Same astpsi!(divxfac unused on the matrix path — retained for call-site compatibility)equil: PlasmaEquilibriumintr::KineticForcesInternal: Internal state with mode indexing, geometric matrices (smats/tmats/xmats/ymats/zmats), and per-surface θ-grid bufferskinetic_profiles::Equilibrium.KineticProfileSplines: Named kinetic-profile splines
Reference: [Logan et al., Phys. Plasmas 20, 122507 (2013)]
GeneralizedPerturbedEquilibrium.KineticForces.compute_torque_all_methods! — Method
compute_torque_all_methods!(state::KineticForcesState, intr::KineticForcesInternal,
ctrl::KineticForcesControl, equil, kinetic_profiles)Calculate torque/energy for all enabled methods. For each method, integrates over flux surfaces using adaptive QuadGK quadrature via integrate_psi_quadgk. For multi-n calculations, loops over toroidal mode numbers and assembles block-diagonal kinetic matrices.
Arguments
state::KineticForcesState: Accumulates results for all methodsintr::KineticForcesInternal: Internal state with equilibrium datactrl::KineticForcesControl: Control parameters specifying which methods to runequil: PlasmaEquilibrium with 2D interpolantskinetic_profiles::Equilibrium.KineticProfileSplines: Named kinetic-profile splines
GeneralizedPerturbedEquilibrium.KineticForces.energy_integrand_scalar — Method
energy_integrand_scalar(x::Float64, p::EnergyParams) → ComplexF64Evaluate the physical energy integrand N(x)·exp(-x)/denom(x) at normalized energy x = E/T. Implements [Logan, Park, et al., Phys. Plasmas, 2013] Eq. (8).
GeneralizedPerturbedEquilibrium.KineticForces.evaluate_energy_integrand — Method
evaluate_energy_integrand(x_grid; wn, wt, we, wd, wb, nuk, leff, n,
nutype="harmonic", f0type="maxwellian",
nufac=1.0, ximag=0.0, qt=false) → Vector{ComplexF64}Diagnostic convenience: evaluate the physical x-space energy integrand N(x)·exp(-x)/denom(x) at specified x = E/T values. Returns the integrand value (not the integral) at each point in x_grid. Useful for plotting the energy integrand shape and verifying kinetic resonance resolution.
Example
x = 10 .^ range(-2, stop=2, length=500)
f = KineticForces.evaluate_energy_integrand(x; wn=1e3, wt=2e3, we=5e4,
wd=1e2, wb=3e4, nuk=1e3, leff=1.0, n=1)
plot(x, real.(f); xscale=:log10, xlabel="x = E/T", ylabel="Re(integrand)")GeneralizedPerturbedEquilibrium.KineticForces.find_resonance_energies — Method
find_resonance_energies(leff, wb, n, we, wd) → Vector{Float64}Real positive energies x_res where the resonance condition vanishes:
Ω(x) = leff·wb·√x + n·(we + wd·x) = 0With s = √x this is the quadratic n·wd·s² + leff·wb·s + n·we = 0. Returns the x = s² values for the positive real roots (the locations of the resonance poles of the energy integrand).
GeneralizedPerturbedEquilibrium.KineticForces.find_sign_change_roots — Method
find_sign_change_roots(f, grid) → Vector{Float64}Locate the zeros of a callable f by scanning consecutive grid nodes for strict sign changes (f(x[i])·f(x[i+1]) < 0) and refining each bracket with Roots.Brent. Returns the refined roots in grid order (empty if f never changes sign; a node value of exactly zero is not treated as a crossing).
Single source of truth for the scan-then-Brent idiom — used for dB/dθ extrema in the bounce averaging and for kinetic-resonance surfaces in the ψ quadrature paneling.
GeneralizedPerturbedEquilibrium.KineticForces.integrate_energy — Method
integrate_energy(wn, wt, we, wd, wb, nuk, ell, leff, n, psi, lambda, method;
nutype="harmonic", f0type="maxwellian", nufac=1.0,
ximag=0.0, qt=false, atol=1e-7, rtol=1e-5) → ComplexF64Integrate the kinetic resonance operator over normalized energy x = E/T.
The integral ∫₀^∞ N(x)·exp(-x)/denom(x) dx is evaluated in real x-space over [0, X_ENERGY_MAX] (the integrand and its poles decay as x^p·exp(-x), so the tail there is far below any tolerance) via _integrate_energy_resonant. Each resonance pole (root of Ω(x) = leff·wb·√x + n·(we + wd·x), shifted off the real axis by collisions to xpole = xres - i·ν/Ω′) is removed by subtracting its singular part R/(x - xpole) and adding back the analytic principal-value + residue. A single formula handles all collisionalities: the collisionless case (ν ≡ 0) is the exact ν→0 limit, with its real-axis pole resolved analytically (see `integrateenergyresonant`).
Collision operator types (nutype): "zero", "small", "krook", "harmonic". Distribution function types (f0type): "maxwellian", "jkp", "cgl".
ximag is accepted for backward compatibility but no longer used — resonance poles are now handled analytically rather than by contour deformation.
Returns
ComplexF64: energy integral value
GeneralizedPerturbedEquilibrium.KineticForces.integrate_pitch_gar_quadgk — Method
integrate_pitch_gar_quadgk(wn, wt, we, nuk, bobmax, epsr, q, fbnce, fbnce_norm,
nqty, ell, n, rex, imx, psi, method; ...) → Vector{ComplexF64}Integrate the kinetic resonance operator over pitch angle λ using adaptive Gauss-Kronrod quadrature. Uses QuadGK.quadgk! with an in-place ComplexF64 kernel buffer.
The fbnce interpolant returns [ωb, ωd, f₁, f₂, ...] at each λ, where:
- f₁ = ωb|δJ|²/ro² (scalar torque)
- f₂:end = ωb·Wouterproducts/ro² (kinetic matrix elements, if present)
Splits the domain at the trapped/passing boundary so Gauss-Kronrod resolves the kink in leff = ell + n*q (circulating) → ell (trapped). One quadgk! call writes all nqty complex quantities per λ-evaluation.
Returns
Vector{ComplexF64}of length nqty: integrated pitch-angle results
GeneralizedPerturbedEquilibrium.KineticForces.integrate_pitch_gar_quadgk_wt — Method
integrate_pitch_gar_quadgk_wt(wn, wt, we, nuk, bobmax, epsr, q, fbnce, fbnce_norm,
nqty, ell, n, psi, method; ...) → Vector{ComplexF64}Dual-output variant for the kinetic-matrix path. Emits both the wmm half (rex=0, imx=1 → Fortran kwmat) and the tmm half (rex=1, imx=0 → Fortran ktmat) in a single pitch integration, sharing one energy integration per (λ, E).
Returns a length-2*nqty packed buffer: [wmm | tmm]. The two halves each reproduce Fortran's independent-pass result at Fortran's element-by-element convention (verified via matrix-dump comparison vs Fortran dcon/fourfit.F kwmat_l/ktmat_l). Downstream kwmat ± ktmat combinations in ForceFreeStates/Kinetic.jl then reproduce sing.f:967-1075 exactly for the non-Hermitian Bk, Ck, E_k diagonals.
GeneralizedPerturbedEquilibrium.KineticForces.integrate_psi_quadgk — Method
integrate_psi_quadgk(n, nl, zi, mi, wdfac, divxfac, electron, method,
equil, intr, ctrl, kinetic_profiles; psi_min, psi_max) → NamedTupleIntegrate torque over ψ using adaptive Gauss-Kronrod quadrature with QuadGK.BatchIntegrand. Every integrand evaluation is logged, giving a diagnostic T(ψ) profile at no extra cost (the values are computed anyway — we just keep them).
Returns
NamedTuple with:
total::ComplexF64: Total integrated torquetorque_profile: NamedTuple of (psi, dtdpsi, t_cumulative) from evaluation pointsmatrix_integrated: Trapezoidal-integrated mpert×mpert×6 matrix (if matrix method)psi_nsteps::Int: Number of integrand evaluationspsi_quad_error::Float64: Quadrature error estimate for the total torquepanel_psis::Vector{Float64}: Quadrature panel boundaries actually used (bounds + interior resonant surfaces)resonance_psis::Vector{Float64}: Located kinetic-resonance ψ surfaces (Ω_ℓ(x=1)=0), for diagnostics/plotting
The integral is paneled at the rational-surface ψ locations (intr.sing_psis) and capped at ctrl.maxevals_psi evaluations; a warning is emitted if the quadrature fails to reach ctrl.rtol_psi/ctrl.atol_psi or if a nonzero atol_psi dominates termination.
GeneralizedPerturbedEquilibrium.KineticForces.kinetic_energy_matrices_for_euler_lagrange! — Method
kinetic_energy_matrices_for_euler_lagrange!(kwmat, ktmat, state, psi, n, l, wdfac, intr;
kwargs...) → nothingCompute the six kinetic Euler-Lagrange coefficient matrices of Logan 2015 Eqs 7.30–7.35 (Ak, Bk, Ck, Dk, Ek, Hk) at a single (ψ, n, ℓ) and write them into pre-allocated kwmat[mpert, mpert, 6] (fwmm half, Fortran rex=0, imx=1) and ktmat[mpert, mpert, 6] (ftmm half, rex=1, imx=0).
Matrix-only path (no scalar torque slot in the pitch-angle buffer), so nqty = mpert²·6 instead of 1 + mpert²·6.
Uses integrate_pitch_gar_quadgk_wt to emit both halves from a single energy integration per (λ, E), reproducing Fortran's two-pass semantics (torque.F90:842-847). Per-surface matrix dumps confirm element-by-element match against Fortran fourfit.F:1080-1082 (kwmat_l, ktmat_l).
For the Hermitian-outer-product blocks A/D/H stored as upper-triangles, the mirror rule differs between halves: kwmat[j,i] = conj(kwmat[i,j]) — Hermitian (Sw pure imaginary) ktmat[j,i] = -conj(ktmat[i,j]) — anti-Hermitian (St pure real) Derivation: conj(S_w) = -S_w vs conj(S_t) = S_t, combined with conj(factor) = -factor (factor = -i/(2n)). These mirrors recover Fortran's independent-slot computation at the mirrored (j,i) positions.
GeneralizedPerturbedEquilibrium.KineticForces.kinetic_resonance_psi_nodes — Method
kinetic_resonance_psi_nodes(kinetic_profiles, equil; n, nl, zi=1, mi=2, electron=false, wdfac=1.0, xeval=2.5) → Vector{Float64}ψ_N locations of kinetic resonance surfaces, for use as ψ-quadrature panel boundaries (and, per the kinetic-aware grid-packing plan, as mandatory equilibrium knots).
Locates the zeros of the trapped-branch (leff = ℓ) resonance denominator Ω(x) = leff·ω_b·√x + n·(ω_E + ω_d·x) at energy x = xeval, for every bounce harmonic ℓ ∈ −nl:nl — this is where the energy-space resonance sweeps through the drive-weighted bulk and the NTV torque density peaks (Logan & Park, Phys. Plasmas 20, 122507 (2013), §IV–V). xeval defaults to 2.5, the peak of the Maxwellian-weighted drive x^2.5·e^−x; on the DIII-D case those nodes sit ~3× closer to the measured dT/dψ spikes than the thermal x=1 estimate. The ℓ = 0 node is the ω_d-shifted ExB (superbanana-plateau) resonance.
The frequencies use the pitch-averaged large-aspect-ratio closed forms of the rlar method (tpsi! in Torque.jl / Fortran pentrc torque.F90): ω_b = (π/4)·√(ε/2)·wtran and ω_d = q·T_s/(2·ε·R₀²·Z·e·B₀)·wdfac, with ε = ⟨r⟩/⟨R⟩ clamped away from the axis where the estimate degenerates. Panel placement only needs ~peak-width accuracy, so these cheap estimates (single spline evaluations) are sufficient and no bounce averaging is performed.
GeneralizedPerturbedEquilibrium.KineticForces.method_kind — Method
method_kind(name) -> SymbolReturn the dispatch routing tag (:gar, :fcgl, :rlar, :clar) for the NTV method name, looked up from [METHOD_REGISTRY]. Errors on an unknown method.
GeneralizedPerturbedEquilibrium.KineticForces.nqty_matrix — Method
Number of packed complex entries per λ for the 6 kinetic matrices.
GeneralizedPerturbedEquilibrium.KineticForces.powspace — Method
powspace(xmin, xmax, pow, num, endpoints) → (points, weights)Generate a grid with power-law concentration near endpoints. Port of Fortran powspace_sub from equil/grid.f90.
Arguments
xmin, xmax: Grid boundspow::Int: Power of grid concentration (higher = more refined near edges)num::Int: Number of grid pointsendpoints::String: Where to concentrate: "lower", "upper", or "both"
Returns
points::Vector{Float64}: Grid point locationsweights::Vector{Float64}: Derivatives dx/dnorm (integration weights)
GeneralizedPerturbedEquilibrium.KineticForces.print_summary — Method
print_summary(state::KineticForcesState; verbose::Bool=false)Print a summary of KineticForces results to stdout.
Arguments
state::KineticForcesState: Accumulated computation resultsverbose::Bool: Print detailed per-surface results
GeneralizedPerturbedEquilibrium.KineticForces.psi_panel_points — Method
psi_panel_points(interior, x0, xout) → Vector{Float64}Build the ψ-quadrature node list [x0, interior points strictly inside (x0, xout), xout]. interior is the raw union of resonant-surface locations (rational surfaces ∪ kinetic resonances); this function owns the ordering: sort, drop near-duplicates (closer than PANEL_MERGE_ATOL, e.g. a kinetic resonance coinciding with a rational), and drop points within PANEL_MERGE_ATOL of a bound to avoid degenerate panels. Paneling the integral at these surfaces puts the resonant torque-density peaks (reg_spot/collisionally broadened, but narrow in ψ) on Gauss-Kronrod interval endpoints, which the rule handles natively instead of hunting them by adaptive bisection.
GeneralizedPerturbedEquilibrium.KineticForces.set_perturbation_data! — Method
set_perturbation_data!(kf_intr, pe_state, ffs, equil, metric)Populate perturbation data from PerturbedEquilibriumState into KineticForcesInternal.
Builds three interpolant sets from PE Clebsch displacements:
xs_m— [ξ^ψ, ∂ξ^ψ/∂ψ, ξ^α] CubicSeriesInterpolants over ψdbob_m— δB/B Fourier modes via JBB deweighting (Fortran set_peq)divx_m— ∇·ξ⊥ Fourier modes via JBB deweighting
The JBB deweighting algorithm (Fortran pentrc/inputs.f90:828-868):
- Apply geometric matrices S,T,X,Y,Z in m-space
- Inverse DFT to θ-space
- Divide by J·B² at each θ
- Forward DFT back to m-space
GeneralizedPerturbedEquilibrium.KineticForces.tpsi! — Method
tpsi!(tpsi_var, psi, n, l, zi, mi, wdfac, divxfac, electron, method, equil, intr,
kinetic_profiles; op_wmats=nothing)Toroidal torque resulting from nonambipolar transport in perturbed equilibrium. Imaginary component is proportional to the kinetic energy Im(T) = 2ndW_k.
Arguments
tpsi_var: Output complex torque valuepsi::Float64: Normalized poloidal fluxn::Int: Toroidal mode numberl::Int: Bounce harmonic numberzi::Int: Ion charge in fundamental units (e)mi::Int: Ion mass (units of proton mass)wdfac::Float64: Drift factordivxfac::Float64: Divergence factorelectron::Bool: Calculate quantities for electrons (zi,mi ignored)method::String: Integration method (RLAR, CLAR, *GAR, *TMM, *WMM, *KMM) where * = F,T,P for full,trapped,passingequil: PlasmaEquilibrium with 2D interpolants and named profile/geometry splinesintr::KineticForcesInternal: Internal state with mode indexing and perturbation splineskinetic_profiles::Equilibrium.KineticProfileSplines: Named kinetic-profile splines (ni, ne, Ti, Te, ωE, νi, ν_e) loaded fromkinetic.dat
Optional Arguments
op_wmats::Array{ComplexF64,3}: Store ForceFreeStates matrix elements
Returns
ComplexF64: Toroidal torque due to nonambipolar transport
GeneralizedPerturbedEquilibrium.KineticForces.write_integration_records! — Method
write_integration_records!(mg::HDF5.Group, records::Vector{EnergyIntegrationResult})Write variable-length integration trajectory records using offset-indexed concatenated arrays. This is the standard HDF5 ragged array pattern for storing variable-length data.
Arguments
mg::HDF5.Group: HDF5 group for this methodrecords::Vector{EnergyIntegrationResult}: Integration records to write
GeneralizedPerturbedEquilibrium.KineticForces.write_to_hdf5! — Method
write_to_hdf5!(h5file::HDF5.File, state::KineticForcesState; dVdpsi_spline=nothing,
species_label=nothing)Write KineticForces results to the "KineticForces" group in gpec.h5.
Arguments
h5file::HDF5.File: Open HDF5 file handlestate::KineticForcesState: Accumulated computation resultsdVdpsi_spline: Optional dV/dψN profile interpolant; when given, dV/dψN is written at the quadrature points so the torque density dT/dV = (dT/dψ)/(dV/dψ) is directly availablespecies_label:nothingwrites the run total toKineticForces/<method>/; a label (e.g."ion_z1_m2","electron") writes one species' contribution toKineticForces/PerSpecies/<label>/<method>/. A multi-species run calls this once per species and once for the summed total, so the group is opened-or-created each time.