Repository navigation
KineticForces - FEATURE - Per-bounce-harmonic torque density output - #484
Conversation
The torque density is already computed harmonic by harmonic -- the l loop in integrate_psi_quadgk fills harm_vals and then reduces it -- but only the sum was ever kept. Multi-code NTV benchmarking wants the breakdown: the ITPA TC-24 activity compares contributions from l = 0, +/-1 ... +/-5 across codes, and the Fortran parent already emits dTdpsi_fgar(psi, ell), so Julia could not answer that request at all. KineticForces/<method>/ gains dTdpsi_ell on a new ell dimension scale, shape (npsi, nell) in Julia and (nell, npsi) to a C-ordered reader. Summing over l reproduces dTdpsi. Three things needed care: - The per-l log is permuted by the same sortperm as psi. QuadGK visits nodes in refinement order, not in psi order, so an unsorted log would misalign with the grid silently rather than erroring. - harm_vals is reused at every psi node, so the log stores a copy. - combine_species_states interpolates onto a union grid, and the benchmark runs multi-species, so the summed total needs per-l as well as the per-species groups. The interpolation is now a shared helper used by both the total and each l column, and the combined dtdpsi is taken as the row sum of the per-l matrix, which makes the sum rule exact by construction rather than true to interpolation roundoff. A species without per-l data leaves the combined matrix empty instead of erroring. Quadrature adapts on the summed integrand, so a sharply resonant individual harmonic can be less well resolved than the total; the annotation says so. Profiles still come from the first toroidal mode only, so for npert > 1 the sum over l matches dtdpsi but neither integrates to total_torque. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FabuzB6rkkv638ZUCzaAA2
…bout the Fortran layout PENTRC writes dTdpsi_<method> as (i, ell, psi) in Fortran order, so the h5py view of dTdpsi_ell, (nell, npsi), is not the Fortran orientation. MATLAB reverses HDF5 dimensions too, so it is not a C-ordered reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…harmonic torque log The near-axis validity suppression scales each node's summed torque density by an envelope, but the per-harmonic log kept the unscaled harmonics. Between psi_c and 2 psi_c, sum over ell of dTdpsi_ell then differed from dTdpsi. The multi-ion combined total takes its dtdpsi from that row sum, so it lost the suppression as well. Scale the per-harmonic log by the same envelope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
matt-pharr
left a comment
There was a problem hiding this comment.
Looks good @logan-nc, I pushed a fix for one small bug that claude found with the envelopes changing between calls of the envelope function, and edited the PR description to accurately reflect the structure of the output from the fortran code.
|
One note for anyone using The per-harmonic quantity, its ℓ ordering (−nl first), and the total (a plain sum over ℓ) match Fortran exactly. Both write Not blocking. It's already in the caveats; this just records where it comes from in the Fortran. |
Release note
KineticForces/<method>/now writesdTdpsi_ell, the torque density resolved by bounce harmonic ℓ,on a new
elldimension scale. The values were already computed one harmonic at a time and thensummed; only the sum was kept. Multi-code NTV benchmarking compares the ℓ breakdown, and the Fortran
parent already emits it.
Regression report
Run after merging
origin/developinto the branch (it was 195 commits behind, which would have attributed every intervening develop change to this PR). The merge was clean and the report confirms it: nothing moves against current develop.Full harness on ITER SDCC (Julia 1.12.6, all 19 cases, same refs
84b7e4800vs9aeee4d67): 17 unchanged;gal_resistive_peN/A on both refs; one changed row,diiid_slayer_n1SLAYER γ_Hz at 0.02%, which is that case's known run-to-run scatter (this PR does not touch SLAYER).Review package
https://claude.ai/artifact/4iEvyhb9RQ3S7Xv1gxmVLB
Four figures, so what this adds can be judged without reading the ℓ loop: the decomposition itself
per species and phasing; the sum rule with its 1e-16 residual, which is what makes it a
decomposition rather than a second estimate of the torque; the share of each harmonic's integral
carried by its three largest ψ nodes, which is the honest limit on quoting the high harmonics; and
the per-ℓ comparison across the 195-commit
developmerge. All drawn from the feature's own output.The link is internal and may need access granted — ask @logan-nc, or ask and the page can be sent as
a file instead.
Notes for reviewers
Why
The torque density is already computed harmonic by harmonic — the ℓ loop in
integrate_psi_quadgkfills
harm_valsand then reduces it — but only the sum was ever kept.Multi-code NTV benchmarking wants the breakdown. The ITPA TC-24 activity is comparing the
contributions from ℓ = 0, ±1 … ±5 across GPEC, NTVTOK and MARS-K, and the Fortran parent already
writes the ℓ-resolved profile (
dTdpsi_<method>in PENTRC's NetCDF output), so the Julia code couldnot answer that request at all.
What
KineticForces/<method>/gainsdTdpsi_ellon a newelldimension scale. Shape is(npsi, nell)in Julia, following the repo's
(psi, m)axis convention, so a C-ordered reader (h5py, xarray) sees(nell, npsi). This is not the Fortran orientation: PENTRC definesdTdpsi_<method>as(i, ell, psi)in Fortran order (pentrc/torque.F90), which those readers see as(psi, ell, i)with real and imaginary parts split on
i. A Python comparison against Fortran output thereforeneeds a transpose; the attached
psiandelldimension scales let xarray align the axes by name.Summing over ℓ reproduces
dTdpsi. Per-species groups get it with no extra code.Three things that needed care
sortpermaspsi. QuadGK visits nodes in refinementorder, not in ψ order, so an unsorted log would misalign with the grid silently rather than
erroring.
harm_valsis reused at every ψ node, so the log stores a copy.combine_species_statesinterpolates onto a union grid, and multi-ion runs need per-ℓ on thesummed total as well as per species. The interpolation is now a shared helper used by both the
total and each ℓ column, and the combined
dtdpsiis taken as the row sum of the per-ℓ matrix —which makes the sum rule exact by construction rather than true to interpolation roundoff. A
species without per-ℓ data leaves the combined matrix empty instead of erroring.
Caveats
Quadrature adapts on the summed integrand, so a sharply resonant individual harmonic can be less
well resolved than the total. Panel boundaries sit at every ℓ's kinetic-resonance surface, which
limits but does not remove this. Profiles still come from the first toroidal mode only, so for
npert > 1the sum over ℓ matchesdtdpsibut neither integrates tototal_torque.Testing and verification — complete; ready to review and merge
In-tree tests. New assertions in
runtests_multiion.jlandruntests_kinetic.jlcover the sumrule through
combine_species_states, the on-disk shape and orientation, the ℓ-sum on disk, thedimension-scale attachment, and the mixed case where one species lacks per-ℓ data. The on-disk
orientation was checked from both sides: HDF5.jl reads
(npsi, nell)and h5py reads(nell, npsi),with values indexed as intended.
(An earlier version of this description quoted pass counts of 64/64 and 1845/1845. Those were
measured at the original base, before this branch was brought up to
develop, so they are no longerthe right numbers to cite — CI below is the live evidence.)
CI. All checks green at the head of this branch, including
runtestson both Julia 1.11 and1.x. This is the load-bearing result for the
developmerge described next.Branch is current with
develop. It was 195 commits behind, which would have made a--refs develop,<branch>harness comparison attribute every interveningdevelopchange to thisPR. Merged (cleanly) before the report above was run, so that report isolates this PR's changes.
The per-ℓ output was verified to survive the merge, on a production case. The sum rule alone
could not establish this:
combine_species_statesnow derives the total as the per-ℓ row sum, soΣ_ℓ ≡ total holds by construction and cannot fail. Nor could the regression harness — its kinetic
cases do not set
bounce_harmonic_max, so they run the default of 1, covering ℓ ∈ {−1,0,+1} andtracking only summed and per-species totals. So an ITER-scale n=3 case from the ITPA TC-24 activity
was rerun at this branch's head and its ℓ-resolved integrals compared against the same case on the
pre-merge build:
Ordering and signs are the invariants worth gating here, because they are grid-independent and are
what an ℓ reversal, an index shift or a one-sided ±ℓ symmetrization would break. They hold exactly.
The sub-percent movement comes from
develop, not from this PR. The ξ grid thatb_nis built onwent from 1642 surfaces to 886, so the fgar quadrature placed its 345 nodes almost entirely
differently, while the field itself agrees to 0.1% at matched ψ_n. That grid is the saved
Euler-Lagrange step record (
psi_store), and the drop is the expected effect of #480 (per-column ODEabsolute tolerance,
save_interval3 → 1). Its harness report shows the same ratio ondiiid_n1(2675 → 1440 saved steps, 0.54 against 0.54 here), and its release note documents NTV moving at the
percent level.
That rerun and its comparison script live in the TC-24 benchmark repository, since they need that
activity's ITER equilibrium — so they are not reproducible in-tree, and the numbers are offered as
evidence rather than as a check you can repeat here.
Nothing outstanding from my side. The only thing gating merge is review approval.
🤖 Generated with Claude Code