Repository navigation
Equilibrium - BUGFIX! - Build the root-area weight with the inverse Fourier transform - #458
Conversation
…ourier transform compute_sqrtamat took each unit mode to θ-space with transpose(basis)/N instead of the inverse transform adjoint(basis), so the √(J|∇ψ|) convolution matrix came out with its rows reflected in m and scaled by 1/mtheta: a matrix with zero diagonal and norm ~300 times too small in place of the near-identity weight Fortran GPEC writes as J_surf_2. Everything built on it was wrong — the b̃→b̄ operator, the flux conform R = S·A, the field-space response matrices, the root-area-weighted forcing and response fields, and the conformed singular-coupling matrices. On the benchmark case the corrected operator matches the Fortran J_surf_2 to the grid difference (correlation 1.0000, diagonal within 0.05 %). The new test checks the diagonal, Hermiticity, the weighted Parseval identity on the complete basis, and the inverse operator on the Solovev surface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
…_n1 harness case Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
…inverse transform compute_sqrtamat now calls FourierTransforms.inverse for its backward step instead of spelling out adjoint(basis), so the pair it uses is exactly the forward/inverse pair whose round trip the transform tests check. Bit-identical to the previous commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
244b2e6 to
76808df
Compare
| # Root-area-weighted (coordinate-invariant) forcing field b̃ on the control surface: the | ||
| # applied coil spectrum pushed through the √(J|∇ψ|) weight operator. Tracks the field-space | ||
| # conversion that every ResponseMatrices/ field-space output depends on. | ||
| [quantities.forcing_b_rootarea_norm] |
There was a problem hiding this comment.
Definitely a good add, suprised it wasn't in there already.
ebursch
left a comment
There was a problem hiding this comment.
The implemented changes look good to me, I think using the built in fourier library consistently makes sense and the added test is great. I'm a bit worried about the remaining differences noted in my earlier comment, but my intuition is that they stem from a different part of the workflow. That said, maybe have claude look into it and double check it is unrelated to this issue and if not then I think this piece is good to go.
I see a factor of ~2 off for one pair of arrays and perfect agreement for another in that plot. The legend says there is still a helicity bug. I agree we should confirm that the agreement gets better if the same plot is remade with that bug fixed. Maybe this is a case where trying to make independent PRs for simplicity was the wrong call and we should stack - can you confirm the plot improved when #457 is merged into here? If so, I am fine merging as a stack as long as you are @ebursch (Edited to remove device-specific coil names; the per-coil comparison will be redone on the DIII-D-like example against Fortran GPEC so it can be shown here.) |
|
Do you think it's a factor of 2 even with the log scale @logan-nc ? |
|
For the historical record, @ebursch was right → we are investigating in more detail now |
|
Review package for this PR (republished at a new link; DIII-D-like and synthetic data only): https://claude.ai/code/artifact/3697420a-2578-4693-a150-c1890d39a657 |
Stack summary — OMFIT → Julia error-field tolerance migration (updated 2026-09-17)Nothing in this set merges without a third-party human review, the three small bugfix PRs included. Full page with the diagram, per-PR table, review order and links to every review package (a private claude.ai artifact until the author shares it; DIII-D-like and synthetic data only): https://claude.ai/code/artifact/408ddd36-e52c-42d8-8f5c-52f680f41da9
Benchmark, qualitatively. The original OMFIT project's case, rebuilt from that project's own run inputs, is reproduced once #458, #457 and #460 are in: dominant-mode singular values agree to 1e-4, singular-coupling rows match Fortran GPEC at 1.0000 correlation and within 0.5 % in norm, and every coil set's dominant-mode overlap agrees within 1 % once both codes use the same converged toroidal coil grid. The DIII-D-like example agrees with Fortran to 1e-4 throughout. Any comparison shown on these PRs uses the DIII-D-like examples only. Design issue for what comes after the stack: #461 (momentum-balance module fed by tabulated torque surfaces; stationary solvers only). |
|
On the earlier question about the per-coil plot: that figure compared against a run made before #457 was applied, and it used data that cannot be shown here, so it has been withdrawn. The comparison has been redone where it can be shown: the DIII-D-like error-field example against Fortran GPEC, both codes on matched DCON and coil-grid settings, with this fix, #457 and #460 applied. For the C-coil as built and for single F-coil hoops shifted 1 mm (x and y) or tilted 0.1°, the dominant-mode overlap from Julia's sensitivity table matches Fortran's finite difference of two runs to 0.01–6 % (the 6 % on the weakest hoop), and the dominant singular value to 0.04 % (core window ψ_N ≤ 0.9). Figure in the #448 review package: https://claude.ai/code/artifact/7eb87dcf-6a2d-4532-8bca-f58ceb76968e. On stacking: the three bugfixes are independent of each other and of the stack, but the stack's numbers need all three, so reviewing #458, #457 and #460 first and merging them ahead of the stack is the intent. |
|
It looks like these new artifacts are not public @logan-nc |
|
@logan-nc The develop branch plot is now all zeros? It used to be diagonal w/ slope = -1. There haven't been any commits since the first version, so it seems a bit suspicious.
|
|
@ebursch its just a colorbar normalization issue - the remake normalized the two panels to the same scale and they are off by ~1/M - claude is remaking it now but I believe it still looks the same as before structure wise |

Nothing in
Utilities/FourierTransforms.jlchanges. The forward transformft(data) = basis·data/Nwithbasis = exp(−i(mθ − nν))and the inverseinverse(ft, modes) = adjoint(basis)·modesare untouched, and their round-trip test still passes (20/20). What changes is thatcompute_sqrtamatstops rolling its own backward step and calls that inverse.History of the routine, so the reviewer can judge whether it was ever right:
compute_sqrtamat02b0f0b7(first version)(1/N) Σ_m c_m exp(−imθ_j)viacslth/snlthexp(+imθ)(forward wasexp(−imθ)/N, Fortraniscdftf)ece3e442(complex overhaul)(basis · c)/N= the sameexp(−imθ)/Nconj(basis)·modes=exp(+imθ)1c955043(basis transposed)(transpose(basis) · c)/N, mechanically adjustedadjoint(basis)·modes=exp(+imθ)inverse(ft, c)At every step the routine's backward kernel was the forward kernel
exp(−imθ)with a 1/N, while the library's inverse wasexp(+imθ)without one. The two refactors carried the hand-rolled line across faithfully; the mismatch dates from the first version. The routine was never tested directly: the developruntests_coordinate_invariant.jluses "a generic invertible complex operator standing in for the √weight", and thescripts/test_power_norm_invariance.jlthe old docstring cited is not in the tree.Why the early benchmarks could not see it. Reflecting m → −m is unitary and the 1/N is a scalar, so every norm or Parseval-type identity holds for the old operator up to a constant. The difference only shows in the structure. On the DIII-D example surface (ψ_N 0.99, mtheta 256, 35 modes; script in the review package):
S_old[:, m] == S_new[:, −m] / Nover the 25 mirrored modesJ_surf_2on the benchmark surfaceThe first row is the whole bug in one line: the old column for mode m is the correct column for mode −m divided by N, exactly. The Fortran operator is the independent reference for which of the two is right.
compute_sqrtamatbuilds the √(J|∇ψ|) convolution operator Σ that the coordinate-invariant field space rests on: the b̃→b̄ operator S = Σ/√A, the flux conform R = S·A,field_space_response_matrices, the root-area-weighted forcing and response fields written toPerturbedEquilibrium/, and the conformed singular-coupling matrices. It took each unit mode to θ-space withtranspose(basis)/Ninstead of the inverse transformadjoint(basis), so the matrix came out with its rows reflected in m and scaled by 1/mtheta: a zero diagonal and a norm about 300× too small, where Fortran GPEC'sJ_surf_2is a near-identity weight (diagonal ≈ 0.92).Found while benchmarking the error-field assessment of the original OMFIT project against its Fortran GPEC run, where the dominant-mode singular values were 190× too small and every per-coil overlap was off by factors of 10–900. With the fix the operator matches the Fortran
J_surf_2on that surface to the grid difference (correlation 1.0000, diagonal within 0.05 %) and the per-coil overlaps land on the Fortran values (median ratio 0.95; the remaining scatter is a separate helicity bug fixed in its own PR).What changes
src/Equilibrium/CoordinateInvariant.jl: the inverse step ofcompute_sqrtamatusesadjoint(ft.basis)(the documented inverse transform) with no 1/N; the docstring now states the correct identity and the Fortran correspondence.test/runtests_coordinate_invariant.jl: a testset on the Solovev surface checks the diagonal equals ⟨√(J|∇ψ|)⟩, Hermiticity, each column as the transform of the weighted unit mode, the weighted Parseval identity on the complete basis, androotarea_to_area_weight · area_to_rootarea_weight = I. The old code fails all but Hermiticity.regression-harness/cases/diiid_n1.toml: tracks‖forcing b̃‖(PerturbedEquilibrium/forcing_b_root_area), since no tracked quantity on develop depended on the operator.What moves: every field-space output (
ResponseMatrices/field-space matrices,forcing_b_root_area,forcing_b_area,response_b_*,SingularCoupling/C_*as stored) and anything read from them. Flux-space quantities and the harness's existing 47 quantities are unchanged.Review package (the operator before/after on the DIII-D example surface and the reflection check): linked in the comments as a claude.ai artifact.
cc @matt-pharr
Release note
The root-area weight operator (Σ/√A, Fortran's
J_surf_2) was built with the wrong inverse transform, reflecting it in m and scaling it by 1/mtheta; all coordinate-invariant field-space outputs and the conformed singular-coupling matrices were wrong. Fixed and tested against the weighted Parseval identity and against Fortran GPEC.Regression report
🤖 Generated with Claude Code
https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu