Repository navigation
Conversation
…mpute_contra_displacements as a keyword The eigenmode energy decomposition runs the contravariant convolution unregularized and has no PerturbedEquilibriumControl, so the width becomes a keyword; reconstruct_physical_fields passes ctrl.reg_spot through unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s, result, and surface geometry First piece of the eigenmode energy decomposition: the [EnergyDecomposition] control struct, the result struct whose fields are the HDF5 leaf names, and the per-surface geometry (gradient and tangent bases, metric, B² derivatives) that every kernel shares instead of re-deriving it as the Fortran does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gy decomposition Shear S, curvature projection κ_ψ, parallel-current ratio σ, and the three destabilizing terms K1 = |∇ψ|²σS, K2 = μ₀B²σ², K3 = 2p'κ_ψ per θ point, following the Fortran gpeq_shear/gpeq_curvature/gpeq_K with the periodic spline derivative of the shear geometry term. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nmode and verify its curl has no normal component eigenmode_modes runs the unregularized gpeq chain for the edge eigenvector wt[:, k] on the equilibrium knots inside the solution domain, splining ξ and ξ' from the ODE grid and recomputing ξ_s from the ideal Euler-Lagrange matrices at each node as the Fortran gpeq_sol does; surface_fields! puts one surface on the θ grid with the band-limited ξ_n of gpeq_normal; effective_field! adds the ξ_n(μ₀ j × n̂) correction of gpeq_c; curl_residual! evaluates (∇×b_eff)·∇ψ in mode space. The knots matter: between them the metric coefficients, the matrices and the bicubic geometry are interpolated separately and the identity only holds to O(h⁴) (1e-4 on the mpsi = 16 Solovev fixture, O(1) next to a rational surface); on the knots the Solovev test reaches 5e-13. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ndard-form energy densities on a surface One θ pass per surface produces |b_eff|²/μ₀ with its component split and the three K_i|ξ_n|² terms (Fortran gpeq_epf/gpeq_dst) and, for the standard form, |b|²/μ₀, j·(b×ξ*) and (∇·ξ)*(ξ·∇p) (gpeq_firstform); surface_integral is the periodic Riemann sum the Fortran uses. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…boundary eigenmodes over psi decompose_energy runs the per-surface kernels threaded over the equilibrium knots of the solution domain, integrates the effective-field and standard-form pieces in ψ with a cubic spline, scales them by 2μ₀/ψ₀² to the normalization of eigenmode_plasma_energies, and stores the two consistency checks (curl residual over the knots, mismatch against ep[k]) as data. The per-surface fields now form Jξ^ψ, the covariant b_i and ξ_n pointwise from the exact local metric instead of the mode-truncated gpeq_contra/gpeq_cova/gpeq_normal arrays: with the truncated arrays the |V|² and K₂|ξ_n|² pieces no longer cancel and the effective-field total sat 21 % below ep[1] on the Solovev fixture independent of the ψ grid. Pointwise, both forms converge with the grid: relative mismatch against ep at mpsi = 16/48/128 is 1.4e-2/3.4e-3/2.3e-3 (effective) and 1.2e-2/1.4e-3/2.5e-4 (standard) for the least-stable mode; the knot curl residual stays at 1e-13. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rceFreeStates/EnergyDecomposition The eigenmode δW_p decomposition sits beside FreeBoundaryStability, with the profiles on the decomposition ψ scale, the optional Densities/ subgroup on a θ scale, zero-extent datasets for a disabled form, and a full long_name/units/dims table so the schema test holds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he [EnergyDecomposition] section The stage runs right after the ForceFreeStates output is written, so it is available with force_termination = true and without any forcing definition; it warns and skips when the solve has no dense ξ solution or no free-boundary eigenmodes, and main returns it as energy. energy_decomposition is the scripting entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ev and DIII-D-like ideal decks Both decks integrate forward with vac_flag, so the eigenmode energy decomposition runs and its group is produced for the regression cases built on them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…in solovev_n1 and diiid_n1 The two δW_p route totals, their relative mismatch against eigenmode_plasma_energies, and the relative (∇×b_eff)·∇ψ residual of the least-stable eigenmode. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nmodes A short page with the two forms of δW_p, the K kernel, the normalization against eigenmode_plasma_energies, the TOML section, the output group and the API, plus the schema and architecture entries. Docstring cross-references of the unexported helpers become plain code spans since they are never rendered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ngth in the energy-decomposition kernel σ, K₂ and κ_ψ now take B² = (χ'/J)²(g_θθ + 2q g_θζ + q²g_ζζ) from a bicubic of the straight-field-line metric on the equilibrium grid instead of the equilibrium's separate |B| spline, which differs from it by up to several percent near the edge of the Solovev example; the difference broke the |V|² ↔ K₂|ξ_n|² cancellation of the effective-field form. Solovev mpsi = 128: effective-form mismatch against ep[1] 1.08e-2 → 7.9e-3, standard form unchanged at 1.7e-5; mpsi = 256: 9.9e-4 and 8.8e-3. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed field as δB in the energy-decomposition page Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and the energy decomposition over through gpec.h5 The [EnergyDecomposition] stage still runs inside main after the force-free-states solve, but its result no longer rides on the returned NamedTuple: it is written to ForceFreeStates/EnergyDecomposition/ in gpec.h5, and scripts get it from energy_decomposition(result.ffs), which is now exported next to solve and perturbed_equilibrium. This matches how the LocalStability stage hands its result over and keeps main's (; ffs, pe, slayer) shape that runtests_result_struct.jl pins. Also restore develop's text where the earlier formatter run had reflowed unrelated lines in GeneralizedPerturbedEquilibrium.jl and FieldReconstruction.jl, and make the main-stage test read the decomposition back from gpec.h5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oolkit/GPEC into feature/energy-decomposition
…and exports clear of the resonant-coupling lines develop's ResonantCoupling additions land on the same lines the branch used for its include and exports in PerturbedEquilibrium.jl and for its key-file bullet in architecture.md. Put the branch's lines where develop did not touch (after the FieldReconstruction include, in the type-export block, after the Response.jl bullet) so the branch merges cleanly without a merge commit. EnergyDecomposition.jl needs nothing from Response, SingularCoupling or Utils at parse time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The review found one critical integration issue and two moderate correctness/schema issues.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
Adds an optional energy-decomposition diagnostic for free-boundary eigenmodes, with effective-field and standard-form calculations, HDF5 output, tests, regression tracking, and documentation.
Changes:
- Adds decomposition APIs and pipeline/TOML integration.
- Adds regression quantities and example configuration.
- Adds tests, documentation, and HDF5 schema guidance.
- Refactors displacement regularization to a keyword argument.
Unresolved findings: critical integration gating issue (4 votes), moderate disabled-field HDF5 shape issue (3 votes), and moderate truncated-domain integration issue (1 vote).
| File | Description |
|---|---|
test/runtests.jl |
Registers energy-decomposition tests. |
test/runtests_energy_decomposition.jl |
Tests calculations, toggles, metadata, and output. |
src/PerturbedEquilibrium/PerturbedEquilibrium.jl |
Includes and exports the new APIs. |
src/PerturbedEquilibrium/FieldReconstruction.jl |
Refactors regularization argument handling. |
src/PerturbedEquilibrium/EnergyDecomposition.jl |
Implements decomposition, diagnostics, and HDF5 writing. |
src/GeneralizedPerturbedEquilibrium.jl |
Integrates the optional stage and scripting API. |
regression-harness/cases/solovev_n1.toml |
Tracks decomposition diagnostics. |
regression-harness/cases/diiid_n1.toml |
Tracks decomposition diagnostics. |
examples/Solovev_ideal_example/gpec.toml |
Enables the feature in the example. |
examples/DIIID-like_ideal_example/gpec.toml |
Enables the feature in the example. |
docs/src/energy_decomposition.md |
Documents theory, usage, outputs, and API. |
docs/src/citations.md |
Adds supporting citations. |
docs/src/api.md |
Documents the scripting entry point. |
docs/make.jl |
Adds the documentation page to navigation. |
docs/development/references.md |
Records implementation references. |
docs/development/hdf5-conventions.md |
Documents the new output schema. |
docs/development/architecture.md |
Updates the architecture overview. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ForceFreeStates.require_solution(result, "energy decomposition") || return nothing | ||
| ForceFreeStates.require(result, :free_boundary, "energy decomposition") || return nothing |
| :effective_b_curl_residual_density, :b_squared_density, :current_coupling_density, :pressure_compression_density) | ||
|
|
||
| # Empty result arrays are written zero-extent (the file's not-computed sentinel). | ||
| _h5_value(x::AbstractArray) = isempty(x) ? similar(x, 0) : x |
logan-nc
left a comment
There was a problem hiding this comment.
Consolidated review from three independent passes (structural, performance, code clarity), each claim checked against the source. Ordered by what I'd want settled before merge. The physics itself was not reviewed here — see the end.
1. Blocking: the stage aborts the run instead of skipping
run_energy_decomposition (GeneralizedPerturbedEquilibrium.jl, new hunk) gates only require_solution and require(:free_boundary), which warn and return. But decompose_energy throws ArgumentError on nlow != nhigh and mats.kinetic !== nothing (EnergyDecomposition.jl:698–699). A legal multi-n or kinetic deck that carries [EnergyDecomposition] therefore kills the whole run after FFS — before PE, KineticForces or SLAYER — contradicting the stage docstring ("warned and skipped, like every optional stage") and docs/src/energy_decomposition.md. Copilot flagged the same gap. ~10 lines: warn-and-return for both conditions in the driver, keep the throws for scripted use.
2. Decide before merge: where the output lives
- The group
ForceFreeStates/EnergyDecomposition/bakes "free-boundary eigenmodes only" into the schema, while the producer is a PE-module stage. Free-boundary enters the code through justwt[:,k](:342) andep[k](:814); adecompose_energy(ffs, edge::Vector; reference=nothing)entry would serve fixed-boundary modes and forced responses at ~half a day, andPerturbedEquilibrium/…would then be the natural home. HDF5 renames are clean breaks here with no shim, so the placement is worth deciding now even if the generalization waits. eigenmode_index = copy(eigenmodes)(:719) echoes a control value into an output group (hdf5-conventions.md: never echo control-struct values outsideInput/). It is defensible as the coordinate of theeigenmodedimension — in which case attach it as a dimension scale; onlypsiis attached today (:926–927).
3. Structural: the geometry block shadows Equilibrium
surface_geometry!(:160–166) reproducesEquilibrium.flux_surface_metric(FluxSurfaceMetrics.jl:16–23) line for line — the function whose docstring declares it "the single source of truth for the |∇ψ| computation reused across the FFS and PE modules" — then extends it withw21/w22/w31/w32and the tangent basis. Extendflux_surface_metric(or add a sibling in Equilibrium) and call it, rather than carrying a second implementation in PE.- The same block re-interpolates geometry with 13 bicubic evaluations per point at the equilibrium knots, where
rzphi_*.nodal_derivs.partials[1:3, i, j]already holds f, ∂f/∂ψ, ∂f/∂θ andmake_metric(Fourfit.jl:193–197) andGeometryProfiles.jl:44read them directly. Only the appended edge node needs the interpolant. The feature is cheap overall (order 0.1–1 s), so this is about consistency with the existing pattern more than speed. metric_bsq_interpolant(:213–230) is a private, persistent B² that competes withequil.eqfun_Band is rebuilt on every call (:717). Whether metric-consistent B² is the right physics is for the physics review; either way its owner should be Equilibrium ormake_metric, so a future consumer (LocalStability shear, KineticForces) does not grow a third B².eigenmode_modes(:361–367) re-encodes −A⁻¹(Bξ′ + Cξ) by hand;compute_node_xi_s!(ForceFreeStates/EulerLagrange.jl:1397, used atResult.jl:174) already does exactly that.surface_geometry!andcurvature_kernel!depend only on(equil, psi)but sit inside the eigenmode loop (:755–770): all geometry is computed twice foreigenmodes = [1, 2]. Hoist per surface.
4. Robustness
surface_geometry!(…; bsq=nothing)leavesgeom.bsqzero, andcurvature_kernel!divides by it (:285). Makebsqrequired.similar(x, 0)(:921–922) collapses disabled 2-D/3-D outputs to rank-1 while the metadata table declares(psi, eigenmode)/(psi, theta, eigenmode)— the writer cannot attach the declared dims (Copilot). The zero-extent sentinel has precedent, but only if the rank is kept.- Single-n is load-bearing, not just declared:
N = ffs.numpert_total(:349) is written intoxi_s[i, :]of widthmpert(:367), aDimensionMismatchthe guard at:698is what prevents. - Constructors hard-code field counts (
for _ in 1:16,1:6,1:13,1:6,1:8at:131/248/400/451/539);fieldcount(T)stops them rotting when a field is added.
5. Cleanup
- Code comments are clean — no run exceeds two lines. The narration is in docstrings:
:208–211and:409–412carry session history (keep the one-sentence reason; drop the rest), andtest:176,:208record observed percentages that belong in the docs, not a comment. docs/src/energy_decomposition.md:64says "better than 1 %"; the tests record 1.4 %, 1.2 % and 3.3 % at the mpsi = 16 fixture (test:176,:208). "≲1–3 % at mpsi = 16, converging with resolution" matches the evidence.test/runtests_energy_decomposition.jl(292 lines) would be ~200 with the same coverage:_ed_metadata_violations(:216–231) duplicatestest/h5_metadata_check.jl— include it;:32–50tests thereg_spotkeyword, not this kernel;:70–73asserts kwdef defaults;:86–90re-derive K1/K2/K3 from:293–295(tautological);:99–128hand-plumbs the curl residual that:173already pins;decompose_energyruns eight times — hoist the shared results.:287–291writes into the shared fixture'sgpec.h5and only works because it is the last testset — write to a temp copy.- Five structs describe their fields in prose rather than a
## Fieldsblock (:104,:232,:376,:435,:516); the unnamed1e-10at:315; μ₀j^θ and μ₀j^ζ computed three times (:286,:475,:572); the 13 hand-named accumulators plus three mirrored copy blocks (:737–749,:796–837) could be driven from one name tuple, as_allocate_result!already half-does.
6. Before merge
- The branch is ~250 commits behind and develop has since rewritten
FieldReconstruction.jl(225 lines) and the driver (+455); your 6-linereg_spotchange and 47-line hook land on rewritten code.DIRTYhere is semantic, not textual (zero conflict markers) — worth re-reading both hunks after the merge-forward. - The regression report was a laptop run (arm64, 4 threads) against a develop that has moved; please re-run on the shared machine after merging forward and refresh the stamp.
- This review deliberately did not cover the physics (metric-B² vs
eqfun_B, the pointwise covariant lowering your docstring at:408–412justifies, the ξ_s recomputation at knots). Those deserve a physics pass and I'd ask for one before approval.
Credit where due: Copilot's two inline comments — the gating gap and the similar(x, 0) rank collapse — were both real and are folded in above.
🤖 Generated with Claude Code
logan-nc
left a comment
There was a problem hiding this comment.
Physics pass, to close the gap my earlier review left open. Checked term by term against the Fortran counterpart and the theory note on origin/recon of the Fortran repository (gpout_recon/gpout_recon2 in gpout.f, gpeq_c/gpeq_K/gpeq_shear/gpeq_curvature/gpeq_cveri/gpeq_firstform in gpeq.f, docs/tex/recon/main.tex) — not on Fortran develop, which is worth stating in the docs since "fidelity to Fortran" means fidelity to that branch. Every claim marked verified was read or derived; the Fortran line references were checked by me as well.
Verdict: correct with caveats. No missing terms, no placeholders; every kernel either matches origin/recon line for line or is a defensible, more accurate deviation. One thing should be settled before merge — see §6 — because it is the untested explanation for the 1.4–3.3 % mismatch.
1. Both δW forms — verified
- Standard form (
:565–585): derived from −½∫ξ*·F(ξ) with the full ξ (the ξ∥ pieces of the J×Q and ∇·ξ terms cancel, so using the full Jξ^θ, Jξ^ζ is right).det_termat:572–575is J·det[j, b, ξ*] with j^ψ = 0; ∇·ξ at:577is (1/J)[∂ψ(Jξ^ψ) + ∂θ(Jξ^θ) − 2πin Jξ^ζ] in the exp(2πi(mθ−nζ)) convention. Term for term identical togpeq_firstform. One divergence: ∂θ(Jξ^θ) is spectral here (:415–418) and a periodic spline there — fine, but annotate it as a Fortran deviation. - Effective field b_eff = b + ξ_n μ₀ j×n̂ (
:455–480): all six component formulas verified by hand and identical togpeq_c(cwt/cwz/cvp/cvt/cvz). μ₀j^θ = −F′/J and μ₀j^ζ = −(μ₀p)′/χ′ − qF′/J follow from ∇×B and (j×B)·∇ψ = p′, givenF_deriv= d(2πF)/dψ_N (inferred from the Fortransq%f1(1)usage; please confirm). - K = K1 + K2 + K3 (
:293–295): identical togpeq_Kandmain.tex; units consistent (K|ξ_n|² is Pa); K3 = 2p′κ_ψ checks against the interchange term. Caveat: K1, K2 and the completing-the-square identity were verified againstmain.texandgpeq_Konly — Bernstein 1983, Chance 1992 and Boozer 2004 are not indocs/resources/, so the equation numbers this PR adds toreferences.md/citations.mdare unverified. Please either add the PDFs or cite the theory note. - ½, Re, conjugation, normalization: Re[(Jc^i)* c_i]/(μ₀J) is Hermitian-correct; the ½ matches Fortran
dw_raw = 0.5*total; the 2μ₀/ψ₀² scale (:602) with unit-power-normalizedwt[:,k]as edge BC (:342) is the same convention as Fortran'sdcon_facafteridcon.fundoes its rescaling;ep = diag(wt†·wp·wt)withwp = (u2/u1)/ψ₀²matchesfree.f. No vacuum or surface term belongs in the comparison —epis the plasma boundary term Ξ†(FΞ′+KΞ), i.e. the volume δW_p;evis separate. Verified in concept; the DCON ½/μ₀ scale was not re-derived from Glasser 2016, but the 1e-3 agreement rules out a missing O(1) factor.
2. Metric-B² vs eqfun_B — your choice is right; say so at the call site
Fortran gpeq_K and gpeq_curvature take B² from eqfun (bsq_val = eqfun%f(1)**2, with fx/fy for its derivatives). make_metric builds only g_ij/J, J, J_ψ — no eqfun — so the Euler–Lagrange operator and the b reconstruction carry the metric B², and the C∥ = K2|ξ_n|² cancellation holds only with that B². Using it is operator-consistent and an improvement over Fortran. The ~1 % is the mismatch between the equilibrium's direct |B| and the inverse-map metric at mpsi = 16 — coordinate-transformation accuracy converging with equilibrium resolution, not hidden physics. It needs the project's one-line deviation note at :285, :291, :294, not only in the docstring.
3. Pointwise covariant lowering — the claim holds, with two corrections
gpeq_c back-transforms band-limited modes before squaring, so ∮|C|² and ∮K2|ξ_n|² each lose out-of-band energy that does not cancel between them; pointwise products on the mtheta grid are Parseval-exact (aliasing aside). So the Julia effective-field numbers will differ from Fortran recon reference values at the few-% level, and Julia is the more accurate. Two corrections to the docstring at :408–412: "resolution-independent" should read "ψ-resolution-independent, mpert-dependent" (the deficit shrinks with the band); and this is a hybrid — Jξ^ψ is formed pointwise (:416, :426) but Jξ^θ, Jξ^ζ still come from the truncated jmat convolution in compute_contra_displacements — the docstring implies all-pointwise.
4. ξ_s at the knots — verified, no physical content in knot-vs-node
−A⁻¹(Bξ′ + Cξ) (:361–367) matches Fortran gpeq_sol and Julia's own compute_node_xi_s! (EulerLagrange.jl:1397, same raw A; Cholesky vs LDLᵀ immaterial). It is the algebraic Euler–Lagrange relation for Ξ_s, so evaluating at knots rather than ODE nodes changes only the interpolation accuracy of (ξ, ξ′), nothing physical. That argues for calling the existing helper rather than re-encoding it (as my earlier review noted).
5. The curl residual — right check, state its scope
(:495–515) is identical to gpeq_cveri and is the Ξ_s equation itself (∇ψ·∇×C = 0 ⇔ δj·∇ψ = 0 closure), so at the knots it holds by construction. It validates the ξ_s algebra, the g22/g23/g33 lowering and the b_θ, b_ζ assembly against A, B, C — not the ψ-ODE and not b_ψ or g11/g12/g13/|∇ψ|². A roundoff-level value says nothing about the eigenmode's correctness. Worth one sentence in the docs so it isn't read as a global check.
6. Convergence — the one thing to settle before merge
Expected: ~h⁴ for the cubic-spline ψ-quadrature on a smooth integrand, spectral in θ. Observed from the PR's own numbers (mpsi 16 → 128): effective form ~h^0.9, standard form ~h^1.8. That is not quadrature-order behaviour.
The leading explanation is a mis-documented Fortran deviation. gpout.f says verbatim, just above the energy check: "Perform the energy check on the DCON solution grid. Do not use rzphi%xs for this comparison; it is a coarse geometry grid." — and integrates on psifac. decomposition_grid (:309–316) integrates on the rzphi_xs equilibrium knots, and the docstring at :305 says gpout_recon "loops over the same knots" — true of its diagnostics loop, not of its energy integral. With the 1/(m − nq) structure in ξ_s and Jξ^θ, the coarse-knot integral is the natural suspect for both the size of the mismatch and the sub-quadrature rate; the slower effective-form rate is consistent with K depending on spline derivatives (bsq_psi, dratio) that lose an order. A confound: ep itself moves with mpsi.
Discriminating test, cheap to run: fix mpsi, integrate on ODE nodes ∪ knots instead of knots alone, and report how the relative error changes. If it collapses, fix the grid and the docstring; a separate mpert scan would then bound the band-truncation floor at mpsi = 128.
7. Fortran correspondence and the annotations owed
gpout_recon/gpeq_c/gpeq_K/gpeq_shear/gpeq_curvature/gpeq_cveri/gpeq_epf/gpeq_dst/gpeq_firstform ↔ decompose_energy/effective_field!/curvature_kernel!/curl_residual!/surface_densities!. Term-level divergences, each needing the project's one-line deviation comment at the call site: (a) metric B² vs eqfun B²; (b) pointwise vs band-truncated C_i, ξ_n; (c) spectral vs spline ∂θ; (d) radial grid knots vs psifac; (e) reg_spot = 0 vs Fortran's regularized xmp1/xmt path in gpeq_firstform (inferred from variable names, not traced). Also: the long_name on current_coupling and pressure_compression (:940–941) says "imaginary part is roundoff" — true of the volume totals, not of the per-surface profiles; reword.
Not checked: the Bernstein/Chance/Boozer equation numbers; the DCON ½/μ₀ convention from Glasser 2016 directly; the edge amplitude in sum_eigenmode_contributions; gpeq_firstform's regularization path; gpout_recon2's radial grid; aliasing error of the mtheta Riemann sum.
🤖 Generated with Claude Code
…warning on multi-n, kinetic and non-forward runs run_energy_decomposition now warns and returns nothing for a non-forward integrator, nn_low != nn_high or kinetic matrices instead of letting decompose_energy throw and abort main. Disabled forms are written as their zero-extent arrays directly, so the sentinels keep the rank their dims attribute declares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c B² and node ξ_s with Equilibrium and ForceFreeStates in the energy decomposition Add Equilibrium.flux_surface_basis (interpolant and knot methods, sharing the ∇ψ kernel of flux_surface_metric) and move metric_bsq_interpolant next to it. surface_geometry! takes bsq as a required keyword and reads the stored knot values on equilibrium knots. eigenmode_modes calls ForceFreeStates.compute_node_xi_s!, which now also accepts a single displacement vector. Geometry, the curvature kernel and μ₀j are computed once per surface before the eigenmode loop, the per-surface integrals and density copies are driven by the profile name tuples, the scratch constructors use fieldcount, and the structs document their fields. Decomposition totals are unchanged to 1e-15 on the Solovev fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iations from the Fortran recon branch Mark at each call site where the decomposition departs from the Fortran recon branch (metric B², pointwise products, spectral θ-derivative, knot-grid integration, reg_spot = 0). Correct the decomposition_grid and surface_fields! docstrings and the current_coupling and pressure_compression long_names, and state in the docs what the curl residual validates and the measured convergence, including the open edge offset of the effective-field form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and drop redundant tests Use the shared h5_metadata_check walker, compute the decompositions once, drop tests that re-derived definitions or duplicated the pinned curl residual, and add checks for the skip warnings, the zero-extent ranks, knot versus interpolant geometry and the required bsq keyword. The scripting-entry test now writes to a scratch copy of gpec.h5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bedEquilibrium/EnergyDecomposition with an eigenmode_index coordinate The decomposition of free-boundary DCON eigenmodes now writes to PerturbedEquilibrium/EnergyDecomposition/ instead of ForceFreeStates/EnergyDecomposition/, with no legacy path. eigenmode_index is marked as the dimension scale of the eigenmode axis and attached to every total, profile and density, so it reads as a coordinate rather than an echoed control value. The writer, tests, docs, schema table and the solovev_n1 and diiid_n1 harness cases move together, and the docs state that the stage covers free-boundary eigenmodes only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…omposition # Conflicts: # docs/development/references.md # docs/make.jl # regression-harness/cases/diiid_n1.toml # src/GeneralizedPerturbedEquilibrium.jl
…r own display order
…uracy and attribute the form offset to the equilibrium tolerance Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s instead of splining the saved ODE steps Each decomposition knot now gets its Euler-Lagrange state by integrating from the nearest saved step that no singular surface separates from it (ForceFreeStates.solution_state_at), so ξ and ξ' no longer depend on how densely the forward sweep saved. On the Solovev example the standard-form mismatch is -4.0e-4 at every eulerlagrange_tolerance from 1e-7 to 1e-11, where the splined value moved from -1.45e-3 to -4.0e-4. Pre-existing harness rows are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Release note
A new
[EnergyDecomposition]section decomposes the plasma potential energy δW_p of selected free-boundary eigenmodes on the (ψ,θ) grid in two forms, the effective-field form ½∫[|b_eff|²/μ₀ − K|ξ_n|²] with its shear-current, parallel-current and pressure-curvature terms, and the standard form ½∫[|b|²/μ₀ − j·(b×ξ*) + (∇·ξ)*(ξ·∇p)]; it verifies (∇×b_eff)·∇ψ = 0, compares each total witheigenmode_plasma_energies, and writes everything toForceFreeStates/EnergyDecomposition/ingpec.h5.Regression report
Notes for reviewers
src/PerturbedEquilibrium/EnergyDecomposition.jlfollows the Fortran GPECgpeq_c,gpeq_shear,gpeq_curvature,gpeq_K,gpeq_epf,gpeq_dst,gpeq_firstformroutine by routine (the Fortranrecon_flag/recon_flag2diagnostics;docs/tex/recon/main.texin the Fortran repository has the derivations).recon_flag3was deliberately not ported.gpeq_contra/gpeq_cova/gpeq_normalarrays; with the truncated arrays the |V|² and K₂|ξ_n|² pieces do not cancel and the effective-field total sits 21 % below ep[1] independent of the ψ grid.With these, each form reproduces ep[k] to better than 1 % on the Solovev decks (1e-5 to 1e-2 depending on the radial resolution). A Fortran run of the same case (
reg_flag=f, matchedpsihigh) reproduces its own ep to 1.4e-4 in both forms; the effective form needs metric derivatives (shear, curvature) that the standard form does not, so its residual tracks the coordinate accuracy of the Julia equilibrium.reg_spotkeyword refactor ofcompute_contra_displacementsis numerically neutral (every pre-existing harness row unchanged).🤖 Generated with Claude Code
Notes
This is has the same feature with PrincetonUniversity/GPEC#213 this PR. But would be much more clear and neat version.