Repository navigation
PerturbedEquilibrium - FEATURE - Add the psi-resolved torque response matrices T_xe and T_source - #472
Conversation
… matrices T_xe and T_coil Build the cumulative torque response of a forward kinetic Euler-Lagrange solve on the applied root-area-weighted field (T_xe, the Fortran GPEC gpout_dw_matrix array) and in coil-set space (T_coil), write them under PerturbedEquilibrium/TorqueResponse/, and contract the run's own forcing into a cumulative torque profile. Gated by the new [PerturbedEquilibrium] compute_torque_response control. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
…r node Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
|
Not urgent — @matt-pharr, this is a self-contained addition (new output only, gated off by default) and can wait behind your current work. The one thing worth a skeptical eye when you get to it is the 13 % gap between the T_xe-contracted torque and the fgar quadrature, stated as an open question in the body. |
…response-matrix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
…que-response decks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
…urce column, including a forcing file T_coil becomes T_source: coil sets keep one column each, and any other forcing is one source named after its forcing file, so its 1×1 T_source/2 is T_applied. The kinetic example returns to develop's forcing.dat and only gains compute_torque_response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
|
@logan-nc I have been in the process of investigating the T_xe vs. fgar difference you mentioned for the past ~week, and I think it's a bug in the kinetic-matrix bounce averaging (pentrc/torque.F90, the wmmt/wemt bounce-averaged W vectors), not something from this PR. Claude thinks "the W vectors are conjugated inconsistently with the scalar |δJ|² route, so the self-consistent kinetic matrices effectively see the up-down-mirrored perturbation." The missing conjugation also exists in fortran GPEC and only affects non-up-down-symmetric equilibria, so Solovev doesn't show it. I am doing a bunch of fortran and julia runs both at the moment. It doesn't show up in the d3d example because you get lucky with a spurious cancellation that happens in 147131 specifically. You can hold 1.6.0 on fortran on this if you want but otherwise we can make it the first hotfix. I think it will still take another day or two to finish hunting down just because the runs to check it at higher and higher resolution take a while and I don't want to bog down feynman (especially since I graduated :^) ) |
|
Thanks @matt-pharr ! This would be a great bug hunt-down if it works out. Don't you have big new fancy ITER computers you can bog down instead of our little old feynman thats doing all it can before APS? |
|
I am currently running a convergence scan on 20 nodes on ITER's SDCC cluster, which has pretty low usage on a saturday night :) I am going to be running all of my harness/GPEC stuff there from now on I think, so long as everything is nice and effectively parallelized! |
|
Based on this scan using different upper and lower deltas, can confirm that the torque gap between FGAR and T_xe is only present for up-down-asymmetric equilibria, and is fixed by the aforementioned complex conjugate. The difference in torques between the two paths grows the greater a difference in upper and lower triangularity the equilibrium has. This bug is present in fortran as well, and I will open a very small PR there to fix it before 1.6. In the second figure you can see that with the fix we converge cleanly to the two disagreeing by ~0.01%, which could be due to some inherent difference in the two treatments still but regardless is much smaller than the present error, and is the same for all levels of up-down asymmetry. We can do more convergence testing for 2.0.0 while we are writing the paper IMO and that could expose more errors, that is the sort of thing has exposed all these little numerical improvements for Delta' calculations we have merged in recently, and I think a series of convergence plots for key quantities would make some nice appendix plots for the paper. |
|
Great sleuthing @matt-pharr! |
|
@logan-nc I can do so here since I have already posted the plots here. I opened a separate PR in fortran to correct this. |
…the kinetic Euler-Lagrange matrices see the physical perturbation
…-response-matrix
matt-pharr
left a comment
There was a problem hiding this comment.
Went through and checked everything for correctness, some docs are a bit egregious in terms of length and verbosity but I think it is not worth holding this on that since we want to do a docstring pass once the dust has settled from 2.0.0 API changes. Merging this now



Release note
kinetic_source = "calculated") on up-down-asymmetric equilibria move: the kinetic Euler-Lagrange matrices change, and with them the kinetic eigenfunctions, plasma response, δW and torques. Up-down-symmetric equilibria are unaffected up to round-off; the two tracked Solovev kinetic cases move by ~1e-7 relative in the energies and by 0.02–0.16 % in fgar and the ξ/b^θ norms after one adaptive ODE step flips. Thesolovev_kinetic_calculateddeck also turns oncompute_torque_response, so three torque routes are now tracked there (harness @ 7333488)A forward kinetic solve (
integrator = "forward",kinetic_factor > 0) can now write the ψ-resolved torque response matrices of Fortran GPEC'sgpout_dw_matrix:T_xe(ψ, m, m′)on the applied root-area-weighted control-surface field andT_source(ψ)in forcing-source space (one column per coil set, or the forcing file as a single source), underPerturbedEquilibrium/TorqueResponse/, gated by the new[PerturbedEquilibrium] compute_torque_responsecontrol. The run's own forcing is contracted into a cumulative torque profile, andtorque_profiledoes the same for any applied spectrum.This PR also fixes a conjugation error in the bounce-averaged W vectors that build the kinetic Euler-Lagrange matrices. Because of it, self-consistent kinetic runs on up-down-asymmetric equilibria responded to the up-down-mirrored perturbation. With the fix, the torque from
T_xeand the KineticForces fgar quadrature agree to 0.01–0.05 % on converged grids. Fortran GPEC has the same error, fixed in PrincetonUniversity/GPEC#357.Regression report
Full harness, all 19 cases,
develop@ 1cb9bf0 vs this branch @ 7333488 (develop merged in; SDCCregress_fanout.sh, harness and case files from this branch, Julia 1.12.6, CI manifest pin):diiid_slayer_n1SLAYER roots (Q ≤ 3.8e-07, ω/γ ≤ 0.014 Hz), below its noise thresholds, OKsolovev_kinetic_calculated(12 of 19 quantities) andsolovev_kinetic_nuzero(5 of 14), tables belowsolovev_kinetic_calculated:T_xe applied-forcing torque(N/A on develop)gal_resistive_pe, all 8 quantities; same on develop, not from this PRBoth changed cases are Solovev, which is up-down symmetric. There the conjugation fix is an identity in exact arithmetic, and the energies move only at ~1e-7 relative. In
solovev_kinetic_calculatedthat is enough to flip one adaptive ODE step (606 → 607 saved), which accounts for the larger moves in the ψ-resolved ξ and b^θ norms (0.11–0.16 %) and in fgar (0.02 %). The newTorqueResponse/total_torque(2.285706e-03 N·m) equals the tracked boundary-response torqueEnergies/toroidal_torqueto every printed digit. On this deck it is 0.8 % above fgar, as expected atmpsi = 16(see the convergence notes below).No tracked case is up-down asymmetric with calculated kinetic matrices, so the harness cannot show the fix's real effect. The evidence for that is in the notes below.
Notes for reviewers
Construction. Per stored node
k,T_xe(ψ_k) = K†·[U₁(ψ_k)†U₂(ψ_k)]·K · 2n·i/(2μ₀)withK = U₁ₗ⁻¹·D·P·R(D = diag(1/(χ₁(m − n·q_lim)·2πi)),Pthe flux-space permeability,R = S·Athe b̃ → Φ conform). This is the Fortrandwks → bsurfmat → gind → gres → ptofchain collapsed to one edge inverse, which the globally consistentu_storebasis allows; a scratch check against the literal Fortran route (U₂U₁⁻¹,U₁/U₁ₗ, per-step) agreed to 2e-16 at a mid-ψ node.T_source = Ks†·[U₁†U₂]·Ks·2n·i/(2μ₀)withKs = U₁ₗ⁻¹·D·P·M.Decisions the Fortran left open, and what was chosen:
T_xematches the Fortran array (no/2; "general (2×) inductance"). The physical torque isb̃†·T_xe·b̃/2, applied intorque_profileand in the storedT_applied/total_torque. With this conventionreal(T_applied[end])equalsEnergies/toroidal_torqueexactly (both reduce to−n·Im(ξ†U₂U₁⁻¹ξ)/(2μ₀); the vacuum part ofwt0is Hermitian and drops out). Note the OMFIT consumerntv_limits_of_efc.pyomits the/2and therefore reports twice the torque;pypec/post.pyoptimize_torquekeeps it.T_xeacts on b̃ (forcing_b_rootarea, the basis of the stored response matrices), not on unit-norm Φ_x —R†·gres·Ris exactly Fortran'sptof†·gres·ptofsinceptof = Σ√A = R.M. With coil forcing,M[:, c]is each coil set's unit-norm Φ_x as built, at its deck currents, on the run's own boundary grid (Fortrancoilmnfromfield_bs_psicarriescoil%curtoo), soΣ_c M[:, c]reproduces the run's forcing to 1e-10 andsum(T_source)/2 == T_appliedat every node. Any other forcing is a single source: the run's applied Φ_x, named after its forcing file (forcingfor anRMPFieldSum), so its 1×1T_source/2 == T_applied. Driving the sources at scale factorssgivess†·T_source·s/2. Reading several forcing files as separate sources is left for later.imag(b̃†T_xe b̃/2) = 2n·δWof the whole Euler-Lagrange energy (ideal + kinetic), not the KineticForcestotal_energy(kinetic only) — named and annotated accordingly.nn_low != nn_high; the2nprefactor is per harmonic.Self-consistency test (
test/runtests_torque_response.jl, Solovev calculated-kinetic fixture with two window-pane coil sets; numbers from a3301e5, before the conjugation fix): the run's forcing referred to the solution basis directly fromu_storegivesT = 1.1060988185e-3 + 2.6342971530e-2 iN·m;T_applied[end] = 1.1060988185e-3 + 2.6342971530e-2 i(agrees to 1e-15),Energies/toroidal_torque = 1.1060987849e-3(3e-8 relative, the Λ-route algebra). Also checked: the same contraction at an interior node, Hermitian/anti-Hermitian split,T_sourcevsM̃†T_xe M̃, the single-source file case, shapes vsInfo/mn_index, and the HDF5 metadata contract on the new group.Benchmark against the NTV quadrature: the 13 % gap is resolved. This body used to report
T_xe/fgar = 1.130 onexamples/DIIID-like_error_field_exampleatmpsi = 64and left the cause open. It had two causes, and neither is in this PR's construction.mpsi = 64neither route is converged. fgar alone moves 21 % betweenmpsi64 and 128, and the gap changes sign. On every deck tried,T_xe/fgar scatters by about ±4 % belowmpsi ≈ 160. It is meaningful frommpsi ≈ 192and converged by 576–1024. Compare total torques only at convergedmpsi, or compare the ψ-resolved densities.T_xeover fgar across ψ ∈ [0.905, 0.935], around the kinetic resonance at ψ ≈ 0.92, and no resolution or tolerance setting moved it. The bounce-averaged W vectors that build the six kinetic Euler-Lagrange matrices were formed asw_m = ∫ conj(f_m)·pl, which pairs the D_m amplitudes with conjugated phases. The matrices therefore encoded the drift-kinetic action of the up-down-mirrored perturbation, while fgar's scalar|∫ jvtheta·conj(pl)|was already physical. Conjugating the bounce averages (wmu_ba,wen_bainsrc/KineticForces/BounceAveraging.jl) makes the matrix route's complex δJ equal fgar's for each pitch angle and bounce harmonic: δJ / conj(bj) has a median of 0.999987 + 0.000001i over 4992 samples, where the old pairing scattered across the complex plane. Up-down-symmetric equilibria such as Solovev are immune, which is why no regression case caught it.DIII-D-like C-coil deck, calculated kinetic matrices, forward integrator (scratch decks on SDCC, not committed). It differs from the deck above in two ways: it uses the example's own kinetic profiles, and the q = 5.2 truncation puts ψ_lim at 0.978.
T_xe[N·m]T_xe/ fgarmpsi = 256mpsi = 256grid_type = "auto"grid_type = "auto"Both torques move with the fix, because the self-consistent ξ changes too.
Two fixed-boundary TokaMaker families test whether the gap tracks up-down asymmetry (R₀ = 1.7 m, a = 0.6 m, κ = 1.7, mean δ = 0.35, β_N = 1.5). One varies δ_upper − δ_lower from 0 to 0.5 and the other varies κ_upper − κ_lower, with 11 shapes each. Every shape ran at 26 grids,
mpsi128–1024, with and without the fix: 1144 runs, none failed. At converged grids (mean overmpsi576–1024) the gap before the fix grows with the asymmetry, to between −0.57 % and +1.09 % across the Δδ family and to −0.28 % across the Δκ family. With the fix it is 0.01–0.05 % for every shape. The symmetric shapes give the same result with and without the fix. The Δδ plots are in the comment thread above.Fortran shows the same thing (PrincetonUniversity/GPEC#357). On its DIII-D-like
run_kinetic_exampleatmpsi = 256, fgar ÷ the self-consistent torque goes from 0.995 to 1.0016 with the fix. On its Solovev example the output is identical with and without the fix, to every printed digit.Caveat observed on the way: with the example's default
psihigh=0.995(domain ending at q=6.2, just past the q=6 rational;cond(U₁) ~ 1e7–1e9over the last nodes, tol 1e-8) the identity broke —T_xeroute −110 N·m vs boundary route 1.95 N·m vs fgar 1739 N·m, andU₂U₁⁻¹at the last stored node differed from the free-boundaryW_plasmaby 300 % and from the previous node by 116 %. The construction is only as good as the edge solution it is built from; whenEnergies/toroidal_torqueandTorqueResponse/total_torquedisagree, the solve's edge is ill-conditioned. This is a property of the forward solve near a rational, not of this PR, but worth knowing before trusting either torque on such a deck.Also in passing: the stale
kinetic_sourcedocstring inForceFreeStatesControl("calculated (PENTRC — not yet implemented)") now describes the implemented path.🤖 Generated with Claude Code
https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu