Skip to content

PerturbedEquilibrium/KineticForces - FEATURE - Write the perturbation that drives neoclassical transport - #543

Open
logan-nc wants to merge 9 commits into
developfrom
feature/kf-write-perturbation
Open

logan-nc wants to merge 9 commits into
developfrom
feature/kf-write-perturbation

Conversation

@logan-nc

@logan-nc logan-nc commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: none (harness @ 136b0fb)
  • Migration: none

The Lagrangian δ|B|/B and ∇·ξ⊥ spectra are what a neoclassical-transport calculation is driven by,
and GPEC already formed them — but only inside KineticForces, on the way to the torque, so getting
them out meant running our own NTV model. Another code wanting to run on GPEC's perturbation had no
way to read it.

Nothing kinetic enters them. They are the perturbed-equilibrium Clebsch displacement put through the
S,T,X,Y,Z geometric matrices and deweighted by J·B², so they belong to the perturbed equilibrium —
which is where the Fortran parent puts them: gpout_pmodb runs in gpec under pmodb_flag, and
pentrc reads that file back (read_pmodb). The Julia port fused the two executables and the output
landed on the wrong side of a seam that is no longer visible.

So the construction moves to PerturbedEquilibrium.compute_modb_modes and is written to
PerturbedEquilibrium/Response/{dBoB_lagrangian, div_xi_perp}, beside the displacement it
transforms and on the same Response/psi scale. An ideal run with no [KineticForces] block now
carries the spectra; KineticForces consumes them and gets smaller.

PerturbedEquilibrium +169 −2
KineticForces +20 −105

Two spectra, not Fortran's four

Fortran writes Eulerian δ|B|, Lagrangian δ|B|, ∇·ξ⊥ and the curvature term, each as modes and
again as θ-functions. Only the two independent spectra are written here:

  • the curvature term is exactly −(dBoB_lagrangian + div_xi_perp);
  • the Eulerian form differs from the Lagrangian by ξ·∇|B|/|B|, which Response/xi_clebsch_psi,
    Response/xi_clebsch_alpha and the equilibrium geometry already determine;
  • θ-space is Analysis.PerturbedEquilibriumModes.modes_to_theta, which FieldReconstruction.jl:983
    already cites as the reason not to inflate stored output. Verified on a real run: it returns
    (1608, 256, 1) θ-space arrays for both new datasets with n = 3 taken from Info/nlow.

Both relations are stated in the dataset annotations, so a consumer can recover either.

Single toroidal mode only, as before: the geometric matrices are assembled at n = nlow, so a
multi-n run writes nothing rather than something wrong.

What this does not change

The move relocated the output, not the computation — same inputs, same matrices, same θ resolution
derived the same way from the equilibrium, same loop order.

Measured twice, on two different bases:

Where to look

file what to check
PerturbedEquilibrium/FieldReconstruction.jl compute_modb_modes + _jbb_deweight!, moved whole. The ψ loop stays serial: its sticky bracket hints are shared mutable state, and threading it would make the spectra depend on the thread count.
PerturbedEquilibrium/Response.jl one call after reconstruct_physical_fields, guarded on ffs.npert == 1.
PerturbedEquilibrium/Utils.jl the two datasets and their annotations.
KineticForces/KineticForcesStructs.jl the part that shrinks: geometric-matrix build, ψ loop and deweighting helper gone; equil and metric left the signature with them.
test/runtests_h5_schema.jl the output contract: present with the right shape, on the ψ dimension scale, absent rather than empty when the run cannot produce them.

Tests

All re-run on the merged branch, not on the pre-merge one:

runtests_kinetic.jl 5073/5073 · runtests_multiion.jl 52/52 · runtests_ntv_limits.jl 98/98 ·
runtests_fullruns.jl 21/21 · runtests_h5_schema.jl 31/31 (6 + 16 + 9).

And the suites the merged-in work brought, since the conflicts were resolved in code they cover:
runtests_sfl_sampling.jl 7/7 · runtests_core_capped_knots.jl 8/8 ·
runtests_kinetic_validity.jl 27/27.

No test deck in test/test_data/ has a [PerturbedEquilibrium] section, so the computation itself is
exercised by the harness case solovev_kinetic_ntv and by the TC-24 runs above rather than by a unit
test; the unit tests cover the output contract.

Integrated, not rebased

This branch now contains develop @ 9fb32c9, including #398 (common straight-fieldline sampling,
core knot capping) and #414 (near-axis kinetic validity suppression). Three conflicts, all of them
"the output moved" against "the implementation that used to be here", resolved to the relocated
form; develop had not changed the dropped code beyond a formatter reindent, and
build_kinetic_metric_matrices, which the relocated copy calls, is unchanged.

Regression report

All 19 cases, origin/develop vs this branch:

19 cases / 268 quantities: 260 unchanged, 0 changed, 8 missing on both refs.

Ref 1: origin/develop                 @ 9fb32c9a6 (2026-10-20)
Ref 2: feature/kf-write-perturbation   @ 136b0fb9f (2026-10-20)
env (both): julia 1.11.7, x86_64-linux-gnu, manifest cb9237e9 (pinned), 16 threads/16 BLAS

The one case that runs the moved code:

Regression Report: solovev_kinetic_ntv
---------------------------------------------------------------------------------------------------------
Quantity                                   origin/develop  feature/kf-write-perturbation  Diff     Status
---------------------------------------------------------------------------------------------------------
NTV torque fgar [Re, Im]                   [1 elem]        [1 elem]                       0.0e+00  OK
NTV psi quadrature evaluations             45              45                             0.0e+00  OK
root-area-weighted total energy Re(et[1])  1.090570e-02    1.090570e-02                   0.0e+00  OK
# singular surfaces                        2               2                              0.0e+00  OK
singular psi locations                     [2 elem]        [2 elem]                       0.0e+00  OK
q0                                         1.900006e+00    1.900006e+00                   0.0e+00  OK
---------------------------------------------------------------------------------------------------------
Summary: 6 unchanged

Two rows that need a word

gal_resistive_pe reports all 8 quantities N/A — on both refs, and in a develop-vs-develop
run as well. Pre-existing, not this PR.

diiid_slayer_n1 passes here (SLAYER γ_Hz diff 1.2e-02, threshold 1e-1), but an earlier run of
this same branch flagged that row at 0.1834. It is the case's own noise, not a code change:
running examples/DIIID-like_SLAYER_example twice at one commit, one deck, 16 threads, one
machine gives Tearing/Roots/gamma[1] = 224.65453423959883 and 224.4712047505119 — a
difference of 0.1833, matching what the harness had reported between refs. omega and Q_root
move by ~1e-5 in the same pair, so it is the 2/1 root extraction specifically. The case's
noise_threshold therefore sits at the edge of its own run-to-run spread, which is worth fixing —
but deliberately not here, since re-pinning a gate inside an unrelated PR is how a real regression
gets through later.


🤖 Generated with Claude Code

https://claude.ai/code/session_01FabuzB6rkkv638ZUCzaAA2

logan-nc and others added 2 commits October 19, 2026 01:12
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.
…ma table

The schema table in hdf5-conventions.md enumerates the top-level groups and what each
holds, so a new group that is not listed there is invisible to anyone reading the schema
rather than the writer. `KineticForces/Perturbation/` is written once per run rather than
per species or per method, which is the part of its placement that is not guessable from
the group name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FabuzB6rkkv638ZUCzaAA2
@logan-nc logan-nc self-assigned this Oct 20, 2026
@github-actions github-actions Bot added the feature New capability label Oct 20, 2026
@logan-nc
logan-nc marked this pull request as ready for review October 20, 2026 04:33
@logan-nc
logan-nc requested a review from jaesun57 October 20, 2026 04:42
logan-nc and others added 6 commits October 20, 2026 08:44
…erturbation

# Conflicts:
#	src/KineticForces/Output.jl
…eoclassical transport

The Lagrangian delta|B|/B and div.xi_perp spectra are what an NTV calculation is
driven by, and GPEC already formed them -- but only inside KineticForces, on the
way to the torque, so getting them out meant running our own NTV model. Another
code wanting to run on GPEC's perturbation had no way to read it.

Nothing kinetic enters them. They are the PE Clebsch displacement put through the
S,T,X,Y,Z geometric matrices and deweighted by J.B^2, so they belong to the
perturbed equilibrium -- which is where the Fortran parent puts them: gpout_pmodb
runs in gpec under pmodb_flag, and pentrc reads that file back (read_pmodb). The
Julia port fused the two executables and the output landed on the wrong side of a
seam that is no longer visible.

So move the construction to PerturbedEquilibrium.compute_modb_modes, write it to
PerturbedEquilibrium/Response/{dBoB_lagrangian,div_xi_perp} beside the
displacement it comes from, and let KineticForces consume it. An ideal run with no
[KineticForces] block now carries the spectra; KineticForces gets smaller.

Harmonics only, as everywhere else in Response/: Analysis.modes_to_theta
reconstructs theta-space from any (psi, numpert_total) dataset, so the real-space
forms are a post-processing call rather than stored arrays. Only the two
independent spectra are written -- the curvature term is
-(dBoB_lagrangian + div_xi_perp), and the Eulerian delta|B|/B differs from the
Lagrangian by xi.grad|B|/|B|, which xi_clebsch_* and the equilibrium geometry
already determine.

Single toroidal mode only, as before: the geometric matrices are assembled at
n = nlow. Multi-n runs write nothing rather than something wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FabuzB6rkkv638ZUCzaAA2
Two of my own mistakes, caught by running them. The full-run block asserted the new
Response datasets appear in regression_solovev_ideal_example, which has no
[PerturbedEquilibrium] section at all -- no test deck does, so that assertion was
unsatisfiable by construction and the computation is covered by the harness case
solovev_kinetic_ntv instead. And the dimension-scale check used C index 0: H5DS
indexes file (row-major) dimensions, so Julia axis 1 of a 2-D dataset is C index 1,
per Utilities.HDF5Annotations.attach_scale!.

Kept as its own commit so the harness stamp from the parent still applies: src/ is
untouched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FabuzB6rkkv638ZUCzaAA2
…nto feature/kf-write-perturbation

Brings in the develop merge made on the branch this morning (a563002, including
#398's SFL-sampling and core-knot-capping work and #414's kinetic validity
suppression), and keeps the relocation on top of it.

Three conflicts, all of them "the output moved to PerturbedEquilibrium" against
"the KineticForces-side implementation that used to be here". Resolved to the
relocated form in every case, after checking that develop had not changed the
code being dropped: the only edit it made there was a formatter reindent of the
`_jbb_deweight!` signature, and `build_kinetic_metric_matrices`, which the
relocated copy calls, is unchanged apart from a space.

The Output.jl docstring conflict keeps develop's formatter list style and drops
the `perturbation` bullet, whose keyword argument no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FabuzB6rkkv638ZUCzaAA2
… code

Runs the repository's pinned JuliaFormatter (1.0.62, the pre-commit rev) over
the lines this PR introduces: the relocated docstring's list, the two annotation
entries and one test continuation. The format workflow is not a PR gate -- it is
disabled pending a repo-wide cleanup -- so the three pre-existing hunks the
formatter also wanted in FieldReconstruction.jl and Response.jl are left alone
rather than carried as churn in a feature PR (it would have rewritten
`((in - 1) * mpert + 1)` as `((in-1)*mpert+1)`, which is not an improvement).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FabuzB6rkkv638ZUCzaAA2
@logan-nc logan-nc changed the title KineticForces - FEATURE - Write the perturbation that drives the torque PerturbedEquilibrium/KineticForces - FEATURE - Write the perturbation that drives neoclassical transport Oct 21, 2026

This branch has not been deployed

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

Labels

feature New capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant