From ce5148aa34eba3d262ab3f3508f1cf192906ef09 Mon Sep 17 00:00:00 2001 From: logan-nc Date: Mon, 19 Oct 2026 01:12:11 -0400 Subject: [PATCH 1/5] KineticForces - FEATURE! - Write the perturbation that drives the torque KineticForces builds the Lagrangian delta|B|/B and div(xi_perp) spectra from the PerturbedEquilibrium Clebsch displacements (`set_perturbation_data!`, the port of Fortran pentrc set_peq) and then keeps them only as interpolants, so nothing of the perturbation survives into gpec.h5. The torque is the only trace left of the field that produced it. That matters outside GPEC. The helical ripple delta_B/B on a flux surface, and its poloidal spectrum, is the input another NTV code needs to be run on a GPEC field -- it is the quantity the ITPA TC-24 benchmarking activity asked for so NTVTOK could be run on GPEC output. Fortran GPEC publishes it via `pmodb_flag`; Julia had no route to it at all. Retain the two mode tables alongside the interpolants and write them once per run to `KineticForces/Perturbation/`: psi PE psi_N grid m, n mode numbers dBoB_lagrangian Lagrangian delta|B|/B Fourier modes (dimensionless) div_xi_perp div(xi_perp) Fourier modes [1/m] Lagrangian, not Eulerian: Fortran fills the same spline from `lagbfun(j)/eqfun%f(1)` (pentrc/inputs.f90:477), and the matrix route this code takes is its sibling branch, `-(jbbdivx + jbbkapx)` at inputs.f90:855. The perturbation is a property of the run, not of a species or a method, so it is written by the call that has no `species_label` and guarded against a second write -- a multi-species run calls `write_to_hdf5!` once per species and once for the total. Nothing in the torque calculation reads the new fields, so this is additive: no existing output changes. --- src/GeneralizedPerturbedEquilibrium.jl | 4 +- src/KineticForces/KineticForcesStructs.jl | 15 +++++++ src/KineticForces/Output.jl | 30 ++++++++++++- test/runtests_kinetic.jl | 52 +++++++++++++++++++++++ 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/GeneralizedPerturbedEquilibrium.jl b/src/GeneralizedPerturbedEquilibrium.jl index 6322d2c79..4dfcfb53d 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -1077,7 +1077,7 @@ function run_kinetic_forces( if kf_ctrl.write_outputs_to_HDF5 h5open(joinpath(result.dir_path, kf_ctrl.HDF5_filename), "cw") do h5file KineticForces.write_to_hdf5!(h5file, kf_state; - dVdpsi_spline=result.equil.profiles.dVdpsi_spline) + dVdpsi_spline=result.equil.profiles.dVdpsi_spline, perturbation=kf_intr) end end else @@ -1102,7 +1102,7 @@ function run_kinetic_forces( dVdpsi_spline=result.equil.profiles.dVdpsi_spline, species_label=label) end KineticForces.write_to_hdf5!(h5file, kf_state; - dVdpsi_spline=result.equil.profiles.dVdpsi_spline) + dVdpsi_spline=result.equil.profiles.dVdpsi_spline, perturbation=kf_intr) end end end diff --git a/src/KineticForces/KineticForcesStructs.jl b/src/KineticForces/KineticForcesStructs.jl index ed9ef14a6..346074010 100644 --- a/src/KineticForces/KineticForcesStructs.jl +++ b/src/KineticForces/KineticForcesStructs.jl @@ -243,6 +243,14 @@ this struct. dbob_m::Any = nothing # δB/B perturbation modes (CubicSeriesInterpolant) divx_m::Any = nothing # ∇·ξ⊥ perturbation modes (CubicSeriesInterpolant) + # The same two quantities as the raw mode tables the interpolants were built from, kept + # so they can be written to the HDF5 file. δB/B is the Lagrangian perturbation of the + # field strength, which is what an NTV code needs as input; nothing in the torque + # calculation reads these fields back. + perturbation_psi::Vector{Float64} = Float64[] # ψ_N of each row + dbob_m_data::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # (nψ, mpert) + divx_m_data::Matrix{ComplexF64} = zeros(ComplexF64, 0, 0) # (nψ, mpert) + # Sticky bracket-search hints for amortized lookup in `tpsi!`. When the outer # quadrature evaluates ψ non-monotonically, FastInterpolations falls back to a # broader search when the hint is stale — still a net win over a cold bracket @@ -445,6 +453,13 @@ function set_perturbation_data!(kf_intr::KineticForcesInternal, pe_state::Pertur kf_intr.dbob_m = cubic_interp(psi_grid, Series(dbob_m_data); itp_opts...) kf_intr.divx_m = cubic_interp(psi_grid, Series(divx_m_data); itp_opts...) + # Retain the mode tables for output. They are the physical δB/B and ∇·ξ⊥ spectra on the + # PE ψ grid, in the same poloidal angle and (m, n) convention as the PerturbedEquilibrium + # field spectra, and are of direct interest to other NTV codes run on GPEC fields. + kf_intr.perturbation_psi = copy(psi_grid) + kf_intr.dbob_m_data = dbob_m_data + kf_intr.divx_m_data = divx_m_data + if kf_intr.verbose @info "set_perturbation_data!: built dbob_m/divx_m/xs_m interpolants " * "(npsi=$npsi, mpert=$mpert)" diff --git a/src/KineticForces/Output.jl b/src/KineticForces/Output.jl index 75f527ae2..902cb259c 100644 --- a/src/KineticForces/Output.jl +++ b/src/KineticForces/Output.jl @@ -8,7 +8,7 @@ then write to gpec.h5 in a single pass. """ write_to_hdf5!(h5file::HDF5.File, state::KineticForcesState; dVdpsi_spline=nothing, - species_label=nothing) + species_label=nothing, perturbation=nothing) Write KineticForces results to the "KineticForces" group in gpec.h5. @@ -18,13 +18,16 @@ Write KineticForces results to the "KineticForces" group in gpec.h5. - `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 +- `perturbation`: optional `KineticForcesInternal`; its δB/B and ∇·ξ⊥ mode tables are written + once to `KineticForces/Perturbation/`. The perturbation is a property of the run, not of a + species, so it is written only on the call that has no `species_label`. - `species_label`: `nothing` writes the run total to `KineticForces//`; a label (e.g. `"ion_z1_m2"`, `"electron"`) writes one species' contribution to `KineticForces/PerSpecies/