Uh oh!
There was an error while loading. Please reload this page.
Jacobian-invariant power-norm flux eigenvalues - #225
Conversation
…genvalues Adds a power-normalized flux Φ-space transformation of the edge-scan energy matrix W so that the eigenvalues of W_Φ = M†·W·M report the Jacobian-invariant energy spectrum alongside the existing ξ-space outputs. Only the eigenspectrum is invariant — ξ, Φ itself, and θ-space reconstructions are all coordinate-dependent (see scripts/test_power_norm_invariance.jl for numerical proof). - New src/ForceFreeStates/PowerNorm.jl: compute_sqrt_jac_delpsi, compute_surface_area_local, compute_sqrtamat, compute_power_norm_eigenvalues, and the free_compute_sqrtamat_spline pre-computation for the edge scan. - Free.jl: compute pn eigenvalues at psilim and at each edge-scan step. - EulerLagrange.jl: pre-compute the sqrtamat spline before the edge-scan loop. - ForceFreeStatesStructs.jl: add pn_et/ep/ev to ForceFreeStatesData and pn_total/plasma/vacuum + sqrtamat_spline fields to EdgeScanState. - GeneralizedPerturbedEquilibrium.jl: write edge_scan/pn_* and vacuum/pn_* to the HDF5 output. - scripts/: diagnostic scripts for the edge-scan eigenvalue comparison, the Jacobian comparison, and the 4-test invariance suite (area, ∮|f|²dA, convmat angle-map, sqrtamat √weight identity) that establishes the precision floor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…jacobian comparison script Align scripts/compare_jacobians_power_norm.jl with the proven-invariant regime from scripts/test_power_norm_invariance.jl (psihigh=0.994, mtheta=1024, delta_m=32) and parameterise the toroidal mode number as const NN at the top so output PNGs carry the n label (jacobian_comparison_n$(NN).png and gpec_edge_eigenvalues_n$(NN).png). Adds an mpert/mlow/mhigh diagnostic that verifies the auto-computed m-range matches across all four jacobians, and inlines the edge-scan eigenvalue plot so no h5 needs to survive the tmpdir. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…oups
Power-normalized flux (Φ-space) eigenvalues are now the default everywhere they
are stored. ξ-space values are retained under XiNorm/ subgroups for benchmarking
against the Fortran GPEC lineage.
Schema changes:
- edge_scan/ → EdgeScan/; power-norm values (total_energy, plasma_energy,
vacuum_energy, vacuum_eigenvalue) live at top level, ξ-space versions move
to EdgeScan/XiNorm/.
- vacuum/ split into FreeBoundaryStability/ (energy matrices and eigenmode
energies) and SurfaceGeometries/Plasma|Wall/{x,y,z} (point clouds).
- wt → W_freeboundary_eigenmodes (ξ-space eigenvector matrix); wt0 →
W_freeboundary (pre-diag total energy matrix). New exports: W_plasma (wp,
saved for the first time) and W_vacuum (wv).
- FreeBoundaryStability/eigenmode_{,plasma_,vacuum_}energies are the Φ-space
arrays; XiNorm/ holds the ξ-space counterparts.
- New EdgeScan/vacuum_eigenvalue computed in Φ-space; ξ-space version now
lives only under XiNorm/.
Consumers updated to match: Analysis/ForceFreeStates plots, the benchmarks
(equil_psihigh_scan, benchmark_git_branches), the fullruns smoke test, the
regression-harness case TOMLs, and all scripts under scripts/. Kinetic Solovev
test thresholds rebaselined to the Φ-space values captured from a clean run
(≈1527.65 for the scalar case, ≈-1.7253 for multi-n).
Follow-on PR will rename the remaining lowercase groups (equil, info, input,
integration → EulerLagrangeSolutions, kinetic, locstab, matrices, singular →
SingularSurfaces, splines) for a consistent CamelCase schema and codify the
naming convention in CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>Promote FreeBoundaryStability/W_freeboundary, W_plasma, W_vacuum to their power-normalized flux (Φ-space) versions so the matrix set matches the eigenvalues stored alongside them. The ξ-space matrices now live under FreeBoundaryStability/XiNorm/ next to the ξ-space eigenvalues, for Fortran benchmarking. Implementation: - VacuumData gets pn_wt0, pn_wp, pn_wv (NaN-initialised, populated only when vac_flag is true). - compute_power_norm_eigenvalues now returns the transformed wt_pn/wp_pn/wv_pn matrices when all_eigenvalues=true (including the NaN path for rational surface crossings). - free_run! copies the returned matrices into vac_data at psilim. - The writer swaps top-level W_* to pn_* and adds the XiNorm/W_* mirrors. W_freeboundary_eigenmodes remains at top level as the ξ-space eigenvector matrix from the diagonalisation of wt0 — it has no Φ-space analogue because Julia's eigen normalizes vectors under a different inner product than the physical Φ-space norm. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FreeBoundaryStability/W_freeboundary_eigenmodes now holds the Φ-space eigenvector matrix of W_freeboundary, consistent with the Φ-space eigenvalues stored alongside it. The ξ-space eigenvector matrix moves to FreeBoundaryStability/XiNorm/W_freeboundary_eigenmodes. Both spaces apply the same phase convention used in free_run!: rotate each column so its largest-magnitude entry is real-positive. Columns are sorted most-unstable first, matching eigenmode_energies. compute_power_norm_eigenvalues returns the sorted+phased eigenvector matrix (reusing eigvecs already computed during eigendecomposition) and free_run! stores it in vac_data.pn_wt. Verified: W·v = λ·v to 2.6e-13, columns are unit L² in Φ-space, and the phase condition is met exactly in both spaces. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
logan-nc
commented
Apr 14, 2026
logan-nc
commented
Apr 14, 2026
HDF5 schema updates in the latest commitsThe Key changes
Full |
Development scripts used during this PR (not being merged)During development I wrote a few exploratory scripts to prove out Jacobian invariance of the power-norm eigenspectrum and to pin down numerical settings that actually exercise invariance. Documenting them here for posterity before removing them — they were scaffolding, not product.
|
| quantity | hamada | pest | rel. diff across jac |
|---|---|---|---|
ξ-space et[1] | +1.5047e+00 | ≈ +1.24e+00 | 8–154% across the edge scan |
power-norm pn_et[1] | +1.5787e+00 | ≈ +1.579e+00 | 0.1–0.5% across the edge scan |
The ξ-space spread was O(10%) at best and grew to >100% near rational surfaces, while the power-norm spread sat at ~10⁻³ across the smooth interior of the edge scan. That disparity is what motivated the rest of the PR — all the downstream schema work (CamelCase restructure, XiNorm/ subgroup, W_freeboundary* in Φ-space by default) exists because this script showed Φ-space is the invariant you want to publish.
scripts/test_power_norm_invariance.jl — unit-level invariance floor
Isolated-operator test of the √weight transform and convmat angle-map across Jacobians, short-circuiting the full Euler–Lagrange pipeline. Established the numerical precision floor at:
PSIHIGH=0.994,N_TOR=3,MLOW=-128,MHIGH=128,MTHETA=1024- Floor: ~6.9e-7 relative area residual
- Both the √weight operator and the convmat angle-map agreed across Jacobians to within ~2× the floor
This set the bar for what "invariant" means numerically in this codebase and told us which knobs (mtheta, psihigh, absolute m-band) were actually binding on the full-stack comparison.
scripts/compare_jacobians_power_norm.jl — full-stack 4-way comparison
Runs the full pipeline on the DIIID-like example four times (hamada / pest / boozer / park), all with apples-to-apples overrides:
psihigh = 0.994 # inside the proven-invariant regime
mtheta = 1024 # matches unit-test θ-grid
cyl_flag = true # absolute mlow/mhigh across jacobians
delta_mlow = -48
delta_mhigh = 48 # mpert = 97 on all four
Before these overrides, the four runs silently disagreed because cyl_flag=false makes mlow/mhigh depend on each Jacobian's qmin/qmax, and mtheta=256 left aliasing as a free parameter. With the overrides, the Φ-space eigenvalue spread across Jacobians collapsed toward the unit-test floor.
scripts/compare_eigenvalues_edge_scan.jl — ξ-space vs Φ-space visualization
Plots the edge-scan eigenvalues from a gpec.h5 in both representations side-by-side. The punchline:
- ξ-space (
FreeBoundaryStability/XiNorm/*): curves disagree across Jacobians by O(1). - Φ-space (
FreeBoundaryStability/*, the new default): curves overlap across Jacobians to the floor.
This is the visual proof of the thing the schema restructure is built around — only the Φ-space spectrum is invariant, so that's what ships as the default output.
Throwaway diagnostic
scripts/diag_pn.jl— ad-hoc prints while wiringpn_wt0/pn_wp/pn_wvintoVacuumData(sqrtamat Hermiticity, Toeplitz structure, M-matrix operator norm, singfac values).
Key results these scripts produced
- First-of-a-kind confirmation (
test_jac_invariance.jl): √A-normalized (power-norm) eigenvalues are Jacobian-invariant at ~0.1–0.5% across the edge scan, vs ξ-space eigenvalues at 8–154% — a 2–3 order-of-magnitude improvement. - Numerical floor on invariance at the operator level: ~6.9e-7.
- Park n=3 anomaly (seen early on) was confirmed to be θ-aliasing, not a correctness bug — n=1 with
mtheta=1024is well-behaved and agrees with the other Jacobians at the floor. - W_plasma + W_vacuum = W_freeboundary in Φ-space holds to 8.9e-16 (additivity preserved by the M†·W·M transform).
- W_freeboundary · v = λ · v verified to 2.6e-13 on Solovev n=1 for the stored Φ-space eigenmodes.
Being removed
These scripts live in scripts/ and were tracked in commits ffad3994 and ab14b09f. Removing them (and the untracked diagnostic diag_pn.jl) in the next commit — they've served their purpose.
These scripts (compare_jacobians_power_norm.jl, compare_eigenvalues_edge_scan.jl, test_power_norm_invariance.jl) were scaffolding used to establish the Jacobian invariance floor (~6.9e-7) and visualize ξ-space vs Φ-space eigenvalues during development of this PR. Their findings are documented in the PR discussion; they are not intended to be carried forward in the repo.
logan-nc
commented
Apr 14, 2026
@ebursch@matt-pharr this is ready to review - you have both thought about these things a lot. It's cool to see them proven and working to our benefit here. |
logan-nc
commented
Apr 28, 2026
@ebursch and @matt-pharr please mark formal reviews here! Something to think about as you look at it and think about your paper: Should we use sqrt(A) weighted fields (Phi-tilde) or sqrt(A) normalized displacements (xi-tilde? More consistent with DCON? Still invariant?). Note the T matrix above translates xi to flux and has a m-nq in it, which could do funny things near rationals. |
logan-nc
commented
Apr 29, 2026
@ebursch and @matt-pharr please submit reviews today - it's been 1 week since our big meeting where we agreed to close PRs. 90% of the line changes are just h5 variable name improvements. There is really just the one new PowerNorm.jl file and corresponding renormalization of the energy matrices in free.jl to double check. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
ebursch
left a comment
There was a problem hiding this comment.
This all looks good to me. Made a few minor suggestions to improve codebase consistency. I will leave approval to @matt-pharr since I am less familiar with the broader code generally.
…uilibrium Extract the duplicated |∇ψ| metric block and flux-surface-area integral into the Equilibrium module (new FluxSurfaceMetrics.jl) as flux_surface_metric and flux_surface_area. Both FFS (PowerNorm.jl) and PE (SingularCoupling.jl) now call these shared helpers, removing the copy-pasted metric code and the "compute_surface_area_local" workaround that existed only to dodge a circular dependency (FFS cannot import PE; Equilibrium is the correct shared home). Addresses ebursch review comments on PR #225. Numerically a no-op except for benign 1-ULP shifts (<=2.2e-16) in 5 PE singular-coupling quantities caused by LLVM FMA contraction differences across the new function boundary; all tracked regression quantities remain well within their 1e-8 noise thresholds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
logan-nc
commented
Jun 10, 2026
@matt-pharr I toggled on auto-merge based on @ebursch's positive review... If the checks fail, that gives you a chance to speak now or forever hold your peace. Otherwise, I'll fix and merge tomorrow |
…ure/power-norm-eigenvalues
…m HDF5 names The power-norm HDF5 restructure removed the vacuum/ group, so all three GPEC regression cases lost their vacuum_eigenvalue track (vacuum/vacuum_eigenvalue no longer exists -> reported missing/N/A). Repoint it to the faithful ξ-space continuation at FreeBoundaryStability/XiNorm/vacuum_eigenvalue. The eigenmode_energies|plasma_energies|vacuum_energies paths already read the power-normalized (Φ-space, Jacobian-invariant) datasets at the final truncation, so relabel the et/ep/ev tracks "Power Normalized ..." to stop calling Φ-space values "total/plasma/vacuum energy". No new tracks added (power-norm only). Verified develop vs local on all three cases: every renamed quantity resolves to a value on local; develop reports N/A for the new paths (it predates the rename), and all unchanged quantities match bit-for-bit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…genvalues The power-norm HDF5 restructure (ecbb5b9) repointed the kinetic fullrun tests at FreeBoundaryStability/eigenmode_energies (now power-normalized, Φ-space) while keeping ξ-space expected values. Power-norm collapses these stable Solovev modes to ~0 (et ~ 1e-8…1e-10), so the assertions fail and would be useless regression anchors. Read FreeBoundaryStability/XiNorm/eigenmode_energies instead — the faithful continuation of develop's vacuum/et. The ex4 anchors (et[1]≈-0.19, et[2]≈17.74, et[3]≈17.49) match XiNorm exactly. Restore ex3's expected et[1]≈16.480 (develop's value; the 1527.65 set in ecbb5b9 matched neither path and was erroneous). Verified locally: 12/12 pass under --check-bounds=yes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ure/power-norm-eigenvalues
…o XiNorm eigenmode energies
The power-norm HDF5 rename left two ξ-norm energy references pointing at the
deleted vacuum/et group: the Solovev kinetic-calculated fullrun assertion
(test/runtests_fullruns.jl:65) and the solovev_kinetic_calculated regression
case. Both jobs failed CI with KeyError: key "vacuum/et" not found.
Point them at FreeBoundaryStability/XiNorm/eigenmode_{energies,plasma_energies,
vacuum_energies} — the same ξ-norm quantities the baselines were recorded
against (not the bare FreeBoundaryStability/* power-norm datasets), matching the
XiNorm convention already used by the DIIID kinetic fullrun at line 40.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Uh oh!
There was an error while loading. Please reload this page.
Brings develop's post-perf/riccati work (power-norm eigenvalues #225, kinetic-history restore #270/#112, KineticForces/NTV module, GridUtilities, vacuum/* → FreeBoundaryStability/* + SurfaceGeometries/* HDF5 reorg, faithful Fortran θ=2π endpoint drop before the metric FFT) onto the tearing branch. Conflict resolutions (5): - src/Utilities/Utilities.jl: include-union — keep tearing's PhysicalConstants/KineticProfiles/NeoclassicalResistivity AND develop's GridUtilities (the latter injects powspace directly, no submodule). - test/runtests.jl: test-union — tearing's SLAYER/dispersion/kinetic_profiles/ resist suites AND develop's runtests_kinetic.jl. - test/runtests_fullruns.jl: took develop's sign-AGNOSTIC kinetic multi-n pin (abs(real(et[1])) < 0.5). Supersedes the tearing bracket: develop's endpoint drop shifts the near-marginal et[1] across zero (+0.190 macOS), so real(et[1])<0 no longer holds. Well-conditioned et[2]/et[3] tight pins retained. - src/GeneralizedPerturbedEquilibrium.jl (main): kept BOTH independent post-PE stages — develop's KineticForces (NTV) block and tearing's SLAYER stage (_run_slayer_stage closure, runs under force_termination too). - src/GeneralizedPerturbedEquilibrium.jl (HDF5 writer): adopted develop's new output schema (FreeBoundaryStability/*, FreeBoundaryStability/XiNorm/*, SurfaceGeometries/*, singular/kinetic/*) and dropped the superseded vacuum/* keys; kept tearing's singular/delta_prime_raw output. Verified: package instantiates + precompiles; Tearing, KineticForces, GridUtilities, NeoclassicalResistivity all present. No code reads the dropped vacuum/* keys (one stale docstring ref in ForceFreeStatesStructs remains, inherited from develop). Full suite validation pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… operator
`ptof` ("power-norm to flux") is stale after the power-normalized → root-area-weighted
terminology change. Verified from Pharr (2026)'s definitions that the operator
sqrtamat·√jarea maps the root-area-weighted field b̃ (tesla) to the poloidal flux Φ
(weber): Φ = √A·sqrtamat·b̃, and the inductances act on it as energy = Φ†L⁻¹Φ. So the
"field → flux" description is correct; rename the mnemonic accordingly:
- ptof → rootareafield_to_flux (incl. _block/_full/_inv locals)
- Equilibrium.control_surface_ptof → control_surface_rootareafield_to_flux (+ export)
- build_control_surface_ptof → build_control_surface_rootareafield_to_flux
- Introduce `R ≡ rootareafield_to_flux` shorthand in two dense math docstrings to keep
them under the 180-col margin.
- Sweep two leftover "power-norm" comments in the test files (reverted-commit fallout).
Operating the response/coupling layer directly in the three tesla fields (b, b̄, b̃) was
considered but deferred: it rescales the FFS energy eigenvalues by 1/A² and the stored
inductances by A² (moves tracked regression values, reopens the PR #225 energy
normalization) — a normalization change for its own issue, not a naming cleanup.
Pure rename — no behavior change. Package precompiles; runtests_coordinate_invariant
9/9; regression diiid_n1 develop vs local unchanged (28/28 OK; the 2 N/A are the earlier
resonant_field key rename, not this commit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Summary
Wis transformed viaW_Φ = M† W MwhereM = T⁻¹ · ptof, withT = diag(i·2π·χ₁·(m−n·q))andptof = sqrtamat · √jarea. The eigenvalues ofW_Φare the coordinate-independent energy — they do not depend on Jacobian choice.edge_scan/pn_total_energy,pn_plasma_energy,pn_vacuum_energyalongside the existinget/ep/evξ-space channels.scripts/test_power_norm_invariance.jlestablishes a numerical floor of ~6.9e-7 for Jacobian invariance of the underlying operators (√weight identity and convmat angle-map) atpsihigh=0.994,mtheta=1024.scripts/compare_jacobians_power_norm.jlruns the full stack for{hamada, pest, park, boozer}and reports residual spreads at matched ψ against the unit-test floor.What is invariant, what isn't
Only the eigenspectrum of
W_Φis Jacobian-invariant. The Φ-space eigenvectors and the θ-space field reconstructed from Φ still carry Jacobian dependence (T depends on m-labelling; ‖Φ‖ = ‖M⁻¹ξ‖ drifts). The module docstring inPowerNorm.jlspells this out explicitly.Observed behaviour
mtheta=1024): all four jacobians agree to ~15 % at edge probe surfaces.J ∝ Bweighting, which concentrates grid density on the high-field side. Likely fixable with largermtheta; not pursued in this PR.Test plan
regress --cases diiid_n1 --refs develop,feature/power-norm-eigenvalues— confirm no baseline drift in existing scalars.julia --project=. scripts/test_power_norm_invariance.jl— confirm operator floor ~7e-7.julia --project=. scripts/compare_jacobians_power_norm.jl(withconst NN = 1) — confirm n=1 full-stack residual spread ~15 %.examples/DIIID-like_ideal_example/jacobian_comparison_n1.pngandgpec_edge_eigenvalues_n1.png.🤖 Generated with Claude Code