From 3a3d50d0a78cafc9e71daff9b8f5187489911e48 Mon Sep 17 00:00:00 2001 From: Matthew Pharr Date: Wed, 8 Jul 2026 11:56:34 -0400 Subject: [PATCH 1/7] KineticForces - OPTIMIZATION - Implement static typing for PitchGARParams to reduce multithreading overhead and add a pre-allocated quadgk buffer --- src/KineticForces/PitchIntegration.jl | 28 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/KineticForces/PitchIntegration.jl b/src/KineticForces/PitchIntegration.jl index a66091c7..7af48258 100644 --- a/src/KineticForces/PitchIntegration.jl +++ b/src/KineticForces/PitchIntegration.jl @@ -22,7 +22,7 @@ Parameters for the GAR pitch-angle integrand. Uses a unified fbnce interpolant that returns [ωb, ωd, f₁, f₂, ...] at each λ, matching Fortran `lambdaintgrl_lsode`. """ -struct PitchGARParams +struct PitchGARParams{F} wn::Float64 # density gradient diamagnetic drift frequency wt::Float64 # temperature gradient diamagnetic drift frequency we::Float64 # electric precession frequency @@ -44,9 +44,11 @@ struct PitchGARParams rex::Float64 # real part multiplier for resonance operator imx::Float64 # imaginary part multiplier for resonance operator nqty::Int # number of flux quantities to integrate - fbnce::Any # CubicSeriesInterpolant: fbnce(λ) → [wb, wd, f₁, ...] + fbnce::F # CubicSeriesInterpolant: fbnce(λ) → [wb, wd, f₁, ...] (typed for stability) fbnce_norm::Vector{Float64} # normalization factors (1/median) fbnce_hint::Base.RefValue{Int} # sticky bracket-search hint for fbnce(λ) + fvals::Vector{ComplexF64} # reusable buffer for the in-place fbnce(fvals, λ) evaluation + esegbuf::Vector{QuadGK.Segment{Float64,ComplexF64,Float64}} # reusable energy-integral QuadGK segment buffer end @@ -84,7 +86,9 @@ function integrate_pitch_gar_quadgk( wn, wt, we, nuk, bobmax, epsr, q, ell, n, psi, method, nutype, f0type, nufac, ximag, qt, energy_atol, energy_rtol, - rex, imx, nqty, fbnce, fbnce_norm, Ref(1)) + rex, imx, nqty, fbnce, fbnce_norm, Ref(1), + Vector{ComplexF64}(undef, 2 + nqty), + QuadGK.alloc_segbuf(Float64, ComplexF64, Float64)) lambda_min = first(fbnce.cache.x) lambda_max = last(fbnce.cache.x) @@ -114,7 +118,8 @@ In-place complex-valued kernel for `quadgk!`. Writes `out[i] = fvals[i+2] * xint for i in 1..nqty. QuadGK natively handles ComplexF64. """ function _pitch_gar_kernel_quadgk!(out::Vector{ComplexF64}, lambda, p::PitchGARParams) - fvals = p.fbnce(lambda; hint=p.fbnce_hint) + p.fbnce(p.fvals, lambda; hint=p.fbnce_hint) + fvals = p.fvals wb = real(fvals[1]) wd = real(fvals[2]) @@ -127,12 +132,12 @@ function _pitch_gar_kernel_quadgk!(out::Vector{ComplexF64}, lambda, p::PitchGARP p.ell, leff, p.n, p.psi, lambda, p.method; nutype=p.nutype, f0type=p.f0type, nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol) + atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) xint_counter = integrate_energy(p.wn, p.wt, p.we, wd, -wb, nueff, p.ell, leff, p.n, p.psi, lambda, p.method; nutype=p.nutype, f0type=p.f0type, nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol) + atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) xint = xint_co + xint_counter else xint = integrate_energy(p.wn, p.wt, p.we, wd, wb, nueff, @@ -182,7 +187,9 @@ function integrate_pitch_gar_quadgk_wt( wn, wt, we, nuk, bobmax, epsr, q, ell, n, psi, method, nutype, f0type, nufac, ximag, qt, energy_atol, energy_rtol, - 1.0, 1.0, nqty, fbnce, fbnce_norm, Ref(1)) + 1.0, 1.0, nqty, fbnce, fbnce_norm, Ref(1), + Vector{ComplexF64}(undef, 2 + nqty), + QuadGK.alloc_segbuf(Float64, ComplexF64, Float64)) lambda_min = first(fbnce.cache.x) lambda_max = last(fbnce.cache.x) @@ -211,7 +218,8 @@ Dual-output pitch kernel. Fills a length-`2*nqty` buffer: One energy integration per λ; both halves share it. """ function _pitch_gar_kernel_quadgk_wt!(out::Vector{ComplexF64}, lambda, p::PitchGARParams) - fvals = p.fbnce(lambda; hint=p.fbnce_hint) + p.fbnce(p.fvals, lambda; hint=p.fbnce_hint) + fvals = p.fvals wb = real(fvals[1]) wd = real(fvals[2]) @@ -224,12 +232,12 @@ function _pitch_gar_kernel_quadgk_wt!(out::Vector{ComplexF64}, lambda, p::PitchG p.ell, leff, p.n, p.psi, lambda, p.method; nutype=p.nutype, f0type=p.f0type, nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol) + atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) xint_counter = integrate_energy(p.wn, p.wt, p.we, wd, -wb, nueff, p.ell, leff, p.n, p.psi, lambda, p.method; nutype=p.nutype, f0type=p.f0type, nufac=p.nufac, ximag=p.ximag, qt=p.qt, - atol=p.energy_atol, rtol=p.energy_rtol) + atol=p.energy_atol, rtol=p.energy_rtol, segbuf=p.esegbuf) xint = xint_co + xint_counter else xint = integrate_energy(p.wn, p.wt, p.we, wd, wb, nueff, From de766625b0e5643832bfada3b30e6eecdbe1a5f1 Mon Sep 17 00:00:00 2001 From: Matthew Pharr Date: Wed, 8 Jul 2026 11:58:28 -0400 Subject: [PATCH 2/7] KineticForces - OPTIMIZATION - Refactor resonant integrand to reduce memory allocation in QuadGK inner loop and update Energy Integration to support QuadGK buffer pre-allocation --- src/KineticForces/EnergyIntegration.jl | 58 ++++++++++++++------------ 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/KineticForces/EnergyIntegration.jl b/src/KineticForces/EnergyIntegration.jl index 7c441582..05b0f977 100644 --- a/src/KineticForces/EnergyIntegration.jl +++ b/src/KineticForces/EnergyIntegration.jl @@ -208,9 +208,32 @@ The collisionless case (ν ≡ 0, real-axis pole) is the exact ν→0⁺ limit o formula; its causal branch is carried by the signed zero of `−pole_offset` (see the add-back below), and its on-axis `0/0` window by the analytic regular-part limit. """ +# Real x-space resonant integrand with pole subtractions +# Explicit function keeps memory allocation out of the QuadGK inner loop. +@inline function _resonant_integrand(x::Float64, p::EnergyParams, + residues::Vector{ComplexF64}, x_poles::Vector{ComplexF64}, npole::Int, + leff::Float64, wb::Float64, n::Int, wd::Float64)::ComplexF64 + val = _energy_integrand_real(x, p) + @inbounds for k in 1:npole + val -= residues[k] / (x - x_poles[k]) + end + if !isfinite(val) + k = 1 + @inbounds for j in 2:npole + abs(x - real(x_poles[j])) < abs(x - real(x_poles[k])) && (k = j) + end + val = _real_pole_regular_part(real(x_poles[k]), p, leff, wb, n, wd) + @inbounds for j in 1:npole + j == k && continue + val -= residues[j] / (x - x_poles[j]) + end + end + return val +end + function _integrate_energy_resonant(p::EnergyParams, leff::Float64, wb::Float64, n::Int, we::Float64, wd::Float64, - atol::Float64, rtol::Float64)::ComplexF64 + atol::Float64, rtol::Float64, segbuf=nothing)::ComplexF64 x_res_list = find_resonance_energies(leff, wb, n, we, wd) # ≤ 2 roots of a quadratic in √x x_poles = ComplexF64[] residues = ComplexF64[] @@ -240,29 +263,10 @@ function _integrate_energy_resonant(p::EnergyParams, leff::Float64, wb::Float64, npole = length(residues) # Physical x-space integrand N(x)·exp(-x)/(i·Ω) minus the subtracted pole parts. - integrand = x -> begin - val = _energy_integrand_real(x, p) - @inbounds for k in 1:npole - val -= residues[k] / (x - x_poles[k]) - end - # At a ν=0 pole Ω rounds to 0 and the physical term is 0/0; substitute the nearest - # pole's analytic regular-part limit, keeping the other (finite) pole subtractions. - if !isfinite(val) - k = 1 - @inbounds for j in 2:npole - abs(x - real(x_poles[j])) < abs(x - real(x_poles[k])) && (k = j) - end - val = _real_pole_regular_part(real(x_poles[k]), p, leff, wb, n, wd) - @inbounds for j in 1:npole - j == k && continue - val -= residues[j] / (x - x_poles[j]) - end - end - return val - end + integrand = x -> _resonant_integrand(x, p, residues, x_poles, npole, leff, wb, n, wd) if npole == 0 - val, _ = quadgk(integrand, 0.0, X_ENERGY_MAX; atol=atol, rtol=rtol) + val, _ = quadgk(integrand, 0.0, X_ENERGY_MAX; atol=atol, rtol=rtol, segbuf=segbuf) return val end @@ -271,9 +275,9 @@ function _integrate_energy_resonant(p::EnergyParams, leff::Float64, wb::Float64, # positional args — a `breaks...` splat of a runtime-length Vector is type-unstable. breaks = unique(sort(real.(x_poles))) smooth_val, _ = if length(breaks) == 1 - quadgk(integrand, 0.0, breaks[1], X_ENERGY_MAX; atol=atol, rtol=rtol) + quadgk(integrand, 0.0, breaks[1], X_ENERGY_MAX; atol=atol, rtol=rtol, segbuf=segbuf) else - quadgk(integrand, 0.0, breaks[1], breaks[2], X_ENERGY_MAX; atol=atol, rtol=rtol) + quadgk(integrand, 0.0, breaks[1], breaks[2], X_ENERGY_MAX; atol=atol, rtol=rtol, segbuf=segbuf) end return smooth_val + pole_contribution end @@ -309,7 +313,7 @@ function integrate_energy(wn::Float64, wt::Float64, we::Float64, wd::Float64, n::Int, psi::Float64, lambda::Float64, method::String; nutype::String="harmonic", f0type::String="maxwellian", nufac::Float64=1.0, ximag::Float64=0.0, qt::Bool=false, - atol::Float64=1e-7, rtol::Float64=1e-5)::ComplexF64 + atol::Float64=1e-7, rtol::Float64=1e-5, segbuf=nothing)::ComplexF64 p = EnergyParams(wn, wt, we, wd, wb, nuk, leff, n, nutype, f0type, nufac, ximag, qt) @@ -317,13 +321,13 @@ function integrate_energy(wn::Float64, wt::Float64, we::Float64, wd::Float64, # CGL has no resonance denominator and no pole — integrate the physical # x-space integrand directly over the half line (QuadGK maps [0,∞) itself). if f0type == "cgl" - val, _ = quadgk(x -> _energy_integrand_real(x, p), 0.0, Inf; atol=atol, rtol=rtol) + val, _ = quadgk(x -> _energy_integrand_real(x, p), 0.0, Inf; atol=atol, rtol=rtol, segbuf=segbuf) return val end # Single resonant path for any collisionality: real x-space PV+residue with a # pole x_pole = x_res - i·ν/Ω′ (off-axis for ν>0, real for ν≡0). - return _integrate_energy_resonant(p, leff, wb, n, we, wd, atol, rtol) + return _integrate_energy_resonant(p, leff, wb, n, we, wd, atol, rtol, segbuf) end """ From d2c45848f87ee889df271eb0eab0695cc24b5277 Mon Sep 17 00:00:00 2001 From: Matthew Pharr Date: Wed, 8 Jul 2026 15:52:49 -0400 Subject: [PATCH 3/7] KineticForces - OPTIMIZATION - Introduce BounceScratch struct for per-surface scratch buffers to reduce memory allocation during bounce-averaging computations --- src/KineticForces/BounceAveraging.jl | 115 ++++++++++++++++++++------- 1 file changed, 85 insertions(+), 30 deletions(-) diff --git a/src/KineticForces/BounceAveraging.jl b/src/KineticForces/BounceAveraging.jl index 3a75b003..0ea80831 100644 --- a/src/KineticForces/BounceAveraging.jl +++ b/src/KineticForces/BounceAveraging.jl @@ -204,6 +204,9 @@ function compute_bounce_data( mpert = length(mfac) do_matrices = !isnothing(smat) + # Per-surface scratch, reused across all λ. + scr = BounceScratch(ntheta, mpert) + # Trapped-passing boundary and λ range lmdatpb = bo / bmax lmdamax = bo / bmin @@ -238,13 +241,13 @@ function compute_bounce_data( # Find bounce points and build θ sub-grid _, _, tdt_pts, tdt_wts = _find_bounce_points_and_grid( lmda, bo, sigma, tspl, ibmax, theta_bmax, - lmdatpb, lmdamax, psi, ntheta) + lmdatpb, lmdamax, psi, ntheta, scr) # Bounce integrals over θ (Fortran lines 674-735) wbbar, wdbar, dJdJ_val, wmats_lmda = _bounce_integrate( tdt_pts, tdt_wts, lmda, lnq, sigma, n, q, bo, tspl, chi1, ro, mfac, dbob_m_f, divx_m_f, divxfac, wdfac, - do_matrices, mpert, smat, tmat, xmat, ymat, zmat) + do_matrices, mpert, smat, tmat, xmat, ymat, zmat, scr) # Physical frequencies (Fortran lines 744-745) wb_arr[ilmda] = wbbar * bhat @@ -263,6 +266,46 @@ function compute_bounce_data( end +""" + BounceScratch(ntheta, mpert; nfine=256) + +Per-surface scratch buffers for the bounce-averaging inner loops. Allocated once in +`compute_bounce_data` and reused across every λ (each λ previously reallocated these +θ- and mode-sized arrays). Sizes are fixed for a flux surface (`ntheta` sub-grid points, +`mpert` Fourier modes), so a single set of buffers serves the whole λ sweep. Buffers that +the callers zero-initialize are `fill!`-reset per λ, preserving bit-for-bit results. +""" +struct BounceScratch + cum_wb_arr::Vector{Float64} # ntheta — cumulative bounce action + jvtheta::Vector{ComplexF64} # ntheta — action integrand + wmu_mt::Matrix{ComplexF64} # mpert × ntheta — W_μ per θ + wen_mt::Matrix{ComplexF64} # mpert × ntheta — W_E per θ + expm::Vector{ComplexF64} # mpert — Fourier basis at a θ + pl::Vector{ComplexF64} # ntheta — bounce phase factor + wmu_ba::Vector{ComplexF64} # mpert — bounce-averaged W_μ + wen_ba::Vector{ComplexF64} # mpert — bounce-averaged W_E + wmats_lmda::Vector{ComplexF64} # nqty_matrix(mpert) — packed W outer products + tspl_f::Vector{Float64} # 5 — in-place tspl(θ) evaluation + vpar_fine::Vector{Float64} # nfine+1 — v_par(θ) for bounce-point search +end + +function BounceScratch(ntheta::Int, mpert::Int; nfine::Int=256) + return BounceScratch( + Vector{Float64}(undef, ntheta), + Vector{ComplexF64}(undef, ntheta), + Matrix{ComplexF64}(undef, mpert, ntheta), + Matrix{ComplexF64}(undef, mpert, ntheta), + Vector{ComplexF64}(undef, mpert), + Vector{ComplexF64}(undef, ntheta), + Vector{ComplexF64}(undef, mpert), + Vector{ComplexF64}(undef, mpert), + Vector{ComplexF64}(undef, nqty_matrix(mpert)), + Vector{Float64}(undef, 5), + Vector{Float64}(undef, nfine + 1), + ) +end + + # ============================================================================ # Internal helpers # ============================================================================ @@ -309,21 +352,24 @@ function _find_bounce_points_and_grid( lmda::Float64, bo::Float64, sigma::Int, tspl, ::Int, theta_bmax::Float64, ::Float64, ::Float64, psi::Float64, - ntheta::Int + ntheta::Int, scr::BounceScratch ) if sigma == 0 # trapped - # Build v_par(θ) = 1 - (λ/bo)*B(θ) and find roots - # Use a dense θ grid to find zero crossings + # Build v_par(θ) = 1 - (λ/bo)*B(θ) and find roots on a dense θ grid. nfine = 256 theta_fine = range(0.0, 1.0, length=nfine+1) - vpar_fine = [1.0 - (lmda / bo) * tspl(mod(θ, 1.0))[1] for θ in theta_fine] + vpar_fine = scr.vpar_fine + @inbounds for i in 1:(nfine+1) + tspl(scr.tspl_f, mod(theta_fine[i], 1.0)) + vpar_fine[i] = 1.0 - (lmda / bo) * scr.tspl_f[1] + end # Find zero crossings bpts = Float64[] for i in 1:nfine if vpar_fine[i] * vpar_fine[i+1] < 0 # Bisect for better accuracy - θ_root = _bisect_vpar(tspl, lmda, bo, theta_fine[i], theta_fine[i+1]) + θ_root = _bisect_vpar(tspl, scr.tspl_f, lmda, bo, theta_fine[i], theta_fine[i+1]) push!(bpts, θ_root) end end @@ -339,7 +385,7 @@ function _find_bounce_points_and_grid( t2 = bpts[1] + 1.0 else # Find deepest potential well (Fortran lines 616-639) - t1, t2 = _find_deepest_well(bpts, tspl, lmda, bo) + t1, t2 = _find_deepest_well(bpts, tspl, scr.tspl_f, lmda, bo) end # Power-law grid refined near bounce points @@ -381,11 +427,13 @@ this routine does not check, it just halves toward the sign change. # Returns - `θ::Float64`: the converged (or capped) bounce-point angle. """ -function _bisect_vpar(tspl, lmda::Float64, bo::Float64, θa::Float64, θb::Float64; tol=1e-12, maxiter=50) - va = 1.0 - (lmda / bo) * tspl(mod(θa, 1.0))[1] +function _bisect_vpar(tspl, tspl_f::Vector{Float64}, lmda::Float64, bo::Float64, θa::Float64, θb::Float64; tol=1e-12, maxiter=50) + tspl(tspl_f, mod(θa, 1.0)) + va = 1.0 - (lmda / bo) * tspl_f[1] for _ in 1:maxiter θm = 0.5 * (θa + θb) - vm = 1.0 - (lmda / bo) * tspl(mod(θm, 1.0))[1] + tspl(tspl_f, mod(θm, 1.0)) + vm = 1.0 - (lmda / bo) * tspl_f[1] if abs(vm) < tol || (θb - θa) < tol return θm end @@ -404,7 +452,7 @@ end Find deepest potential well among bounce point pairs. Ports Fortran lines 616-639. """ -function _find_deepest_well(bpts::Vector{Float64}, tspl, lmda::Float64, bo::Float64) +function _find_deepest_well(bpts::Vector{Float64}, tspl, tspl_f::Vector{Float64}, lmda::Float64, bo::Float64) nbpts = length(bpts) best_vpar = 0.0 best_t1 = 0.0 @@ -418,7 +466,8 @@ function _find_deepest_well(bpts::Vector{Float64}, tspl, lmda::Float64, bo::Floa else θmid = 0.5 * (bpts[i] + bpts[j]) end - vpar_mid = 1.0 - (lmda / bo) * tspl(mod(θmid, 1.0))[1] + tspl(tspl_f, mod(θmid, 1.0)) + vpar_mid = 1.0 - (lmda / bo) * tspl_f[1] if vpar_mid > best_vpar best_t1 = bpts[i] best_t2 = bpts[j] @@ -451,7 +500,7 @@ function _bounce_integrate( mfac::Vector{Int}, dbob_m_f::Vector{ComplexF64}, divx_m_f::Vector{ComplexF64}, divxfac::Float64, wdfac::Float64, do_matrices::Bool, mpert::Int, - smat, tmat, xmat, ymat, zmat + smat, tmat, xmat, ymat, zmat, scr::BounceScratch ) ntheta = length(tdt_pts) theta0 = tdt_pts[1] @@ -460,23 +509,27 @@ function _bounce_integrate( cum_wb = 0.0 cum_wd = 0.0 - # θ-scratch array allocated fresh each call. Pool-based reuse was tried - # (AdaptiveArrayPools) but showed no speedup and introduced severe slowdowns - # at 2+ threads; plain allocations match Fortran baseline behavior. - cum_wb_arr = zeros(Float64, ntheta) + # θ-scratch arrays reused across λ from the per-surface BounceScratch. Zero-reset + # the ones the loop populates only partially (i in 2:ntheta-1, with continue/break), + # so unwritten entries stay 0 exactly as the previous `zeros(...)` did. + cum_wb_arr = scr.cum_wb_arr + fill!(cum_wb_arr, 0.0) # Action integrand - jvtheta = zeros(ComplexF64, ntheta) + jvtheta = scr.jvtheta + fill!(jvtheta, ComplexF64(0.0)) # W vectors for matrix path - wmu_mt = do_matrices ? zeros(ComplexF64, mpert, ntheta) : nothing - wen_mt = do_matrices ? zeros(ComplexF64, mpert, ntheta) : nothing + wmu_mt = scr.wmu_mt + wen_mt = scr.wen_mt + if do_matrices + fill!(wmu_mt, ComplexF64(0.0)) + fill!(wen_mt, ComplexF64(0.0)) + end - # Pre-allocated scratch for hot-loop tspl evaluation + Fourier-basis buffer - # (avoids Vector{Float64}(5) + Vector{ComplexF64}(mpert) allocation per θ - # sub-grid point — previously ~256 allocs × 126 inner iters per call). - tspl_f = Vector{Float64}(undef, 5) - expm = Vector{ComplexF64}(undef, mpert) + # Scratch for hot-loop tspl evaluation + Fourier-basis buffer (fully written per use). + tspl_f = scr.tspl_f + expm = scr.expm for i in 2:ntheta-1 # Edge weights are 0 from powspace θ = tdt_pts[i] @@ -579,7 +632,7 @@ function _bounce_integrate( one_minus_sigma = 1 - sigma bj_integral = ComplexF64(0.0) if do_matrices - pl = Vector{ComplexF64}(undef, ntheta) + pl = scr.pl @inbounds for i in 1:ntheta pl[i] = cis(-twopi * lnq * cum_wb_arr[i] * nrm / pl_denom) end @@ -606,8 +659,10 @@ function _bounce_integrate( # Bounce-average W_μ and W_E vectors (Fortran lines 762-767). # Trapezoidal quadrature: boundary samples weighted by 0.5 (wmu_mt and wen_mt # are zero at i=1 and i=ntheta from the 2:ntheta-1 population loop above). - wmu_ba = zeros(ComplexF64, mpert) - wen_ba = zeros(ComplexF64, mpert) + wmu_ba = scr.wmu_ba + wen_ba = scr.wen_ba + fill!(wmu_ba, ComplexF64(0.0)) + fill!(wen_ba, ComplexF64(0.0)) @inbounds for i in 1:ntheta w = (i == 1 || i == ntheta) ? 0.5 : 1.0 factor = w * (pl[i] + one_minus_sigma / (pl[i] + SINGULAR_EPS)) @@ -636,7 +691,7 @@ function _bounce_integrate( # Scale by wbbar/ro² (Fortran line 789) scale = wbbar / ro^2 Mu = (mpert * (mpert + 1)) ÷ 2 - wmats_lmda = Vector{ComplexF64}(undef, nqty_matrix(mpert)) + wmats_lmda = scr.wmats_lmda # A (Hermitian): upper triangle of W_Z†W_Z, rank-1 → conj(wz[i])·wz[j]. off = 0 From 17a441abc4f1bd0a2a390fc3e75c3e7083522284 Mon Sep 17 00:00:00 2001 From: Matthew Pharr Date: Wed, 8 Jul 2026 17:26:15 -0400 Subject: [PATCH 4/7] KineticForces - DOCS - return comment above newly explicit function --- src/KineticForces/EnergyIntegration.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/KineticForces/EnergyIntegration.jl b/src/KineticForces/EnergyIntegration.jl index 05b0f977..267d820b 100644 --- a/src/KineticForces/EnergyIntegration.jl +++ b/src/KineticForces/EnergyIntegration.jl @@ -217,6 +217,8 @@ add-back below), and its on-axis `0/0` window by the analytic regular-part limit @inbounds for k in 1:npole val -= residues[k] / (x - x_poles[k]) end + # At a ν=0 pole Ω rounds to 0 and the physical term is 0/0; substitute the nearest + # pole's analytic regular-part limit, keeping the other (finite) pole subtractions. if !isfinite(val) k = 1 @inbounds for j in 2:npole From 6f494094cdf0e13ba587ffc2ad9e8f2f4a0408e2 Mon Sep 17 00:00:00 2001 From: Matthew Pharr Date: Wed, 8 Jul 2026 17:29:24 -0400 Subject: [PATCH 5/7] KineticForces - REFACTOR - Consolidate Solovev equilibrium parameters into gpec.toml and remove deprecated sol.toml --- .../gpec.toml | 19 ++++++++++++++++++- .../sol.toml | 11 ----------- 2 files changed, 18 insertions(+), 12 deletions(-) delete mode 100644 examples/Solovev_kinetic_calculated_example/sol.toml diff --git a/examples/Solovev_kinetic_calculated_example/gpec.toml b/examples/Solovev_kinetic_calculated_example/gpec.toml index 4a46bdb4..15ce5854 100644 --- a/examples/Solovev_kinetic_calculated_example/gpec.toml +++ b/examples/Solovev_kinetic_calculated_example/gpec.toml @@ -1,6 +1,10 @@ +# Solovev analytical equilibrium — n=1 ideal stability with calculated kinetic matrices. +# Exercises the real KineticForces (NTV) physics path at full strength; kinetic regression +# fixture (et[1]≈1.894-1.525i). The equilibrium is generated analytically from the embedded +# [SOL_INPUT] section, with n·T tied to the Solovev P₀≈4.27e4 Pa via the local kinetic.dat. + [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file -eq_filename = "sol.toml" # Path to equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) grid_type = "ldp" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized flux coordinate @@ -45,3 +49,16 @@ singfac_min = 1e-4 # Fractional distance from rational q at which ide ucrit = 1e3 # Maximum fraction of solutions allowed before re-normalized write_outputs_to_HDF5 = true verbose = false + +# Solovev analytic equilibrium parameters (eq_type = "sol"); see SolovevConfig in src/Equilibrium. +[SOL_INPUT] +mr = 128 # Number of radial grid zones +mz = 128 # Number of axial grid zones +ma = 128 # Number of flux grid zones +e = 1.6 # Elongation +a = 0.33 # Minor radius +r0 = 1.0 # Major radius +q0 = 1.9 # Safety factor at the magnetic axis (O-point) +p0fac = 1 # Scale on-axis pressure (β changes; Φ, q fixed) +b0fac = 1 # Scale toroidal field at constant β (Bt changes; shape, β fixed) +f0fac = 1 # Scale toroidal field at constant pressure (β, q change; Φ, p, Bp fixed) diff --git a/examples/Solovev_kinetic_calculated_example/sol.toml b/examples/Solovev_kinetic_calculated_example/sol.toml deleted file mode 100644 index e54fdeac..00000000 --- a/examples/Solovev_kinetic_calculated_example/sol.toml +++ /dev/null @@ -1,11 +0,0 @@ -[SOL_INPUT] -mr = 128 # number of radial grid zones -mz = 128 # number of axial grid zones -ma = 128 # number of flux grid zones -e = 1.6 # elongation -a = 0.33 # minor radius -r0 = 1.0 # major radius -q0 = 1.9 # safety factor at the o-point -p0fac=1 # scale on-axis pressure (P-> P+P0*p0fac. beta changes. Phi,q constant) -b0fac=1 # scale toroidal field at constant beta (s*Phi,s*f,s^2*P. bt changes. Shape,beta constant) -f0fac=1 # scale toroidal field at constant pressure (s*f. beta,q changes. Phi,p,bp constant) From 0007e6289e8ccf3ce77069893aa44b313b6ed453 Mon Sep 17 00:00:00 2001 From: Matthew Pharr Date: Wed, 8 Jul 2026 17:40:09 -0400 Subject: [PATCH 6/7] KineticForces - MINOR - Initialize wmats_lmda with NaN to catch uninitialized entries in bounce integration --- src/KineticForces/BounceAveraging.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/KineticForces/BounceAveraging.jl b/src/KineticForces/BounceAveraging.jl index 0ea80831..2c65a598 100644 --- a/src/KineticForces/BounceAveraging.jl +++ b/src/KineticForces/BounceAveraging.jl @@ -692,6 +692,8 @@ function _bounce_integrate( scale = wbbar / ro^2 Mu = (mpert * (mpert + 1)) ÷ 2 wmats_lmda = scr.wmats_lmda + # Fill with NaN to catch uninitialized entries + fill!(wmats_lmda, ComplexF64(NaN, NaN)) # A (Hermitian): upper triangle of W_Z†W_Z, rank-1 → conj(wz[i])·wz[j]. off = 0 From 38b5f4251dd86500048ba5b9f69b123cd5f12e6f Mon Sep 17 00:00:00 2001 From: logan-nc Date: Wed, 8 Jul 2026 17:45:20 -0400 Subject: [PATCH 7/7] KineticForces - MINOR - Address PR review: derive nfine from scratch buffer, BounceScratch field docs fvals stays ComplexF64: the kinetic-matrix (wt) path builds fbnce from complex op_wmats data, so a real buffer throws InexactError in the pitch kernel. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TTu6sczod1Qtjy9e9pB6jP --- src/KineticForces/BounceAveraging.jl | 39 ++++++++++++++++++-------- src/KineticForces/EnergyIntegration.jl | 2 +- src/KineticForces/PitchIntegration.jl | 2 +- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/KineticForces/BounceAveraging.jl b/src/KineticForces/BounceAveraging.jl index 2c65a598..e0153fcb 100644 --- a/src/KineticForces/BounceAveraging.jl +++ b/src/KineticForces/BounceAveraging.jl @@ -274,19 +274,33 @@ Per-surface scratch buffers for the bounce-averaging inner loops. Allocated once θ- and mode-sized arrays). Sizes are fixed for a flux surface (`ntheta` sub-grid points, `mpert` Fourier modes), so a single set of buffers serves the whole λ sweep. Buffers that the callers zero-initialize are `fill!`-reset per λ, preserving bit-for-bit results. + +## Fields +- `cum_wb_arr::Vector{Float64}`: length `ntheta` — cumulative bounce action +- `jvtheta::Vector{ComplexF64}`: length `ntheta` — action integrand +- `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 +- `vpar_fine::Vector{Float64}`: length `nfine+1` — v_par(θ) for bounce-point search; + `_find_bounce_points_and_grid` derives its fine-grid size from this buffer's length """ struct BounceScratch - cum_wb_arr::Vector{Float64} # ntheta — cumulative bounce action - jvtheta::Vector{ComplexF64} # ntheta — action integrand - wmu_mt::Matrix{ComplexF64} # mpert × ntheta — W_μ per θ - wen_mt::Matrix{ComplexF64} # mpert × ntheta — W_E per θ - expm::Vector{ComplexF64} # mpert — Fourier basis at a θ - pl::Vector{ComplexF64} # ntheta — bounce phase factor - wmu_ba::Vector{ComplexF64} # mpert — bounce-averaged W_μ - wen_ba::Vector{ComplexF64} # mpert — bounce-averaged W_E - wmats_lmda::Vector{ComplexF64} # nqty_matrix(mpert) — packed W outer products - tspl_f::Vector{Float64} # 5 — in-place tspl(θ) evaluation - vpar_fine::Vector{Float64} # nfine+1 — v_par(θ) for bounce-point search + cum_wb_arr::Vector{Float64} + jvtheta::Vector{ComplexF64} + wmu_mt::Matrix{ComplexF64} + wen_mt::Matrix{ComplexF64} + expm::Vector{ComplexF64} + pl::Vector{ComplexF64} + wmu_ba::Vector{ComplexF64} + wen_ba::Vector{ComplexF64} + wmats_lmda::Vector{ComplexF64} + tspl_f::Vector{Float64} + vpar_fine::Vector{Float64} end function BounceScratch(ntheta::Int, mpert::Int; nfine::Int=256) @@ -356,7 +370,8 @@ function _find_bounce_points_and_grid( ) if sigma == 0 # trapped # Build v_par(θ) = 1 - (λ/bo)*B(θ) and find roots on a dense θ grid. - nfine = 256 + # Grid size derives from the scratch buffer so it stays in sync with BounceScratch's nfine. + nfine = length(scr.vpar_fine) - 1 theta_fine = range(0.0, 1.0, length=nfine+1) vpar_fine = scr.vpar_fine @inbounds for i in 1:(nfine+1) diff --git a/src/KineticForces/EnergyIntegration.jl b/src/KineticForces/EnergyIntegration.jl index 267d820b..36cc1ece 100644 --- a/src/KineticForces/EnergyIntegration.jl +++ b/src/KineticForces/EnergyIntegration.jl @@ -209,7 +209,7 @@ formula; its causal branch is carried by the signed zero of `−pole_offset` (se add-back below), and its on-axis `0/0` window by the analytic regular-part limit. """ # Real x-space resonant integrand with pole subtractions -# Explicit function keeps memory allocation out of the QuadGK inner loop. +# Explicit function keeps memory allocation out of the QuadGK inner loop. @inline function _resonant_integrand(x::Float64, p::EnergyParams, residues::Vector{ComplexF64}, x_poles::Vector{ComplexF64}, npole::Int, leff::Float64, wb::Float64, n::Int, wd::Float64)::ComplexF64 diff --git a/src/KineticForces/PitchIntegration.jl b/src/KineticForces/PitchIntegration.jl index 7af48258..fa61c595 100644 --- a/src/KineticForces/PitchIntegration.jl +++ b/src/KineticForces/PitchIntegration.jl @@ -47,7 +47,7 @@ struct PitchGARParams{F} fbnce::F # CubicSeriesInterpolant: fbnce(λ) → [wb, wd, f₁, ...] (typed for stability) fbnce_norm::Vector{Float64} # normalization factors (1/median) fbnce_hint::Base.RefValue{Int} # sticky bracket-search hint for fbnce(λ) - fvals::Vector{ComplexF64} # reusable buffer for the in-place fbnce(fvals, λ) evaluation + fvals::Vector{ComplexF64} # reusable buffer for the in-place fbnce(fvals, λ) evaluation; must be complex — the wt-path fbnce carries complex op_wmats data esegbuf::Vector{QuadGK.Segment{Float64,ComplexF64,Float64}} # reusable energy-integral QuadGK segment buffer end