Skip to content

KineticForces Module (formerly PENTRC)#112

Open
logan-nc wants to merge 86 commits into
developfrom
kinetic
Open

KineticForces Module (formerly PENTRC)#112
logan-nc wants to merge 86 commits into
developfrom
kinetic

Conversation

@logan-nc
Copy link
Copy Markdown
Collaborator

@logan-nc logan-nc commented Dec 22, 2025

KineticForces Module (formerly PENTRC)

Complete refactor of the Fortran PENTRC module into a Julia KineticForces module, fully integrated into the GPEC architecture.

Refactoring (Commits 1-9)

  • Renamed PENTRC → KineticForces throughout
  • Converted module-level globals to struct-based parameter passing (EnergyParams, PitchParams)
  • Standardized IO: accumulate results in KineticForcesState, write to gpec.h5
  • Renamed LSODE functions to Julia conventions
  • Implemented pitch angle ODE integration
  • Cleaned up docs, removed all PENTRC references

API Streamlining (Commits 10-11)

  • Deleted Input.jl (file IO stubs no longer needed — data passed via structs)
  • Deleted Grid.jl (only dynamic ODE integration supported)
  • Inlined get_method_flags, added KineticForcesInternal(equil) constructor
  • Fixed b0/bo naming bug in equilibrium initialization

GAR NTV Implementation (Commit 12)

  • New BounceAveraging.jl: powspace() grid generation, compute_bounce_data() for bounce-averaged ωb(λ), ωd(λ), |δJ|²(λ), and optional W vector outer products for kinetic matrix assembly
  • Full calculate_gar(): Replaces stub with complete implementation — bounce data → fbnce interpolant → median normalization → pitch ODE → torque normalization (Eq. 19, Logan et al. 2013)
  • Dynamic ODE ψ integration: integrate_psi_ode() / psi_integrand!() replaces trapezoidal rule on fixed grid, matching Fortran tintgrl_lsode
  • Multi-n support: Block-diagonal numpert_total × numpert_total × 6 kinetic matrix assembly
  • All three integration levels (ψ, λ, energy) use adaptive ODE solvers
  • Supports fgar/tgar/pgar scalar torque and wmm/tmm/kmm/rmm matrix paths

Remaining TODOs

  • build_kinetic_metric_matrices! in Fourfit.jl (s/t/x/y/z geometric matrices for matrix path)
  • compute_kinetic_contribution() body (contracts matrices with ξ displacements)
  • Populating dbob_m/divx_m/xs_m interpolants from PerturbedEquilibriumState

🤖 Generated with Claude Code

@logan-nc logan-nc requested a review from mfairborn23 December 22, 2025 20:36
@logan-nc logan-nc self-assigned this Dec 22, 2025
@logan-nc logan-nc added the enhancement New feature or request label Dec 22, 2025
@logan-nc
Copy link
Copy Markdown
Collaborator Author

@mfairborn23 Please add whatever notes you took in our last meeting to the above summary (you can edit my post)

It is nice to have the PR open so we can al track the files changed and exchange questions / suggestions using the in-line comments. It is clear it is a WIP - so no worries there.

Let's open a separate branch for the kinetic modifications in fourfit.jl that put the kinetic terms into the stability calculation. I don't want this one to depend on that one or that one to have to wait if this one takes some more time. That one can use dummy kinetic matrices for now until we get this one merged.

@logan-nc logan-nc marked this pull request as draft February 13, 2026 20:26
satelite2517 and others added 13 commits February 20, 2026 14:06
- Consolidated main program into a single `Main` function for better readability and modularity.
- Introduced `PentrcControl`, `PentrcInternal`, and `PentrcOutput` structures to encapsulate control parameters, internal state, and output configurations respectively.
- Created a new `Output.jl` file to manage output operations, including writing torque, orbit, and energy data to ASCII files.
- Improved error handling and output directory management.
- Updated documentation and comments for clarity and consistency.
- Updated module descriptions and added detailed function documentation for energy integration and torque calculations.
- Refactored special functions to include elliptic integrals and double factorial computations.
- Improved clarity and organization of code for better maintainability.
…management; implement spline_roots and spline_fit functions in CubicSpline
…RC into GeneralizedPerturbedEquilibrium

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…States; add documentation page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pline_roots with Roots.jl

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…fix scoping, resolve duplicates

- energy.jl: Replace TaskLocalValue with plain Dict (no TaskLocalValues.jl dep)
- energy_integration.jl: Remove duplicate xintgrl_lsode stub (real impl in energy.jl),
  remove duplicate global Ref() definitions, add verbose keyword to tintgrl_grid
- pentrc.jl: Add includes for energy_integration.jl and special.jl
- Input.jl: Move `using DelimitedFiles` to top level, remove `using HDF5` from
  inside functions (already in pentrc.jl), remove shadowed local const declarations
- Utils.jl: Remove dead Utilities sub-module with broken params.jl include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
logan-nc and others added 17 commits April 17, 2026 20:52
…tor, temperature_factor, ExB_rotation_factor, toroidal_rotation_factor)

Previously these four knobs were dead code on KineticForcesControl. Now applied
during profile loading: build splines → compute diamagnetic/toroidal rotation →
scale profiles → reform omegaE → recompute collisionality → rebuild splines.
Documented with Fortran PENTRC differences in docs/src/kinetic_forces.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…etic energy matrix producer

In `compute_kinetic_matrices_at_psi!`, the producer assigned `kwmat = complex(0, imag)`
and `ktmat = complex(real, 0)`, but the downstream FKG reduction in
`ForceFreeStates/Kinetic.jl` follows the Fortran convention where `kwmat` carries
pure-real values (from the "wmm" integrand, `rex=0, imx=1`) and `ktmat` carries
pure-imag values (from the "tmm" integrand, `rex=1, imx=0`), both emerging from the
-i/(2n) normalization. The swap corrupted the asymmetric kinetic adjoints
(`caat = cmat_kin - 2·ktmat[3]`, `bkmat = kwmat[2] + ktmat[2] + i·(...)`), producing
sign-flipped F̄, K̄, Ḡ in the kinetic ODE RHS.

Validated on Solovev kinetic regression (Re(et[1]) sign flips -115.7 -> +99.75)
and the DIIID kinetic example (mpsi=128, pow1 grid, hamada coords, delta_m=±8,
set_psilim_via_dmlim): Re(et[1]) = +35, 4 singular surfaces q=2..5, no pathology.
Ideal Solovev regression invariant to ~1e-13.

Also rename for clarity:
- calculate_gar_matrices! -> kinetic_energy_matrices_for_euler_lagrange!
- out_wmats/full_wmats    -> cmplx_kinetic_energy_mats

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…int before FFT

Fortran fspline_fit_2 (math/fspline.f:293) uses f = fst%fs(:, 0:my-1, iq),
explicitly excluding the duplicated endpoint. Julia was FFT'ing all length(ys)
samples including the θ=0 ≡ θ=2π duplicate, biasing the DC coefficient by
~(f(0) − mean)/N. Small effect (~1% for the a10 kinetic Bk matrix at
ψ=0.30928), but principled — matches Fortran convention.

Detects the duplicate automatically via ys[end] − ys[1] ≈ 2π; if ys is already
non-duplicated, the FFT runs on all samples as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…gral

The phase factor pl[i] = exp(-2πi·lnq·cum_wb_arr[i]/pl_denom) used a
left-Riemann cumulative sum of the bounce integrand, whereas Fortran
torque.F90:736-750 uses spline_fit + spline_int, which for smooth functions
produces a cubic-spline cumulative integral ≈ trapezoidal:

    Fortran bspl%fsi(j)/Δx ≈ g_1 + g_2 + ... + g_{j-1} + g_j/2
    Julia   cum_wb_arr[i]   = g_1 + g_2 + ... + g_{i-1}    (too large by g_{i-1}/2)

The discrepancy was a per-sample phase offset of ~π·lnq·g_i/pl_denom, small
per-point but concentrating at the m=0 column of the kinetic matrices
because m=0 has no oscillating Fourier basis factor to average the
sampling bias out.

Effect at ψ=0.30928, a10 kinetic example (Bk matrix):
- Re(Bk[:, m=0]) Julia/Fortran ratio: 10.5 → 1.00
- Full Bk rel_frob:                    4.30% → 1.60%
- Hk rel_frob:                         30-100% → 1.61%
- Ck, Dk, Ek rel_frob:                 2-5% → 0.5-1.9%

Fix: cum_wb_arr[i] = cum_wb − wb_integrand/2 (same for cum_wd_arr).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… integrals

Convert the three right-Riemann sums in _bounce_integrate (bj_integral and
the wmu_ba/wen_ba bounce-average loops) to explicit trapezoidal with 0.5
endpoint weights. The sums were formally right-Riemann over 2:ntheta,
bit-equivalent to trapezoidal only because the payload arrays (jvtheta,
wmu_mt, wen_mt) are zero at i=1 and i=ntheta from the 2:ntheta-1 population
loop. Writing the weights explicitly makes the quadrature self-correct if
the boundary handling ever changes.

Also remove cum_wd_arr: allocated and written since introduction
(163aef2) but never read. Mirrors a commented-out Fortran branch
(torque.F90:747-751) for magnetic-precession-dominated regime that neither
code activates.

Numerical effect: all 6 kinetic matrix rel_frob values at a10 ψ=0.30928
unchanged (Ak 2.82%, Bk 1.60%, Ck 1.87%, Dk 0.54%, Ek 0.77%, Hk 1.62%).
Tiny ~1e-6 residuals in kwmat Re and ktmat Im (noise from fwmm/ftmm
resonance-operator symmetry) go to exact zero.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ic kwmat/ktmat

Adds `integrate_pitch_gar_quadgk_wt` and `_pitch_gar_kernel_quadgk_wt!` that emit
BOTH the fwmm half (rex=0, imx=1 → Fortran kwmat) and the ftmm 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 prior single-integration approach (rex=imx=1, then split kwmat=real, ktmat=imag)
gives correct forward sums kwmat+ktmat = full but WRONG adjoint combinations
kwmat-ktmat for non-Hermitian B_k, C_k, E_k blocks — because Fortran's kwmat and
ktmat are each genuinely complex (matching Fortran pentrc/torque.F90:842-847 rex/imx
assignments and pentrc/pitch.f90:376 integrand decomposition), not pure real / pure
imaginary. Per-surface matrix dumps against Fortran dcon/fourfit.F:1080-1082
(`kwmat_l`, `ktmat_l`) confirm element-by-element agreement with the dual-output
convention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-triangle reconstruction

Rewires `kinetic_energy_matrices_for_euler_lagrange!` and
`compute_kinetic_matrices_at_psi!` to populate `kwmat` and `ktmat` directly via the
new `integrate_pitch_gar_quadgk_wt` dual-output kernel, replacing the prior post-hoc
`complex(real(full), 0)` / `complex(0, imag(full))` split.

For A, D, H Hermitian-outer-product blocks (wz†wz, wx†wx, wy†wy) stored as upper
triangles, the lower-triangle reconstruction now uses different mirrors per half:

  kwmat[j,i] =  conj(kwmat[i,j])   (Hermitian)
  ktmat[j,i] = -conj(ktmat[i,j])   (anti-Hermitian)

Derivation: S_w = complex(0, imag(xint)) is pure imaginary so conj(S_w) = -S_w,
whereas S_t = complex(real(xint), 0) is pure real so conj(S_t) = S_t. Combined with
conj(factor) = -factor (factor = -i/(2n)), these mirrors recover Fortran's
independently-computed (j,i) slots exactly. Implemented via a `mirror_sign`
parameter to a shared `_assemble_hermitian!` closure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…itian kinetic A

The previous implementation computed `aamat = (amat_lu \ amat_kin)'` which equals
`(A_kin⁻¹ · A_kin)' = I` exactly. This silently zeroed `umat_diff = I - aamat` and
DROPPED the `im·psio_over_n · umat_diff · b1mat` term from `paat`, the
`im·psio_over_n · aamat · bkmat` term from `r1mat`, and the
`im·psio_over_n · umat_diff · cmat_kin` term from `r2mat` whenever `amat_kin` was
non-Hermitian.

Fortran dcon/sing.f:1004-1008 computes

    temp2 = amat_kin
    zgbtrs("C", ..., amatlu, temp2)        ! temp2 = A_kin⁻ᴴ · amat_kin
    aamat = CONJG(TRANSPOSE(temp2))        ! aamat = amat_kin^H · A_kin⁻¹

which is NOT the identity when `amat_kin` is non-Hermitian. This was hidden while
the kinetic producer gave pure-real `kwmat` and pure-imag `ktmat` (amat_kin stayed
Hermitian); exposing it required the correct dual-output kernel (prior commits) that
makes amat_kin genuinely non-Hermitian via the anti-Hermitian ktmat contribution.

Fix: `aamat = (amat_lu' \ amat_kin)'` — the `amat_lu'` backslash path solves Aᴴ·x = b,
giving `temp = A_kin⁻ᴴ · amat_kin` which after adjoint becomes the Fortran form.

With all three kinetic fixes in place (dual-output kernel, anti-Hermitian ktmat
mirror, and this aamat correction), the DIIID_kinetic_example DCON benchmark
reaches Fortran agreement:

  Re(et[1]): Julia 1.0481, Fortran 1.0507, err 0.25%
  Im(et[1]): Julia -0.3012, Fortran -0.3013, err 0.01%

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ts_prim

Clarifies that Julia stores the primitive (pre-Schur-reduction) geometric forms
D = χ₁·(g23 + q·g33·m/n) and the analogous E. The `_prim` suffix follows the
existing `fmats_prim` convention.

Previously, a reader diffing against Fortran fourfit would find that Fortran
saves two distinct splines — `dbats/ebats` (primitive) and `dmats/emats`
(kinetic-block overwrites with χ₁²·(g22+2q·g23+q²·g33) and related) — whereas
Julia's field named `dmats` actually holds the primitive form. The overwritten
form is only consumed by Fortran's alternate on-demand singular-surface Schur
path (`fkg_kmats_flag=.false.` in sing.f), which Julia does not implement.
Renaming makes the intent explicit and avoids a silent naming trap.

Pure rename; no numerical change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rface

# Conflicts:
#	src/Equilibrium/DirectEquilibrium.jl
…lytic resonance poles

Port the QuadGK energy-integration robustness from PR #220 into the
KineticForces energy integrator, with a corrected pole decomposition.

- Map x = E/T to u = 1-exp(-x) on [0,1), absorbing the Maxwellian weight
  into du, so the integral covers [0,infinity) without an xmax cutoff.
- Remove each resonance pole analytically by a Sokhotski-Plemelj
  decomposition: subtract R/(u-u_pole) and add back the elementary
  integral R*(log(1-u_pole)-log(-u_pole)). The same complex pole u_pole
  (x_pole = x_res - i*nu/Omega') is used in both the subtraction and the
  add-back, so the decomposition is exact and converges for nu > 0 as well
  as nu -> 0 (PR #220 subtracted at the real u_res but added at the
  complex u_pole, which diverges for collisional cases).
- Collisionless limit uses the causal Sokhotski-Plemelj branch.
- CGL has no resonance denominator: integrate the physical x-space
  integrand directly over [0,infinity) via QuadGK.
- ximag is accepted for signature compatibility but is now unused.

Adds find_resonance_energies and unit tests: resonance-root cases, a
collisional cross-check against direct x-space quadrature, and a
collisionless-limit continuity check. 110 kinetic unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e resonances

When kinetic_source="calculated" runs the full KF torque pipeline, the pitch
loop can hand the energy integrator parameter sets whose resonance polynomial
has a root at very large or very small x. Two corresponding NaN traps appear
in the Sokhotski-Plemelj analytic pole contribution:

- x_res > 700: exp(-x_res) underflows to zero, R becomes 0+0im, and
  R*(log(1-u_pole) - log(-u_pole)) evaluates as 0*(-Inf+iπ) = NaN. The
  resonance's true contribution is identically zero (the Maxwellian weight
  is below machine precision) — skip the resonance.
- nu_res / omega_prime = Inf (harmonic ν overflows at tiny x_res, or
  omega_prime ≈ 1e-30): exp(-x_pole) at infinite imaginary argument returns
  NaN. The resonance is infinitely collisionally broadened — no localized
  pole, no extraction needed; skip and let QuadGK integrate the smooth
  integrand directly.

Verified by the regression-harness solovev_kinetic_calculated case (which
exercises the calculated-source KF pipeline that the "fixed" fullruns do
not): pre-port et[1] = 19.914 - 0.628i, post-fix et[1] = 19.908 - 0.658i
(0.03% / 5% relative). 110 kinetic unit tests still pass.

Also updated:
- test/test_data/regression_solovev_kinetic_calculated/gpec.toml: replace
  removed dmlim/set_psilim_via_dmlim keys with develop's psiedge edge-scan
  band so the calculated-source case is runnable.
- test/runtests_fullruns.jl: refresh ex4 (Solovev kinetic multi-n) baseline
  to the develop-consistent eigenvalue 0.22325 — the prior -0.01248 dates
  from before develop's edge-scan and periodic-theta-endpoint evolution.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…schema

The DIIID kinetic benchmark's auto-generated gpec.toml used the deprecated
set_psilim_via_dmlim and dmlim keys, which develop removed (replaced with
the psiedge edge-scan band). The new FFS rejects unknown keys, so the
benchmark errored out before any compute. Drop the deprecated pair; the
existing psiedge = 1.00 (no edge-scan truncation) is the closest current
equivalent.

A/B verified with the new u-substitution energy integrator on the
merged tree: same DIIID benchmark numbers as the pre-port integrator
(FGAR T = 1.805 N·m vs Fortran 1.780, 1.39% err; FGAR dW = 0.160 vs
0.137, 16.94% err). The dW drift from PR #224's body (0.141 → 0.160)
is caused by the develop merge upstream of KF (most likely the
periodic-theta-endpoint fix changing bounce-data evaluation), not by
this port — the energy integrator is faithful.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
KineticForces — Complete PE→KF pipeline and validate DIIID-PENTRC benchmark
@logan-nc
Copy link
Copy Markdown
Collaborator Author

@claude review this PR

@claude
Copy link
Copy Markdown

claude Bot commented May 23, 2026

Code Review: PR #112 — KineticForces Module

This is a substantial PR that introduces the new KineticForces module. The architecture is sound — the callback injection pattern to avoid inverting ForceFreeStates→KineticForces load order is clean, and the struct-based parameter passing replacing Fortran globals is a good improvement. The benchmark scripts are thorough and the regression case infrastructure is welcome. Below are issues worth addressing before merge.


Bugs

Hardcoded absolute paths in examples/a10_kinetic_example/gpec.toml

eq_filename = "/Users/nlogan/Code/gpec/docs/examples/a10_ideal_example/fix_a100_k10_q2_bn010_prof1"
kinetic_file = "/Users/nlogan/Code/gpec/docs/examples/a10_ideal_example/a10_prof1.txt"

These paths will fail for everyone else. The committed example file is non-functional. Either document it as a template that requires local editing, or make the paths relative / use a placeholder.

bmax != B_vals[ibmax] check is dead code (Torque.jl, tpsi!)

bmax = maximum(B_vals)
ibmax = argmax(B_vals)
if bmax != B_vals[ibmax]
    error(...)
end

B_vals[ibmax] == bmax by construction — argmax returns the index of maximum. This error can never trigger; the check should be removed.

build_kinetic_metric_matrices is single-n only without adequate warning

At Fourfit.jl:3508 (in the diff), the y/z matrix loop uses n = intr.nlow as a fixed constant. For multi-n runs (npert > 1) this silently gives wrong matrices for n ≠ nlow. The function docstring doesn't flag this as a known limitation. At minimum, add an assertion or @warn when intr.npert > 1.


Performance

Any-typed fields on hot-path structs prevent type inference

In KineticForcesInternal, the fields dbob_m, divx_m, smats, tmats, xmats, ymats, zmats, xs_m are all typed ::Any. Similarly MethodResult.torque_profile is ::Any. These fields are evaluated inside the torque integration loops. Julia cannot infer return types through Any field access, so every interpolant call incurs dynamic dispatch. These should be typed (e.g., as a union of Nothing and the concrete interpolant type, or by making the struct parametric).

tspl spline rebuilt on every tpsi! call

A new CubicSeriesInterpolant tspl is constructed from scratch at each call to tpsi! (Torque.jl), which is invoked O(hundreds) of times per ψ quadrature. The pre-allocated buffer arrays (tpsi_B, tpsi_jac, etc.) on KineticForcesInternal suggest the intent to amortize setup cost, but the cubic_interp(xs, Series(hcat(...))) call at the end still allocates a new spline object each time. Pre-computing and caching the poloidal spline per ψ surface (e.g., via a small ψ → tspl dict) or restructuring to build it once per integrate_psi_ode call would avoid repeated construction.

expm allocation inside calculate_fcgl inner loop

expm = exp(im * twopi * intr.mfac * theta)

This allocates a new vector every θ iteration. Pre-allocate once and use @. expm = exp(im * twopi * intr.mfac * theta).


Code Quality

calculate_rlar is an incomplete placeholder but lacks a warning

calculate_rlar has a full docstring and no warning, but uses:

kappaint_val = sqrt(mean(abs.(dbob_m_f).^2))  # Placeholder: simplified estimate

This is not a physical implementation. calculate_clar correctly emits @warn "CLAR method not yet fully implemented, returning zero". calculate_rlar should do the same, or its docstring should state explicitly that it is a stub.

Bare catch swallows unexpected exceptions in find_kinetic_singular_surfaces!

try
    cond_vals[i] = evaluate_fbar_condition(...)
catch
    cond_vals[i] = Inf
end

A bare catch will silently absorb any error (e.g., a MethodError or BoundsError in the interpolants) and substitute Inf, causing silent incorrect behavior. At minimum catch e and rethrow unless the exception is LinearAlgebra.SingularException or DomainError.

Typo in citations.md

"providing the theoretical foundation for the the KineticForces functionality."


Minor

  • The FourceFreeStates rename dmatsdmats_prim and ematsemats_prim is a breaking change to the FourFitVars public API; the old names no longer exist. Downstream code (or any user calling ffit.dmats directly) will get a FieldError. If these are considered internal fields, no action needed — just noting it for release notes.

  • The new regression case solovev_kinetic_calculated adds test data but the test itself is commented out (the kinetic validation is explicitly deferred). This is useful infrastructure, but the regression harness will report this case as having no baseline — worth a note in the PR that this case is seeded for future use.

@logan-nc logan-nc marked this pull request as ready for review May 23, 2026 17:36
@logan-nc logan-nc requested review from matt-pharr and removed request for mfairborn23 and satelite2517 May 23, 2026 17:37
@logan-nc
Copy link
Copy Markdown
Collaborator Author

@matt-pharr you don't necessarily have to review all this - I just had to put someone. The merge of #224 into here actually contains all the documentation, plots and such.

I have claude working through it's own review points now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants