Repository navigation
Conversation
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
marked this pull request as ready for review
October 20, 2026 04:33
…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
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release note
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 gettingthem 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_pmodbruns ingpecunderpmodb_flag, andpentrcreads that file back (read_pmodb). The Julia port fused the two executables and the outputlanded on the wrong side of a seam that is no longer visible.
So the construction moves to
PerturbedEquilibrium.compute_modb_modesand is written toPerturbedEquilibrium/Response/{dBoB_lagrangian, div_xi_perp}, beside the displacement ittransforms and on the same
Response/psiscale. An ideal run with no[KineticForces]block nowcarries the spectra;
KineticForcesconsumes them and gets smaller.PerturbedEquilibriumKineticForcesTwo 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:
dBoB_lagrangian+div_xi_perp);Response/xi_clebsch_psi,Response/xi_clebsch_alphaand the equilibrium geometry already determine;Analysis.PerturbedEquilibriumModes.modes_to_theta, whichFieldReconstruction.jl:983already 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 fromInfo/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 amulti-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:
differing only in where the spectra are written) both spectra come back bitwise identical, ψ
grid included, and
KineticForces/fgar/total_torqueis unchanged to the last digit(292.91305542711694 for the perturbative even-phasing case). That branch pair predates Equilibrium - BUGFIX! - Sample flux surfaces on common straight-fieldline angles, cap core EL knots, and suppress near-axis kinetic terms #398 and
KineticForces - FEATURE! - Suppress kinetic terms inside the near-axis validity limit and report validity profiles #414, so it isolates the relocation cleanly but says nothing about this base.
solovev_kinetic_ntv— see thereport below. No test deck in
test/test_data/has a[PerturbedEquilibrium]section, so thatcase is where the computation actually runs.
Where to look
PerturbedEquilibrium/FieldReconstruction.jlcompute_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.jlreconstruct_physical_fields, guarded onffs.npert == 1.PerturbedEquilibrium/Utils.jlKineticForces/KineticForcesStructs.jlequilandmetricleft the signature with them.test/runtests_h5_schema.jlTests
All re-run on the merged branch, not on the pre-merge one:
runtests_kinetic.jl5073/5073 ·runtests_multiion.jl52/52 ·runtests_ntv_limits.jl98/98 ·runtests_fullruns.jl21/21 ·runtests_h5_schema.jl31/31 (6 + 16 + 9).And the suites the merged-in work brought, since the conflicts were resolved in code they cover:
runtests_sfl_sampling.jl7/7 ·runtests_core_capped_knots.jl8/8 ·runtests_kinetic_validity.jl27/27.No test deck in
test/test_data/has a[PerturbedEquilibrium]section, so the computation itself isexercised by the harness case
solovev_kinetic_ntvand by the TC-24 runs above rather than by a unittest; 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/developvs this branch:Two rows that need a word
gal_resistive_pereports all 8 quantitiesN/A— on both refs, and in a develop-vs-developrun as well. Pre-existing, not this PR.
diiid_slayer_n1passes here (SLAYER γ_Hzdiff 1.2e-02, threshold 1e-1), but an earlier run ofthis same branch flagged that row at 0.1834. It is the case's own noise, not a code change:
running
examples/DIIID-like_SLAYER_exampletwice at one commit, one deck, 16 threads, onemachine gives
Tearing/Roots/gamma[1]=224.65453423959883and224.4712047505119— adifference of 0.1833, matching what the harness had reported between refs.
omegaandQ_rootmove by ~1e-5 in the same pair, so it is the 2/1 root extraction specifically. The case's
noise_thresholdtherefore 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