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: required psi (normalized poloidal flux), n_e, T_i, T_e, omega_E (and n_i, defaulting to n_e if omitted); optional omega_tor, chi_e (perpendicular heat diffusivity $\chi_\perp$), and chi_phi (toroidal momentum diffusivity $\chi_\phi$). Each dataset carries a units attribute; the root carries schema_version and provenance. Densities are m⁻³, temperatures eV, frequencies rad/s, diffusivities m²/s. Additional datasets named n_* (e.g. n_D, n_T) are named per-species densities for multi-ion runs; names outside n_* are reserved for future schema fields and ignored. Write files with write_kinetic_h5 (which round-trips the per-species densities).
  • ASCII (.gpeckf/.kin/.dat) — legacy six-column whitespace table psi_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 file

Each 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:

KnobDefaultStageDescription
density_factor1.0Profile loaderDensity scaling (ni, ne)
temperature_factor1.0Profile loaderTemperature scaling (Ti, Te)
ExB_rotation_factor1.0Profile loaderExB rotation scaling (omegaE)
toroidal_rotation_factor1.0Profile loaderTotal toroidal rotation scaling (wphi)
wdfac1.0Evaluation timeMagnetic drift frequency scaling
nufac1.0Evaluation timeCollisionality scaling
divxfac1.0Evaluation 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:

  1. Build first-pass cubic splines from original (unscaled) profiles
  2. Compute $\omega_{*n,i}$, $\omega_{*T,i}$, and $\omega_\phi$ at each grid point
  3. Apply density_factor to density, temperature_factor to temperature, and update diamagnetic terms: $\omega_{*n,\text{new}} = \texttt{temperature\_factor} \cdot \omega_{*n,i}$ (density_factor cancels in $T \cdot (dn/d\psi)/n$), $\omega_{*T,\text{new}} = \texttt{temperature\_factor} \cdot \omega_{*T,i}$
  4. Reform ExB rotation: $\omega_E = \texttt{toroidal\_rotation\_factor} \cdot \omega_\phi - \omega_{*n,\text{new}} - \omega_{*T,\text{new}}$
  5. Apply ExB scaling: $\omega_E \mathrel{*}= \texttt{ExB\_rotation\_factor}$
  6. Recompute collisionality from scaled density and temperature
  7. 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.

Differences from Fortran PENTRC
  1. 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, use nufac.

  2. Consistent derivative ordering: Fortran's inputs.f90:269-272 mixes pre-scaling spline derivatives with post-scaling array values when computing the toroidal_rotation_factor back-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
end
g = f["KineticForces/fgar/EnergyIntegrals"]            # h5py, 0-based
x_k = g["x_all"][g["trajectory_offsets"][k]:g["trajectory_offsets"][k + 1]]
GeneralizedPerturbedEquilibrium.KineticForces.METHOD_REGISTRYConstant
METHOD_REGISTRY

Single 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 in intr.method
  • flag — the KineticForcesControl field symbol that enables the method
  • kind — dispatch routing tag consumed by method_kind / Torque.jl (:gar for the GAR/matrix family, :fcgl/:rlar/:clar for 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.

source
GeneralizedPerturbedEquilibrium.KineticForces.BounceScratchType
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}: length ntheta — bounce-action integrand samples
  • g_wd::Vector{Float64}: length ntheta — drift integrand samples
  • cum_wb_arr::Vector{Float64}: length ntheta — cumulative bounce-action integral
  • jvtheta::Vector{ComplexF64}: length ntheta — action integrand
  • bj_samples::Vector{ComplexF64}: length ntheta — action bounce-integral samples
  • wsamp::Vector{ComplexF64}: length ntheta — per-mode W bounce-integral samples
  • wmu_mt::Matrix{ComplexF64}: mpert × ntheta — W_μ per θ
  • wen_mt::Matrix{ComplexF64}: mpert × ntheta — W_E per θ
  • expm::Vector{ComplexF64}: length mpert — Fourier basis at a θ
  • pl::Vector{ComplexF64}: length ntheta — bounce phase factor
  • wmu_ba::Vector{ComplexF64}: length mpert — bounce-averaged W_μ
  • wen_ba::Vector{ComplexF64}: length mpert — bounce-averaged W_E
  • wmats_lmda::Vector{ComplexF64}: length nqty_matrix(mpert) — packed W outer products
  • tspl_f::Vector{Float64}: length 5 — in-place tspl(θ) evaluation
  • int_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
source
GeneralizedPerturbedEquilibrium.KineticForces.IonSpeciesType
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.

source
GeneralizedPerturbedEquilibrium.KineticForces.KineticForcesControlType
KineticForcesControl

User-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).

source
GeneralizedPerturbedEquilibrium.KineticForces.KineticForcesInternalType
KineticForcesInternal

Internal 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 parameters
  • mthsurf, mfac: Poloidal grid info
  • dbob_m, divx_m: Perturbation mode interpolants
  • sing_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.

source
GeneralizedPerturbedEquilibrium.KineticForces._energy_integrand_realMethod
_energy_integrand_real(x::Float64, p::EnergyParams) → ComplexF64

Physical 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).

source
GeneralizedPerturbedEquilibrium.KineticForces._energy_numeratorMethod
_energy_numerator(x::Float64, p::EnergyParams) → ComplexF64

Numerator 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).

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

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

source
GeneralizedPerturbedEquilibrium.KineticForces._quadrature_weightsMethod
_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.

source
GeneralizedPerturbedEquilibrium.KineticForces._real_pole_regular_partMethod
_real_pole_regular_part(xr, p, leff, wb, n, wd) → ComplexF64

Laurent 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}).

source
GeneralizedPerturbedEquilibrium.KineticForces._resonance_nodes_from_frequenciesMethod
_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).

source
GeneralizedPerturbedEquilibrium.KineticForces._setup_surface_stateMethod
_setup_surface_state(psi, n, l, zi, mi, wdfac, electron,
                      equil, intr, kinetic_profiles) → NamedTuple

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

source
GeneralizedPerturbedEquilibrium.KineticForces.calculate_clarMethod
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)::ComplexF64

Calculate 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)

source
GeneralizedPerturbedEquilibrium.KineticForces.calculate_fcglMethod
calculate_fcgl(psi, n, l, tspl, dbob_m_f, divx_m_f, divxfac, n_s, T_s,
               equil, intr)::ComplexF64

Calculate 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]

source
GeneralizedPerturbedEquilibrium.KineticForces.calculate_garMethod
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...)::ComplexF64

Calculate 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

  1. Compute bounce-averaged quantities via compute_bounce_data()
  2. Build fbnce interpolant over λ, normalize for numerical stability
  3. Integrate over pitch angle via integrate_pitch_gar_quadgk()
  4. Apply torque normalization (Eq. 19, Logan et al. 2013)
  5. 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. Use rex_override=1.0, imx_override=1.0 to get full complex result for simultaneous kwmat/ktmat extraction via compute_kinetic_matrices_at_psi!.

Reference: [Logan et al., Phys. Plasmas 20, 122507 (2013)]

source
GeneralizedPerturbedEquilibrium.KineticForces.calculate_rlarFunction
calculate_rlar(psi, n, l, q, epsr, wdian, wdiat, welec, wdhat, wbhat,
               nueff, dVdpsi, n_s, T_s, dbob_m_f, bo, bmin)::ComplexF64

Calculate 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]

source
GeneralizedPerturbedEquilibrium.KineticForces.combine_species_statesMethod
combine_species_states(states) -> KineticForcesState

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

source
GeneralizedPerturbedEquilibrium.KineticForces.compute_bounce_dataMethod
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) → BounceData

Compute 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 flux
  • n: Toroidal mode number
  • l: Bounce harmonic number
  • q: 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 Fortran vspl equivalent)
  • mfac: Poloidal mode numbers [mlow:mhigh]
  • chi1: 2π·ψ₀ flux normalization
  • ro: 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 factors
  • mass: 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
source
GeneralizedPerturbedEquilibrium.KineticForces.compute_calculated_kinetic_matricesMethod
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 (carries kinetic_factor, kinetic_source)
  • equil: PlasmaEquilibrium with 2D interpolants and named profile/geometry splines
  • ffs_intr: ForceFreeStatesInternal (mode indexing)
  • metric: MetricData (provides ψ grid via metric.xs)
  • ffit: FourFitVars (used only for numpert_total cross-check)

Keyword arguments

  • kf_ctrl: KineticForcesControl, defaults to KineticForcesControl(). 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 via Equilibrium.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)
source
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 entry
  • ktmat::Array{ComplexF64,3}: Output (mpert×mpert×6), ftmm half, zeroed on entry
  • psi, n, l, zi, mi, wdfac, divxfac, electron: Same as tpsi! (divxfac unused on the matrix path — retained for call-site compatibility)
  • equil: PlasmaEquilibrium
  • intr::KineticForcesInternal: Internal state with mode indexing, geometric matrices (smats/tmats/xmats/ymats/zmats), and per-surface θ-grid buffers
  • kinetic_profiles::Equilibrium.KineticProfileSplines: Named kinetic-profile splines

Reference: [Logan et al., Phys. Plasmas 20, 122507 (2013)]

source
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 methods
  • intr::KineticForcesInternal: Internal state with equilibrium data
  • ctrl::KineticForcesControl: Control parameters specifying which methods to run
  • equil: PlasmaEquilibrium with 2D interpolants
  • kinetic_profiles::Equilibrium.KineticProfileSplines: Named kinetic-profile splines
source
GeneralizedPerturbedEquilibrium.KineticForces.evaluate_energy_integrandMethod
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)")
source
GeneralizedPerturbedEquilibrium.KineticForces.find_resonance_energiesMethod
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) = 0

With 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).

source
GeneralizedPerturbedEquilibrium.KineticForces.find_sign_change_rootsMethod
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.

source
GeneralizedPerturbedEquilibrium.KineticForces.integrate_energyMethod
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) → ComplexF64

Integrate 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
source
GeneralizedPerturbedEquilibrium.KineticForces.integrate_pitch_gar_quadgkMethod
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
source
GeneralizedPerturbedEquilibrium.KineticForces.integrate_pitch_gar_quadgk_wtMethod
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.

source
GeneralizedPerturbedEquilibrium.KineticForces.integrate_psi_quadgkMethod
integrate_psi_quadgk(n, nl, zi, mi, wdfac, divxfac, electron, method,
                      equil, intr, ctrl, kinetic_profiles; psi_min, psi_max) → NamedTuple

Integrate 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 torque
  • torque_profile: NamedTuple of (psi, dtdpsi, t_cumulative) from evaluation points
  • matrix_integrated: Trapezoidal-integrated mpert×mpert×6 matrix (if matrix method)
  • psi_nsteps::Int: Number of integrand evaluations
  • psi_quad_error::Float64: Quadrature error estimate for the total torque
  • panel_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.

source
GeneralizedPerturbedEquilibrium.KineticForces.kinetic_energy_matrices_for_euler_lagrange!Method
kinetic_energy_matrices_for_euler_lagrange!(kwmat, ktmat, state, psi, n, l, wdfac, intr;
                                            kwargs...) → nothing

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

source
GeneralizedPerturbedEquilibrium.KineticForces.kinetic_resonance_psi_nodesMethod
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.

source
GeneralizedPerturbedEquilibrium.KineticForces.powspaceMethod
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 bounds
  • pow::Int: Power of grid concentration (higher = more refined near edges)
  • num::Int: Number of grid points
  • endpoints::String: Where to concentrate: "lower", "upper", or "both"

Returns

  • points::Vector{Float64}: Grid point locations
  • weights::Vector{Float64}: Derivatives dx/dnorm (integration weights)
source
GeneralizedPerturbedEquilibrium.KineticForces.psi_panel_pointsMethod
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.

source
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:

  1. xs_m — [ξ^ψ, ∂ξ^ψ/∂ψ, ξ^α] CubicSeriesInterpolants over ψ
  2. dbob_m — δB/B Fourier modes via JBB deweighting (Fortran set_peq)
  3. divx_m — ∇·ξ⊥ Fourier modes via JBB deweighting

The JBB deweighting algorithm (Fortran pentrc/inputs.f90:828-868):

  1. Apply geometric matrices S,T,X,Y,Z in m-space
  2. Inverse DFT to θ-space
  3. Divide by J·B² at each θ
  4. Forward DFT back to m-space
source
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 value
  • psi::Float64: Normalized poloidal flux
  • n::Int: Toroidal mode number
  • l::Int: Bounce harmonic number
  • zi::Int: Ion charge in fundamental units (e)
  • mi::Int: Ion mass (units of proton mass)
  • wdfac::Float64: Drift factor
  • divxfac::Float64: Divergence factor
  • electron::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,passing
  • equil: PlasmaEquilibrium with 2D interpolants and named profile/geometry splines
  • intr::KineticForcesInternal: Internal state with mode indexing and perturbation splines
  • kinetic_profiles::Equilibrium.KineticProfileSplines: Named kinetic-profile splines (ni, ne, Ti, Te, ωE, νi, ν_e) loaded from kinetic.dat

Optional Arguments

  • op_wmats::Array{ComplexF64,3}: Store ForceFreeStates matrix elements

Returns

  • ComplexF64: Toroidal torque due to nonambipolar transport
source
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 method
  • records::Vector{EnergyIntegrationResult}: Integration records to write
source
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 handle
  • state::KineticForcesState: Accumulated computation results
  • dVdpsi_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 available
  • species_label: nothing writes the run total to KineticForces/<method>/; a label (e.g. "ion_z1_m2", "electron") writes one species' contribution to KineticForces/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.
source