From f07c0799ff222f6a79a10d41c04b6a0e21c8ff2b Mon Sep 17 00:00:00 2001 From: logan-nc Date: Wed, 12 Aug 2026 17:59:10 -0400 Subject: [PATCH 1/3] ALL - REFACTOR - Module-mirroring CamelCase HDF5 schema (issue #226) Physics-first reorganization of gpec.h5 into 10 CamelCase top-level groups (Info, Input, Equilibrium, ForceFreeStates, LocalStability, SingularSurfaces, PerturbedEquilibrium, KineticForces, Tearing, SurfaceGeometries): - Groups CamelCase at all levels, datasets snake_case, data-driven tokens verbatim. - splines/ -> Equilibrium/{Profiles,Geometry}; matrices/ -> ForceFreeStates/ EulerLagrangeMatrices/{Ideal,Kinetic}; integration/ -> ForceFreeStates/ Solutions/ForwardIntegration; FreeBoundaryStability/ and EdgeScan/ move under ForceFreeStates/; slayer/ -> Tearing/ with CamelCase subgroups. - Galerkin outputs split three ways: solution functions to Solutions/ GalerkinIntegration/Solution, RPEC matching data to .../Match (kept pending a Galerkin-owner decision on the debug-only drop), per-surface Dp/PEST-3 results consolidated into SingularSurfaces/GalerkinDeltaPrime next to the ideal delta_prime_matrix and GGJ coefficients. - KineticForces: records/ -> EnergyIntegrals/, matrices_/ -> /KineticMatrices/. - Input echoes flushed (kinetic/, slayer/settings/): inputs live only under Input/, everything else is derived output. Writer<->Rerun.jl paths locked by shared H5_* consts. - Fixed 8 benchmark/example readers of the long-removed vacuum/ group. - Regression harness: case TOMLs repointed (quantity names unchanged); extractor gains a LEGACY_PREFIX_MAP fallback so cross-commit comparisons and --ref-range scans work across the rename boundary; retired only kinetic_factor (x2 solovev_kinetic cases, group flushed). - New test/runtests_h5_schema.jl enforces CamelCase group naming on a full-run output; docs get docs/development/hdf5-conventions.md + refreshed schema tables. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz --- CLAUDE.md | 4 + benchmarks/benchmark_against_fortran_run.jl | 53 ++-- .../benchmark_diiid_kinetic_stability.jl | 6 +- benchmarks/benchmark_git_branches.jl | 4 +- .../benchmark_solovev_kinetic_stability.jl | 2 +- benchmarks/compare_gal_vs_el.jl | 14 +- benchmarks/compare_jbgradpsi_m2.jl | 18 +- benchmarks/equil_psihigh_scan.jl | 2 +- benchmarks/gal_validation/gal_beta_scan.jl | 22 +- benchmarks/gal_validation/gal_epsilon_scan.jl | 22 +- .../pfac_study/gal_pfac_scan2d.jl | 8 +- benchmarks/plot_xi_eigenmode.jl | 16 +- benchmarks/scan_resistivity_m2.jl | 12 +- benchmarks/scan_rotation_m2.jl | 12 +- benchmarks/verify_gal_ideal.jl | 8 +- benchmarks/verify_gal_match.jl | 10 +- benchmarks/verify_gal_solution.jl | 8 +- docs/development/architecture.md | 2 +- docs/development/hdf5-conventions.md | 51 ++++ docs/src/ballooning.md | 4 +- docs/src/conventions.md | 10 +- docs/src/forcing_terms.md | 2 +- docs/src/galerkin.md | 2 +- docs/src/stability.md | 16 +- docs/src/workflow.md | 21 +- examples/DIIID-like_SLAYER_example/gpec.toml | 2 +- .../gpec.toml | 2 +- .../gpec.toml | 2 +- examples/DIIID-like_ideal_example/gpec.toml | 2 +- examples/LAR_beta_scan/gpec.toml | 2 +- examples/LAR_beta_scan/run_scan.jl | 13 +- examples/LAR_epsilon_scan/gpec.toml | 2 +- examples/LAR_epsilon_scan/run_scan.jl | 13 +- examples/LAR_ideal_match_test/gpec.toml | 2 +- examples/LAR_resistive_match_test/gpec.toml | 2 +- examples/Solovev_ideal_example/gpec.toml | 2 +- examples/Solovev_ideal_example_3D/gpec.toml | 2 +- .../Solovev_kinetic_NTV_example/gpec.toml | 2 +- regression-harness/cases/diiid_n1.toml | 96 +++---- regression-harness/cases/diiid_slayer_n1.toml | 32 +-- .../cases/efit_fixedbdy_separatrix.toml | 10 +- .../cases/gal_resistive_diiid.toml | 20 +- .../cases/gal_resistive_pe.toml | 16 +- .../cases/solovev_kinetic_calculated.toml | 36 +-- .../cases/solovev_kinetic_ntv.toml | 12 +- .../cases/solovev_kinetic_nuzero.toml | 36 +-- regression-harness/cases/solovev_multi_n.toml | 30 +- regression-harness/cases/solovev_n1.toml | 42 +-- regression-harness/src/extractor.jl | 77 ++++- regression-harness/src/runner.jl | 10 +- regression-harness/src/types.jl | 2 +- src/Analysis/Equilibrium.jl | 28 +- src/Analysis/ForceFreeStates.jl | 56 ++-- src/Analysis/PerturbedEquilibrium.jl | 42 +-- src/Analysis/PerturbedEquilibriumModes.jl | 22 +- src/Equilibrium/ReadEquilibrium.jl | 2 +- src/ForceFreeStates/Ballooning.jl | 4 +- src/ForceFreeStates/EulerLagrange.jl | 2 +- src/ForceFreeStates/Galerkin/GalerkinSolve.jl | 73 ++--- src/ForceFreeStates/ResistEval.jl | 2 +- src/ForcingTerms/CoilGeometry.jl | 2 +- src/ForcingTerms/ForcingTerms.jl | 2 +- src/GeneralizedPerturbedEquilibrium.jl | 269 +++++++++--------- src/KineticForces/KineticForcesStructs.jl | 2 +- src/KineticForces/Output.jl | 11 +- src/PerturbedEquilibrium/Utils.jl | 24 +- src/Rerun.jl | 25 +- src/Tearing/Runner/HDF5Output.jl | 90 ++---- src/Tearing/Runner/Runner.jl | 2 +- test/runtests.jl | 1 + test/runtests_coils.jl | 4 +- test/runtests_fullruns.jl | 8 +- test/runtests_h5_schema.jl | 66 +++++ test/runtests_parallel_integration.jl | 2 +- test/runtests_rerun_from_h5.jl | 22 +- test/runtests_slayer_runner.jl | 55 ++-- .../gpec.toml | 2 +- .../gpec.toml | 2 +- 78 files changed, 880 insertions(+), 736 deletions(-) create mode 100644 docs/development/hdf5-conventions.md create mode 100644 test/runtests_h5_schema.jl diff --git a/CLAUDE.md b/CLAUDE.md index ac6754303..0adf0ae4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -146,6 +146,10 @@ Additional file hygiene (enforced by pre-commit hooks): - Files must end with exactly one newline - LF line endings only (no CRLF) +### HDF5 Output Conventions + +The `gpec.h5` schema follows one physics-first convention (CamelCase groups at all levels, snake_case datasets, data-driven tokens verbatim, inputs only under `Input/`, five named top-level physics-topic exceptions). **Do not invent new group names or echo inputs into output groups** — read **[`docs/development/hdf5-conventions.md`](docs/development/hdf5-conventions.md)** before adding or moving any HDF5 output, and keep the regression-harness extractor's legacy map in sync on renames. + ### TOML Annotation Conventions Config-style TOML files (`examples/*/gpec.toml`, `examples/*/sol.toml`, `test/test_data/*` fixtures, `regression-harness/cases/*.toml`) follow one shared annotation style (header comment block, inline `# description` on every variable line sourced from the matching config struct's docstring, no Fortran references, no deprecated variables). **Do not invent a new convention** — read **[`docs/development/toml-conventions.md`](docs/development/toml-conventions.md)** in full before adding or editing one of these files. diff --git a/benchmarks/benchmark_against_fortran_run.jl b/benchmarks/benchmark_against_fortran_run.jl index b48cb39cc..74793ea59 100644 --- a/benchmarks/benchmark_against_fortran_run.jl +++ b/benchmarks/benchmark_against_fortran_run.jl @@ -312,17 +312,17 @@ function load_julia_outputs(h5_path::String) julia = Dict{String,Any}() h5open(h5_path, "r") do f - julia["psilim"] = read(f, "info/psilim") - julia["qlim"] = read(f, "info/qlim") - julia["et"] = read(f, "vacuum/et") - julia["psi_q"] = read(f, "splines/profiles/xs") - julia["q"] = read(f, "splines/profiles/q") - julia["di"] = haskey(f, "locstab/di") ? read(f, "locstab/di") : Float64[] - julia["dr"] = haskey(f, "locstab/dr") ? read(f, "locstab/dr") : Float64[] + julia["psilim"] = read(f, "Info/psilim") + julia["qlim"] = read(f, "Info/qlim") + julia["et"] = read(f, "ForceFreeStates/FreeBoundaryStability/eigenmode_energies") + julia["psi_q"] = read(f, "Equilibrium/Profiles/xs") + julia["q"] = read(f, "Equilibrium/Profiles/q") + julia["di"] = haskey(f, "LocalStability/di") ? read(f, "LocalStability/di") : Float64[] + julia["dr"] = haskey(f, "LocalStability/dr") ? read(f, "LocalStability/dr") : Float64[] - julia["psio"] = haskey(f, "equil/psio") ? read(f, "equil/psio") : NaN + julia["psio"] = haskey(f, "Equilibrium/psio") ? read(f, "Equilibrium/psio") : NaN - sc = "perturbed_equilibrium/singular_coupling" + sc = "PerturbedEquilibrium/SingularCoupling" julia["rational_psi"] = haskey(f, "$sc/rational_psi") ? read(f, "$sc/rational_psi") : Float64[] julia["rational_q"] = haskey(f, "$sc/rational_q") ? read(f, "$sc/rational_q") : Float64[] julia["rational_n"] = haskey(f, "$sc/rational_n") ? read(f, "$sc/rational_n") : Int[] @@ -333,30 +333,31 @@ function load_julia_outputs(h5_path::String) julia["chirikov_parameter"] = haskey(f, "$sc/chirikov_parameter") ? read(f, "$sc/chirikov_parameter") : Float64[] julia["delta_prime"] = haskey(f, "$sc/delta_prime") ? read(f, "$sc/delta_prime") : ComplexF64[] - pe = "perturbed_equilibrium" + pe = "PerturbedEquilibrium" # Fortran Phi_x/Phi_tot are the area-weighted field b̄ (tesla), matching forcing/response_b_area directly. julia["forcing_vec"] = haskey(f, "$pe/forcing_b_area") ? read(f, "$pe/forcing_b_area") : ComplexF64[] # Phi_x = b̄_x julia["response_vec"] = haskey(f, "$pe/response_b_area") ? read(f, "$pe/response_b_area") : ComplexF64[] # Phi_tot = b̄_tot - julia["b_n"] = haskey(f, "$pe/response/b_n") ? read(f, "$pe/response/b_n") : Matrix{ComplexF64}(undef, 0, 0) - julia["Jbgradpsi"] = haskey(f, "$pe/response/b_psi_area_weighted") ? read(f, "$pe/response/b_psi_area_weighted") : Matrix{ComplexF64}(undef, 0, 0) - julia["xi_psi"] = haskey(f, "$pe/response/xi_psi") ? read(f, "$pe/response/xi_psi") : Matrix{ComplexF64}(undef, 0, 0) - julia["xi_n"] = haskey(f, "$pe/response/xi_n") ? read(f, "$pe/response/xi_n") : Matrix{ComplexF64}(undef, 0, 0) - julia["clebsch_psi1"] = haskey(f, "$pe/response/clebsch_psi1") ? read(f, "$pe/response/clebsch_psi1") : Matrix{ComplexF64}(undef, 0, 0) - julia["clebsch_alpha"] = haskey(f, "$pe/response/clebsch_alpha") ? read(f, "$pe/response/clebsch_alpha") : Matrix{ComplexF64}(undef, 0, 0) - julia["psi_grid"] = haskey(f, "integration/psi") ? read(f, "integration/psi") : Float64[] + julia["b_n"] = haskey(f, "$pe/Response/b_n") ? read(f, "$pe/Response/b_n") : Matrix{ComplexF64}(undef, 0, 0) + julia["Jbgradpsi"] = haskey(f, "$pe/Response/b_psi_area_weighted") ? read(f, "$pe/Response/b_psi_area_weighted") : Matrix{ComplexF64}(undef, 0, 0) + julia["xi_psi"] = haskey(f, "$pe/Response/xi_psi") ? read(f, "$pe/Response/xi_psi") : Matrix{ComplexF64}(undef, 0, 0) + julia["xi_n"] = haskey(f, "$pe/Response/xi_n") ? read(f, "$pe/Response/xi_n") : Matrix{ComplexF64}(undef, 0, 0) + julia["clebsch_psi1"] = haskey(f, "$pe/Response/clebsch_psi1") ? read(f, "$pe/Response/clebsch_psi1") : Matrix{ComplexF64}(undef, 0, 0) + julia["clebsch_alpha"] = haskey(f, "$pe/Response/clebsch_alpha") ? read(f, "$pe/Response/clebsch_alpha") : Matrix{ComplexF64}(undef, 0, 0) + julia["psi_grid"] = haskey(f, "ForceFreeStates/Solutions/ForwardIntegration/psi") ? read(f, "ForceFreeStates/Solutions/ForwardIntegration/psi") : Float64[] # R,Z,φ: loaded via modes_to_theta helper below (not raw modes) julia["h5_path"] = h5_path # stash for modes_to_theta # mn_index[:, 1] = m values, mn_index[:, 2] = n values for each mode index - julia["m_modes"] = haskey(f, "info/mn_index") ? Int.(read(f, "info/mn_index")[:, 1]) : Int[] + julia["m_modes"] = haskey(f, "Info/mn_index") ? Int.(read(f, "Info/mn_index")[:, 1]) : Int[] # Control surface matrices - rm = "$pe/response_matrices" + rm = "$pe/ResponseMatrices" julia["permeability"] = haskey(f, "$rm/permeability") ? read(f, "$rm/permeability") : Matrix{ComplexF64}(undef, 0, 0) julia["plasma_inductance"] = haskey(f, "$rm/plasma_inductance") ? read(f, "$rm/plasma_inductance") : Matrix{ComplexF64}(undef, 0, 0) julia["surface_inductance"] = haskey(f, "$rm/surface_inductance") ? read(f, "$rm/surface_inductance") : Matrix{ComplexF64}(undef, 0, 0) julia["reluctance"] = haskey(f, "$rm/reluctance") ? read(f, "$rm/reluctance") : Matrix{ComplexF64}(undef, 0, 0) - julia["wt0"] = haskey(f, "vacuum/wt0") ? read(f, "vacuum/wt0") : Matrix{ComplexF64}(undef, 0, 0) + fbs = "ForceFreeStates/FreeBoundaryStability" + julia["wt0"] = haskey(f, "$fbs/W_freeboundary") ? read(f, "$fbs/W_freeboundary") : Matrix{ComplexF64}(undef, 0, 0) end return julia end @@ -1194,12 +1195,12 @@ function generate_plots(fort, julia, bench_dir, nn) h5_path = julia["h5_path"] rzphi_panels = [] for (comp_label, f_key, h5_var) in [ - ("ξ_R", "xi_r_fun", "perturbed_equilibrium/response/xi_R"), - ("ξ_Z", "xi_z_fun", "perturbed_equilibrium/response/xi_Z"), - ("ξ_φ", "xi_phi_fun", "perturbed_equilibrium/response/xi_phi"), - ("b_R", "b_r_fun", "perturbed_equilibrium/response/b_R"), - ("b_Z", "b_z_fun", "perturbed_equilibrium/response/b_Z"), - ("b_φ", "b_phi_fun", "perturbed_equilibrium/response/b_phi")] + ("ξ_R", "xi_r_fun", "PerturbedEquilibrium/Response/xi_R"), + ("ξ_Z", "xi_z_fun", "PerturbedEquilibrium/Response/xi_Z"), + ("ξ_φ", "xi_phi_fun", "PerturbedEquilibrium/Response/xi_phi"), + ("b_R", "b_r_fun", "PerturbedEquilibrium/Response/b_R"), + ("b_Z", "b_z_fun", "PerturbedEquilibrium/Response/b_Z"), + ("b_φ", "b_phi_fun", "PerturbedEquilibrium/Response/b_phi")] f_fun = get(fort, f_key, Matrix{ComplexF64}(undef, 0, 0)) # Reconstruct theta-space from mode-space via modes_to_theta (applies ν phase + helicity) diff --git a/benchmarks/benchmark_diiid_kinetic_stability.jl b/benchmarks/benchmark_diiid_kinetic_stability.jl index b5465f1ad..9ae37b6b3 100644 --- a/benchmarks/benchmark_diiid_kinetic_stability.jl +++ b/benchmarks/benchmark_diiid_kinetic_stability.jl @@ -8,7 +8,7 @@ Fortran GPEC's kinetic DCON reference. Runs `GPE.main()` with `kinetic_source="calculated"` and `kinetic_factor=1.0` against the EFIT g-file and `.kin` profile taken from a Fortran GPEC kinetic example directory, then compares the least-stable total-energy -eigenvalue `vacuum/et[1]` against `W_t_eigenvalue[:, 0]` in the Fortran +eigenvalue `ForceFreeStates/FreeBoundaryStability/eigenmode_energies[1]` against `W_t_eigenvalue[:, 0]` in the Fortran `dcon_output_n1.nc`. No inputs are duplicated into this repo — everything is read from the @@ -202,9 +202,9 @@ function run_benchmark(fortran_dir::String=default_fortran_dir()) isfile(h5path) || error("Expected Julia output not found: $h5path") et = h5open(h5path, "r") do h5 - read(h5["vacuum/et"]) + read(h5["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) end - isempty(et) && error("vacuum/et is empty in $h5path") + isempty(et) && error("ForceFreeStates/FreeBoundaryStability/eigenmode_energies is empty in $h5path") # et is stored as a length-2*N real array (re,im interleaved) by HDF5.jl # when the underlying Julia array is ComplexF64. NCDatasets and HDF5 give # us a ComplexF64 array directly here. diff --git a/benchmarks/benchmark_git_branches.jl b/benchmarks/benchmark_git_branches.jl index 7d49f3bc2..c8e017bc2 100755 --- a/benchmarks/benchmark_git_branches.jl +++ b/benchmarks/benchmark_git_branches.jl @@ -194,8 +194,8 @@ function run_example_benchmark(example_path, num_runs) end h5 = h5open(gpec_path, "r") - et = read(h5["FreeBoundaryStability/eigenmode_energies"]) - nsteps = read(h5["integration/nstep"]) + et = read(h5["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) + nsteps = read(h5["ForceFreeStates/Solutions/ForwardIntegration/nstep"]) close(h5) avg_runtime = sum(runtimes) / length(runtimes) diff --git a/benchmarks/benchmark_solovev_kinetic_stability.jl b/benchmarks/benchmark_solovev_kinetic_stability.jl index 4ab64e5ae..c942a20c6 100644 --- a/benchmarks/benchmark_solovev_kinetic_stability.jl +++ b/benchmarks/benchmark_solovev_kinetic_stability.jl @@ -155,7 +155,7 @@ function run_julia_reference() GPE.main([rundir]) wall = time() - t0 et = h5open(joinpath(rundir, "gpec.h5"), "r") do h5 - read(h5["FreeBoundaryStability/eigenmode_energies"]) + read(h5["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) end return real(et[1]), imag(et[1]), wall end diff --git a/benchmarks/compare_gal_vs_el.jl b/benchmarks/compare_gal_vs_el.jl index cfb1d5272..cff8b26d4 100644 --- a/benchmarks/compare_gal_vs_el.jl +++ b/benchmarks/compare_gal_vs_el.jl @@ -1,7 +1,7 @@ # Overlay the IDEAL gal matched ξ(ψ) against the EL total-energy eigenmode ξ(ψ), for the same edge # eigenvector w. The ideal gal solution should reproduce the EL (DCON) ideal solution. # -# EL : ξ_EL(ψ) = U_EL(ψ) · (U_EL_edge \ w) (fundamental matrix, integration/xi_psi) +# EL : ξ_EL(ψ) = U_EL(ψ) · (U_EL_edge \ w) (fundamental matrix, ForceFreeStates/Solutions/ForwardIntegration/xi_psi) # gal: ξ_gal(ψ) = U_gal(ψ) · w (identity-at-edge ⇒ coefficient is w itself) # w = eigenvector of the total energy operator W = W_plasma + W_vacuum (FreeBoundaryStability). # @@ -19,12 +19,12 @@ ksel = length(ARGS) >= 3 ? ARGS[3] : "highest" to_c(a) = eltype(a) <: Complex ? ComplexF64.(a) : map(x -> ComplexF64(x.re, x.im), a) et, wt, u1, psiE, gxi, psiG, issing, mlow, sing_psi = h5open(h5path) do f - (to_c(read(f["FreeBoundaryStability/eigenmode_energies"])), - to_c(read(f["FreeBoundaryStability/W_freeboundary_eigenmodes"])), - to_c(read(f["integration/xi_psi"])), read(f["integration/psi"]), - to_c(read(f["galerkin/match/xi"])), read(f["galerkin/solution/psi"]), - Bool.(read(f["galerkin/solution/issing"])), read(f["info/mlow"]), - read(f["galerkin/sing_psi"])) + (to_c(read(f["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"])), + to_c(read(f["ForceFreeStates/FreeBoundaryStability/W_freeboundary_eigenmodes"])), + to_c(read(f["ForceFreeStates/Solutions/ForwardIntegration/xi_psi"])), read(f["ForceFreeStates/Solutions/ForwardIntegration/psi"]), + to_c(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/xi"])), read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi"]), + Bool.(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/issing"])), read(f["Info/mlow"]), + read(f["SingularSurfaces/GalerkinDeltaPrime/sing_psi"])) end mpert = size(u1, 1) diff --git a/benchmarks/compare_jbgradpsi_m2.jl b/benchmarks/compare_jbgradpsi_m2.jl index b8da06fcc..dd30afbd2 100644 --- a/benchmarks/compare_jbgradpsi_m2.jl +++ b/benchmarks/compare_jbgradpsi_m2.jl @@ -1,10 +1,10 @@ -# Compare the area-normalized b^ψ (perturbed_equilibrium/response/psi_area = b^ψ/⟨J·|∇ψ|⟩_θ) for one +# Compare the area-normalized b^ψ (PerturbedEquilibrium/Response/psi_area = b^ψ/⟨J·|∇ψ|⟩_θ) for one # poloidal harmonic between two GPEC runs that are identical except for which ξ feeds PerturbedEquilibrium: # (1) IDEAL galerkin matched ξ (gal_match_flag=true, gal_ideal_flag=true) # (2) SHOOTING ξ (gal_match_flag=false) # -# PE writes no ψ grid, so it's reconstructed: gal-ideal → galerkin/solution/psi minus issing points; -# shooting → integration/psi. +# PE writes no ψ grid, so it's reconstructed: gal-ideal → ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi minus issing points; +# shooting → ForceFreeStates/Solutions/ForwardIntegration/psi. # Usage: julia --project=. benchmarks/compare_jbgradpsi_m2.jl [gal_h5] [shoot_h5] [out.png] [m] using HDF5, Plots, Printf @@ -18,14 +18,14 @@ to_c(a) = eltype(a) <: Complex ? ComplexF64.(a) : map(x -> ComplexF64(x.re, x.im # gal-ideal run: PE grid = gal solution grid with the on-surface (issing) points dropped pa_g, psi_g, mlow, sing_psi, sing_m = h5open(gal_h5) do f - pa = to_c(read(f["perturbed_equilibrium/response/psi_area"])) # [npsi, mpert] - iss = Bool.(read(f["galerkin/solution/issing"])) - (pa, read(f["galerkin/solution/psi"])[.!iss], read(f["info/mlow"]), - read(f["galerkin/sing_psi"]), read(f["galerkin/sing_m"])) + pa = to_c(read(f["PerturbedEquilibrium/Response/psi_area"])) # [npsi, mpert] + iss = Bool.(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/issing"])) + (pa, read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi"])[.!iss], read(f["Info/mlow"]), + read(f["SingularSurfaces/GalerkinDeltaPrime/sing_psi"]), read(f["SingularSurfaces/GalerkinDeltaPrime/sing_m"])) end -# shooting run: PE grid = integration/psi +# shooting run: PE grid = ForceFreeStates/Solutions/ForwardIntegration/psi pa_s, psi_s = h5open(sh_h5) do f - (to_c(read(f["perturbed_equilibrium/response/psi_area"])), read(f["integration/psi"])) + (to_c(read(f["PerturbedEquilibrium/Response/psi_area"])), read(f["ForceFreeStates/Solutions/ForwardIntegration/psi"])) end size(pa_g, 1) == length(psi_g) || error("gal grid mismatch: npsi=$(size(pa_g,1)) vs grid=$(length(psi_g))") diff --git a/benchmarks/equil_psihigh_scan.jl b/benchmarks/equil_psihigh_scan.jl index b4917fd3c..56f6e9ec6 100644 --- a/benchmarks/equil_psihigh_scan.jl +++ b/benchmarks/equil_psihigh_scan.jl @@ -69,7 +69,7 @@ function run_ffs_et1(config_path::String, eq_type::String, psihigh::Float64)::Fl try GeneralizedPerturbedEquilibrium.main([tmpdir]) h5open(joinpath(tmpdir, "gpec.h5"), "r") do h5 - et = read(h5["FreeBoundaryStability/eigenmode_energies"]) + et = read(h5["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) return real(et[1]) end catch diff --git a/benchmarks/gal_validation/gal_beta_scan.jl b/benchmarks/gal_validation/gal_beta_scan.jl index a1ee33c5e..11b8fd69e 100644 --- a/benchmarks/gal_validation/gal_beta_scan.jl +++ b/benchmarks/gal_validation/gal_beta_scan.jl @@ -1,8 +1,8 @@ #!/usr/bin/env julia # Gal-enabled TJ-analytic beta (pc) scan: at each pc, run the full GPEC pipeline with gal_flag=true # and capture three Δ′ measures per resonant surface (m=2 q=2, m=3 q=3): -# gal = galerkin/pest3_Delta diagonal (RDCON singular-Galerkin, this port) -# stride = singular/delta_prime_matrix diag (STRIDE BVP) +# gal = SingularSurfaces/GalerkinDeltaPrime/pest3_Delta diagonal (RDCON singular-Galerkin, this port) +# stride = SingularSurfaces/delta_prime_matrix diag (STRIDE BVP) # cajump = (ca_r-ca_l)/((2pi)^2 psio) (ca-jump primary) # Reuses examples/LAR_beta_scan/gpec.toml. Writes gal_beta_scan_results.h5 + CSV next to this script. using Pkg @@ -19,18 +19,18 @@ const BASE = TOML.parsefile(joinpath(REPO, "examples", "LAR_beta_scan", "gpec.to function extract(h5) h5open(h5, "r") do f out = Dict{String,Any}() - out["q0"] = read(f, "equil/q0"); out["qmax"] = read(f, "equil/qmax") - out["dW_total"] = real(read(f, "vacuum/et")[1]) - psio = read(f, "equil/psio"); denom = (2pi)^2 * psio - mlow = read(f, "info/mlow"); mpert = read(f, "info/mpert"); nlow = read(f, "info/nlow") + out["q0"] = read(f, "Equilibrium/q0"); out["qmax"] = read(f, "Equilibrium/qmax") + out["dW_total"] = real(read(f, "ForceFreeStates/FreeBoundaryStability/eigenmode_energies")[1]) + psio = read(f, "Equilibrium/psio"); denom = (2pi)^2 * psio + mlow = read(f, "Info/mlow"); mpert = read(f, "Info/mpert"); nlow = read(f, "Info/nlow") # surface m-values (STRIDE side) - m_s = vec(read(f, "singular/m")); n_s = vec(read(f, "singular/n")); msing = length(m_s) + m_s = vec(read(f, "SingularSurfaces/m")); n_s = vec(read(f, "SingularSurfaces/n")); msing = length(m_s) # STRIDE delta_prime_matrix - dpm = haskey(f["singular"], "delta_prime_matrix") ? read(f, "singular/delta_prime_matrix") : nothing - cal = read(f, "singular/ca_left"); car = read(f, "singular/ca_right") + dpm = haskey(f["singular"], "delta_prime_matrix") ? read(f, "SingularSurfaces/delta_prime_matrix") : nothing + cal = read(f, "SingularSurfaces/ca_left"); car = read(f, "SingularSurfaces/ca_right") # gal - gm = haskey(f, "galerkin") && haskey(f["galerkin"], "sing_m") ? vec(read(f, "galerkin/sing_m")) : Int[] - gp = haskey(f, "galerkin") && haskey(f["galerkin"], "pest3_Delta") ? read(f, "galerkin/pest3_Delta") : nothing + gm = haskey(f, "SingularSurfaces/GalerkinDeltaPrime/sing_m") ? vec(read(f, "SingularSurfaces/GalerkinDeltaPrime/sing_m")) : Int[] + gp = haskey(f, "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta") ? read(f, "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta") : nothing for (s, m) in enumerate(m_s) key = "m$(m)" # ca-jump diff --git a/benchmarks/gal_validation/gal_epsilon_scan.jl b/benchmarks/gal_validation/gal_epsilon_scan.jl index d615b5a6c..04fe00f13 100644 --- a/benchmarks/gal_validation/gal_epsilon_scan.jl +++ b/benchmarks/gal_validation/gal_epsilon_scan.jl @@ -1,8 +1,8 @@ #!/usr/bin/env julia # Gal-enabled TJ-analytic beta (pc) scan: at each pc, run the full GPEC pipeline with gal_flag=true # and capture three Δ′ measures per resonant surface (m=2 q=2, m=3 q=3): -# gal = galerkin/pest3_Delta diagonal (RDCON singular-Galerkin, this port) -# stride = singular/delta_prime_matrix diag (STRIDE BVP) +# gal = SingularSurfaces/GalerkinDeltaPrime/pest3_Delta diagonal (RDCON singular-Galerkin, this port) +# stride = SingularSurfaces/delta_prime_matrix diag (STRIDE BVP) # cajump = (ca_r-ca_l)/((2pi)^2 psio) (ca-jump primary) # Reuses examples/LAR_epsilon_scan/gpec.toml. Writes gal_epsilon_scan_results.h5 + CSV next to this script. using Pkg @@ -19,18 +19,18 @@ const BASE = TOML.parsefile(joinpath(REPO, "examples", "LAR_epsilon_scan", "gpec function extract(h5) h5open(h5, "r") do f out = Dict{String,Any}() - out["q0"] = read(f, "equil/q0"); out["qmax"] = read(f, "equil/qmax") - out["dW_total"] = real(read(f, "vacuum/et")[1]) - psio = read(f, "equil/psio"); denom = (2pi)^2 * psio - mlow = read(f, "info/mlow"); mpert = read(f, "info/mpert"); nlow = read(f, "info/nlow") + out["q0"] = read(f, "Equilibrium/q0"); out["qmax"] = read(f, "Equilibrium/qmax") + out["dW_total"] = real(read(f, "ForceFreeStates/FreeBoundaryStability/eigenmode_energies")[1]) + psio = read(f, "Equilibrium/psio"); denom = (2pi)^2 * psio + mlow = read(f, "Info/mlow"); mpert = read(f, "Info/mpert"); nlow = read(f, "Info/nlow") # surface m-values (STRIDE side) - m_s = vec(read(f, "singular/m")); n_s = vec(read(f, "singular/n")); msing = length(m_s) + m_s = vec(read(f, "SingularSurfaces/m")); n_s = vec(read(f, "SingularSurfaces/n")); msing = length(m_s) # STRIDE delta_prime_matrix - dpm = haskey(f["singular"], "delta_prime_matrix") ? read(f, "singular/delta_prime_matrix") : nothing - cal = read(f, "singular/ca_left"); car = read(f, "singular/ca_right") + dpm = haskey(f["singular"], "delta_prime_matrix") ? read(f, "SingularSurfaces/delta_prime_matrix") : nothing + cal = read(f, "SingularSurfaces/ca_left"); car = read(f, "SingularSurfaces/ca_right") # gal - gm = haskey(f, "galerkin") && haskey(f["galerkin"], "sing_m") ? vec(read(f, "galerkin/sing_m")) : Int[] - gp = haskey(f, "galerkin") && haskey(f["galerkin"], "pest3_Delta") ? read(f, "galerkin/pest3_Delta") : nothing + gm = haskey(f, "SingularSurfaces/GalerkinDeltaPrime/sing_m") ? vec(read(f, "SingularSurfaces/GalerkinDeltaPrime/sing_m")) : Int[] + gp = haskey(f, "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta") ? read(f, "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta") : nothing for (s, m) in enumerate(m_s) key = "m$(m)" # ca-jump diff --git a/benchmarks/gal_validation/pfac_study/gal_pfac_scan2d.jl b/benchmarks/gal_validation/pfac_study/gal_pfac_scan2d.jl index a0446b721..92ed200ef 100644 --- a/benchmarks/gal_validation/pfac_study/gal_pfac_scan2d.jl +++ b/benchmarks/gal_validation/pfac_study/gal_pfac_scan2d.jl @@ -9,10 +9,10 @@ const PFACS = [5e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1] function extract(h5) h5open(h5, "r") do f - mlow = read(f, "info/mlow"); mpert = read(f, "info/mpert"); nlow = read(f, "info/nlow") - m_s = vec(read(f, "singular/m")) - dpm = read(f, "singular/delta_prime_matrix") - gm = vec(read(f, "galerkin/sing_m")); gp = read(f, "galerkin/pest3_Delta") + mlow = read(f, "Info/mlow"); mpert = read(f, "Info/mpert"); nlow = read(f, "Info/nlow") + m_s = vec(read(f, "SingularSurfaces/m")) + dpm = read(f, "SingularSurfaces/delta_prime_matrix") + gm = vec(read(f, "SingularSurfaces/GalerkinDeltaPrime/sing_m")); gp = read(f, "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta") g3 = NaN; s3 = NaN; g2 = NaN; s2 = NaN for (s, m) in enumerate(m_s) gi = findfirst(==(m), gm) diff --git a/benchmarks/plot_xi_eigenmode.jl b/benchmarks/plot_xi_eigenmode.jl index e7c675207..9fd1a58c3 100644 --- a/benchmarks/plot_xi_eigenmode.jl +++ b/benchmarks/plot_xi_eigenmode.jl @@ -3,9 +3,9 @@ # The total energy operator W = W_plasma + W_vacuum (free_run!, Free.jl); its eigenvectors are the # free-boundary edge displacement patterns and the eigenvalues are δW. This picks the eigenmode with the # largest Re(eigenvalue) and reconstructs its radial profile by projecting the EL fundamental matrix -# (integration/xi_psi) onto that edge eigenvector: c = U_edge \ w, ξ(ψ) = U(ψ)·c. +# (ForceFreeStates/Solutions/ForwardIntegration/xi_psi) onto that edge eigenvector: c = U_edge \ w, ξ(ψ) = U(ψ)·c. # -# Requires a run with populate_dense_xi=true so integration/xi_psi is the dense axis-basis fundamental +# Requires a run with populate_dense_xi=true so ForceFreeStates/Solutions/ForwardIntegration/xi_psi is the dense axis-basis fundamental # matrix (not the Riccati S-matrices). # Usage: julia --project=. benchmarks/plot_xi_eigenmode.jl [path/to/gpec.h5] [out.png] @@ -17,12 +17,12 @@ outpng = length(ARGS) >= 2 ? ARGS[2] : joinpath(@__DIR__, "xi_eigenmode.png") to_c(a) = eltype(a) <: Complex ? ComplexF64.(a) : map(x -> ComplexF64(x.re, x.im), a) et, wt, u1, psi, mlow, sing_psi = h5open(h5path) do f - (to_c(read(f["FreeBoundaryStability/eigenmode_energies"])), - to_c(read(f["FreeBoundaryStability/W_freeboundary_eigenmodes"])), - to_c(read(f["integration/xi_psi"])), - read(f["integration/psi"]), - read(f["info/mlow"]), - haskey(f, "galerkin/sing_psi") ? read(f["galerkin/sing_psi"]) : Float64[]) + (to_c(read(f["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"])), + to_c(read(f["ForceFreeStates/FreeBoundaryStability/W_freeboundary_eigenmodes"])), + to_c(read(f["ForceFreeStates/Solutions/ForwardIntegration/xi_psi"])), + read(f["ForceFreeStates/Solutions/ForwardIntegration/psi"]), + read(f["Info/mlow"]), + haskey(f, "SingularSurfaces/GalerkinDeltaPrime/sing_psi") ? read(f["SingularSurfaces/GalerkinDeltaPrime/sing_psi"]) : Float64[]) end mpert, _, nstep = size(u1) diff --git a/benchmarks/scan_resistivity_m2.jl b/benchmarks/scan_resistivity_m2.jl index 5ed0931dc..6a5695efd 100644 --- a/benchmarks/scan_resistivity_m2.jl +++ b/benchmarks/scan_resistivity_m2.jl @@ -1,4 +1,4 @@ -# Plot the m=2 area-normalized b^ψ (perturbed_equilibrium/response/psi_area) across a resistivity scan +# Plot the m=2 area-normalized b^ψ (PerturbedEquilibrium/Response/psi_area) across a resistivity scan # of the RESISTIVE gal matched PE runs (gal_match_flag=true, gal_ideal_flag=false), one curve per η. # Overlays the shooting (ideal, η→0) reference. The η-scan dirs are produced by the bash loop over # /tmp/etascan_ (each a copy of the 0.993 config with gal_eta scaled). @@ -14,10 +14,10 @@ to_c(a) = eltype(a) <: Complex ? ComplexF64.(a) : map(x -> ComplexF64(x.re, x.im # read m=target area-normalized b^ψ on the run's PE grid function read_m2(h5; gal::Bool) h5open(h5) do f - pa = to_c(read(f["perturbed_equilibrium/response/psi_area"])) # [npsi, mpert] - col = mtarget - read(f["info/mlow"]) + 1 - psi = gal ? read(f["galerkin/solution/psi"])[.!Bool.(read(f["galerkin/solution/issing"]))] : - read(f["integration/psi"]) + pa = to_c(read(f["PerturbedEquilibrium/Response/psi_area"])) # [npsi, mpert] + col = mtarget - read(f["Info/mlow"]) + 1 + psi = gal ? read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi"])[.!Bool.(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/issing"]))] : + read(f["ForceFreeStates/Solutions/ForwardIntegration/psi"]) (psi, pa[:, col]) end end @@ -34,7 +34,7 @@ eta_ref = 8e-8 # rational surface for m=target sing_psi, sing_m = h5open(joinpath(scandirs[1], "gpec.h5")) do f - (read(f["galerkin/sing_psi"]), read(f["galerkin/sing_m"])) + (read(f["SingularSurfaces/GalerkinDeltaPrime/sing_psi"]), read(f["SingularSurfaces/GalerkinDeltaPrime/sing_m"])) end psi_res = mtarget in sing_m ? sing_psi[findfirst(==(mtarget), sing_m)] : NaN diff --git a/benchmarks/scan_rotation_m2.jl b/benchmarks/scan_rotation_m2.jl index 63cf2f04d..bd01dfe4e 100644 --- a/benchmarks/scan_rotation_m2.jl +++ b/benchmarks/scan_rotation_m2.jl @@ -1,4 +1,4 @@ -# Plot the m=2 area-normalized b^ψ (perturbed_equilibrium/response/psi_area) across a ROTATION scan +# Plot the m=2 area-normalized b^ψ (PerturbedEquilibrium/Response/psi_area) across a ROTATION scan # of the resistive gal matched PE runs (gal_match_flag=true, gal_ideal_flag=false), fixed η=8e-8, # rotation f = 1,2,4,8,16 Hz (forced eigenvalue γ_s = 2πi·n·f). One curve per rotation; overlays the # shooting (ideal) reference. Scan dirs produced by the bash loop over /tmp/rotscan_. @@ -13,10 +13,10 @@ to_c(a) = eltype(a) <: Complex ? ComplexF64.(a) : map(x -> ComplexF64(x.re, x.im function read_m2(h5; gal::Bool) h5open(h5) do f - pa = to_c(read(f["perturbed_equilibrium/response/psi_area"])) - col = mtarget - read(f["info/mlow"]) + 1 - psi = gal ? read(f["galerkin/solution/psi"])[.!Bool.(read(f["galerkin/solution/issing"]))] : - read(f["integration/psi"]) + pa = to_c(read(f["PerturbedEquilibrium/Response/psi_area"])) + col = mtarget - read(f["Info/mlow"]) + 1 + psi = gal ? read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi"])[.!Bool.(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/issing"]))] : + read(f["ForceFreeStates/Solutions/ForwardIntegration/psi"]) (psi, pa[:, col]) end end @@ -29,7 +29,7 @@ scandirs, rots = scandirs[ord], rots[ord] @printf("%d scan runs: rotation f = %s Hz (η fixed = 8e-8)\n", length(rots), join((@sprintf("%g", r) for r in rots), ", ")) sing_psi, sing_m = h5open(joinpath(scandirs[1], "gpec.h5")) do f - (read(f["galerkin/sing_psi"]), read(f["galerkin/sing_m"])) + (read(f["SingularSurfaces/GalerkinDeltaPrime/sing_psi"]), read(f["SingularSurfaces/GalerkinDeltaPrime/sing_m"])) end psi_res = mtarget in sing_m ? sing_psi[findfirst(==(mtarget), sing_m)] : NaN diff --git a/benchmarks/verify_gal_ideal.jl b/benchmarks/verify_gal_ideal.jl index 1c0d0ad41..45cf79127 100644 --- a/benchmarks/verify_gal_ideal.jl +++ b/benchmarks/verify_gal_ideal.jl @@ -8,10 +8,10 @@ h5path = length(ARGS) >= 1 ? ARGS[1] : "/tmp/gal_ideal_test/gpec.h5" to_c(a) = eltype(a) <: Complex ? ComplexF64.(a) : map(x -> ComplexF64(x.re, x.im), a) cout, deltar, mxi, mdxi, sols, sols_d, sing_psi = h5open(h5path) do f - (to_c(read(f["galerkin/match/cout"])), to_c(read(f["galerkin/match/deltar"])), - to_c(read(f["galerkin/match/xi"])), to_c(read(f["galerkin/match/xi_deriv"])), - to_c(read(f["galerkin/solution/xi"])), to_c(read(f["galerkin/solution/xi_deriv"])), - read(f["galerkin/sing_psi"])) + (to_c(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/cout"])), to_c(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/deltar"])), + to_c(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/xi"])), to_c(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/xi_deriv"])), + to_c(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi"])), to_c(read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi_deriv"])), + read(f["SingularSurfaces/GalerkinDeltaPrime/sing_psi"])) end msing = length(sing_psi) mpert, ngrid, mcoil = size(mxi) diff --git a/benchmarks/verify_gal_match.jl b/benchmarks/verify_gal_match.jl index 86f8cfffe..ee6a06290 100644 --- a/benchmarks/verify_gal_match.jl +++ b/benchmarks/verify_gal_match.jl @@ -1,4 +1,4 @@ -# Piece 2 verification: RPEC outer↔inner matched solution (galerkin/match/*). +# Piece 2 verification: RPEC outer↔inner matched solution (ForceFreeStates/Solutions/GalerkinIntegration/Match/*). # 1. linear-solve residual ‖mat·cof − rmat‖/‖rmat‖ # 2. matched ξ / ξ′ finiteness # 3. edge column == identity basis: each coil drive j must give ξ_edge = e_j (the j-th harmonic), @@ -11,10 +11,10 @@ h5path = length(ARGS) >= 1 ? ARGS[1] : "examples/DIIID-like_gal_resistive_exampl @info "Reading $h5path" xi, dxi, cout, cin, deltar, eig, resid, sing_psi = h5open(h5path) do f - (read(f["galerkin/match/xi"]), read(f["galerkin/match/xi_deriv"]), - read(f["galerkin/match/cout"]), read(f["galerkin/match/cin"]), - read(f["galerkin/match/deltar"]), read(f["galerkin/match/rpec_eig"]), - read(f["galerkin/match/residual"]), read(f["galerkin/sing_psi"])) + (read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/xi"]), read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/xi_deriv"]), + read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/cout"]), read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/cin"]), + read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/deltar"]), read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/rpec_eig"]), + read(f["ForceFreeStates/Solutions/GalerkinIntegration/Match/residual"]), read(f["SingularSurfaces/GalerkinDeltaPrime/sing_psi"])) end # HDF5 stores ComplexF64 as a compound (re,im); convert if needed to_c(a) = eltype(a) <: Complex ? a : map(x -> ComplexF64(x.re, x.im), a) diff --git a/benchmarks/verify_gal_solution.jl b/benchmarks/verify_gal_solution.jl index e6c02b653..66dac7931 100644 --- a/benchmarks/verify_gal_solution.jl +++ b/benchmarks/verify_gal_solution.jl @@ -1,5 +1,5 @@ # Piece 1 verification: reconstructed gal ξ(ψ) and analytic ξ′(ψ). -# 1. shapes / finiteness sanity of galerkin/solution arrays +# 1. shapes / finiteness sanity of ForceFreeStates/Solutions/GalerkinIntegration/Solution arrays # 2. analytic ξ′ vs centered finite-difference of ξ — agree in the smooth interior, diverge at the # packed edge (the spline-endpoint-derivative artifact we deliberately avoid) # Usage: julia --project=. verify_gal_solution.jl [path/to/gpec.h5] @@ -9,9 +9,9 @@ h5path = length(ARGS) >= 1 ? ARGS[1] : "examples/DIIID-like_gal_resistive_exampl @info "Reading $h5path" psi, q, issing, xi, dxi, sing_psi = h5open(h5path) do f - (read(f["galerkin/solution/psi"]), read(f["galerkin/solution/q"]), - read(f["galerkin/solution/issing"]), read(f["galerkin/solution/xi"]), - read(f["galerkin/solution/xi_deriv"]), read(f["galerkin/sing_psi"])) + (read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi"]), read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/q"]), + read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/issing"]), read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi"]), + read(f["ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi_deriv"]), read(f["SingularSurfaces/GalerkinDeltaPrime/sing_psi"])) end issing = Bool.(issing) mpert, ngrid, nsol = size(xi) diff --git a/docs/development/architecture.md b/docs/development/architecture.md index b9ca1a43f..2eb4a2422 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -155,7 +155,7 @@ The complete GPEC analysis pipeline: 5. **Output**: - All results saved to single HDF5 file (default: `gpec.h5`) - - HDF5 groups: `input/`, `info/`, `equil/`, `splines/`, `locstab/`, `integration/`, `singular/`, `vacuum/`, and perturbed equilibrium data + - Top-level HDF5 groups: `Info/`, `Input/`, `Equilibrium/`, `ForceFreeStates/`, `LocalStability/`, `SingularSurfaces/`, `PerturbedEquilibrium/`, `KineticForces/`, `Tearing/`, `SurfaceGeometries/` (see `docs/development/hdf5-conventions.md`) ## Key Data Structures diff --git a/docs/development/hdf5-conventions.md b/docs/development/hdf5-conventions.md new file mode 100644 index 000000000..f62f8777d --- /dev/null +++ b/docs/development/hdf5-conventions.md @@ -0,0 +1,51 @@ +# HDF5 Output Schema Conventions + +Conventions for the structure and naming of the `gpec.h5` output file. Every writer that adds a group or dataset must follow these rules; the naming is enforced by `test/runtests_h5_schema.jl`. + +## Governing principle: physics-first organization + +**The schema must be intuitive to a plasma physicist who is not a developer of this code.** The top level largely mirrors the TOML sections / major `src/` modules — which are themselves organized along physics lines — but **physics intuition wins whenever the two diverge**. Worked examples of that rule: + +- All per-rational-surface stability results consolidate under `SingularSurfaces/`, regardless of which algorithm produced them: the ideal BVP `delta_prime_matrix`, the GGJ coefficients, and the Galerkin outer-region Δ′/PEST-3 results (`GalerkinDeltaPrime/`) live side by side. Provenance is recorded in the subgroup name, not by scattering results across producer-owned groups. +- `EulerLagrangeMatrices` over a bare `Matrices` — group names must say what the data *is*, not which array it came from. Same reasoning renamed `records/` → `EnergyIntegrals/` and `matrices_/` → `KineticMatrices/`. + +Physics-topic groups elevated to top level (rather than nested under their producer): `Info/`, `Input/`, `SingularSurfaces/`, `LocalStability/`, `SurfaceGeometries/`. + +## Naming rules + +- **Groups are CamelCase at every level** (`ForceFreeStates/`, `PerSurface/`, `GalerkinDeltaPrime/`). +- **Datasets (leaves) are snake_case** (`eigenmode_energies`, `delta_prime_matrix`). Established physics symbols keep their natural case (`E`, `F`, `Q_root_real`, `pest3_Delta`, `2piF`). +- **Data-driven tokens are stored verbatim**: coil-set names under `Input/RawInputs/Coils/`, KineticForces method tokens (`fgar`, …), scan indices (`Surface_`, `psi_`). + +## Inputs live only under `Input/` + +`Input/gpec_toml_raw` stores the full merged TOML, and `Input/RawInputs/` stores the raw equilibrium/forcing/coil data — together they make `gpec.h5` a self-contained rerun snapshot (`Rerun.jl` reconstructs every control struct from them; the writer/reader path pair is locked by shared `H5_*` consts in `GeneralizedPerturbedEquilibrium.jl`). **Never echo TOML flags or control-struct values into any other group** — every group outside `Input/` is derived output. (The former `kinetic/` and `slayer/settings/` echoes were removed under this rule.) + +## Schema + +Top level (10 groups): + +| Group | Contents | +|---|---| +| `Info/` | Run metadata: `git_version`, mode-number ranges (`mpert`, `mlow`, …, `mn_index`), `psilim`, `qlim` | +| `Input/` | Rerun snapshot: `gpec_toml_raw`, `RawInputs/{Equilibrium, ForcingTerms, Coils/}` | +| `Equilibrium/` | Scalars (β, q₀, q95, …) plus `Profiles/` (1-D: xs, 2piF, mu0p, dVdpsi, q) and `Geometry/` (2-D: rcoords, offset, nu, jac) | +| `ForceFreeStates/` | `Solutions/ForwardIntegration/` (u-solutions), `Solutions/GalerkinIntegration/` (`Solution/`, `Match/`, `msing`), `EulerLagrangeMatrices/{Ideal,Kinetic}`, `FreeBoundaryStability/`, `EdgeScan/` | +| `LocalStability/` | Mercier `di`, resistive interchange `dr`, `ballooning_Delta_prime`, ballooning α boundary | +| `SingularSurfaces/` | Per-rational-surface data: ψ, q, m/n, GGJ coefficients, `delta_prime_matrix`/`delta_prime_raw`/`delta_coil`, `GalerkinDeltaPrime/`, `Kinetic/` | +| `PerturbedEquilibrium/` | `ForcingModes/`, `Response/`, `ResponseMatrices/`, `SingularCoupling/`, `Energies/`, control-surface spectra | +| `KineticForces/` | `/` (torque/energy profiles, `EnergyIntegrals/`, `KineticMatrices/`) | +| `Tearing/` | `PerSurface/` (+ `DpMatrix/`), `Roots/`, `LayerWidths/`, `Diagnostics/{ValidRoots,Poles,FilteredRoots}`, `Scan/Surface_/` | +| `SurfaceGeometries/` | `{Plasma,Wall}/{x,y,z}` point clouds | + +Reserved (documented, not yet written): `ForceFreeStates/Solutions/RiccatiIntegration/` — the third integrator backend slot alongside `ForwardIntegration` and `GalerkinIntegration`. + +## File-wide conventions + +- Complex numbers are stored as the native HDF5.jl compound type (readable by h5py as a compound dtype). +- `NaN` is the not-computed sentinel in numeric datasets (e.g. auto-derived settings, rootless growth-rate entries). +- Ragged (variable-length) data uses the flat-plus-`offsets` companion pattern (`offsets[k+1] - offsets[k]` = length of row `k`) rather than HDF5 VLEN types, e.g. `KineticForces//EnergyIntegrals/` and `Tearing/Diagnostics/*`. + +## Back-compatibility policy + +Schema renames are clean breaks in `src/` readers — no dual-path reads. The **only** legacy fallback lives in the regression harness (`regression-harness/src/extractor.jl`, `LEGACY_PREFIX_MAP`), so cross-commit comparisons and `--ref-range` scans keep working across a rename boundary. When renaming a path, update the writer, all readers, the case TOMLs, and add the new→old pair to that map. diff --git a/docs/src/ballooning.md b/docs/src/ballooning.md index 3313e5351..72697da20 100644 --- a/docs/src/ballooning.md +++ b/docs/src/ballooning.md @@ -909,5 +909,5 @@ Mercier calculation and the new `Bal.jl` calculation. The local-stability output now stores ballooning ``\Delta'`` in the fourth `locstab_fs` entry. In the HDF5 output this is written as -`locstab/ballooning_Delta_prime`, distinct from the tearing ``\Delta'`` outputs -under `singular/` and `perturbed_equilibrium/singular_coupling/`. +`LocalStability/ballooning_Delta_prime`, distinct from the tearing ``\Delta'`` outputs +under `SingularSurfaces/` and `PerturbedEquilibrium/SingularCoupling/`. diff --git a/docs/src/conventions.md b/docs/src/conventions.md index 143ab7d1b..ad0c917d6 100644 --- a/docs/src/conventions.md +++ b/docs/src/conventions.md @@ -307,19 +307,19 @@ The internal flux-conform operator is just ``R = \Sigma\sqrt{A} = `` `rootarea_t - **`forcing_b` / `forcing_b_root_area` / `forcing_b_area`** (and the `response_*` triplet) — the control-surface forcing and response spectra in the bare (``b``), root-area-weighted (``\tilde b``) and - area-weighted (``\bar b``) representations, under `perturbed_equilibrium/`. + area-weighted (``\bar b``) representations, under `PerturbedEquilibrium/`. - **`b_n`** — the bare normal field ``\mathbf{b}\cdot\hat{\mathbf n}`` (and the area-weighted radial field - `b_psi_area_weighted`), under `perturbed_equilibrium/response/`. + `b_psi_area_weighted`), under `PerturbedEquilibrium/Response/`. - **`resonant_area_weighted_field`** / **`C_resonant_area_weighted_field`** — the resonant area-weighted field ``\bar b^{\,r} = \Phi^r/A^r`` and its coupling matrix, under - `perturbed_equilibrium/singular_coupling/`. The sibling `penetrated_area_weighted_field` follows the + `PerturbedEquilibrium/SingularCoupling/`. The sibling `penetrated_area_weighted_field` follows the same convention. - **Root-area-weighted (``\tilde b``) space** — the control-surface response matrices (`permeability`, `reluctance`, `plasma_inductance`, `surface_inductance`) are stored in this coordinate-invariant space - under `perturbed_equilibrium/response_matrices/`. The stored `rootarea_to_area_weight_operator` ``S`` + under `PerturbedEquilibrium/ResponseMatrices/`. The stored `rootarea_to_area_weight_operator` ``S`` recovers the area-weighted field forms (``L_{\bar b} = S\,\tilde L\,S^\dagger``) and the scalar `surface_area` ``A`` recovers flux (``\Phi = A\,\bar b``). -- **Eigenmode energies** (`FreeBoundaryStability/eigenmode_energies`) — the generalized eigenvalues of the +- **Eigenmode energies** (`ForceFreeStates/FreeBoundaryStability/eigenmode_energies`) — the generalized eigenvalues of the pencil ``W\,v = \lambda\,N\,v``, where ``N`` is the power-normalization (surface-norm) matrix built from the Jacobian Fourier coefficients: ``\xi^\dagger N\,\xi = \oint J\,|\xi(\theta)|^2\,d\theta\,/\,V' = \langle|\xi|^2\rangle``. The eigenvalues are stationary values of the power quotient diff --git a/docs/src/forcing_terms.md b/docs/src/forcing_terms.md index f8427de84..ba2f7d752 100644 --- a/docs/src/forcing_terms.md +++ b/docs/src/forcing_terms.md @@ -179,7 +179,7 @@ Use `convert_coil_dat_to_h5` / `convert_coil_h5_to_dat` to migrate legacy files, ### Rerun snapshot When a coil run writes `gpec.h5`, the coil geometry actually used (after shifts/tilts and with -currents) is captured under `input/raw_inputs/coils/`. The run can then be replayed with +currents) is captured under `Input/RawInputs/Coils/`. The run can then be replayed with `main_from_h5` even if the original `.dat`/`.h5` files are gone: - `--coil-source forcing-modes` (default) recomputes the field from the stored TOML coil config diff --git a/docs/src/galerkin.md b/docs/src/galerkin.md index d9ac34436..e55906e2b 100644 --- a/docs/src/galerkin.md +++ b/docs/src/galerkin.md @@ -8,7 +8,7 @@ a single global banded system. Cells adjacent to each rational surface ("resona singular behavior is built into the basis rather than resolved numerically. The solve produces the inter-surface Δ′ matrix and the PEST-3 matching blocks -(``A'``, ``B'``, ``\Gamma'``, ``\Delta'``), written to the HDF5 output under the `galerkin/` +(``A'``, ``B'``, ``\Gamma'``, ``\Delta'``), written to the HDF5 output under the `SingularSurfaces/GalerkinDeltaPrime/` group. These are the outer-region inputs to resistive matched-asymptotic stability analysis [Glasser 2016, Phys. Plasmas **23**, 072505]. diff --git a/docs/src/stability.md b/docs/src/stability.md index 9e2d7f8d4..ccd061460 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -142,26 +142,26 @@ Setting `local_stability_flag = true` in `[ForceFreeStates]` runs a local high-` stability scan over every flux surface, in addition to the global ideal analysis above. For the derivation and implementation details behind these diagnostics, see [Ballooning and Mercier Local Stability](ballooning.md). -Three diagnostics are produced and stored under the `locstab/` HDF5 group, each a profile +Three diagnostics are produced and stored under the `LocalStability/` HDF5 group, each a profile in normalized poloidal flux ``\psi``: -- **Mercier criterion ``D_I``** (`locstab/di`) — the ideal interchange criterion. A surface +- **Mercier criterion ``D_I``** (`LocalStability/di`) — the ideal interchange criterion. A surface is Mercier-unstable where ``D_I > 0``. It is evaluated from the ``\det(\bar{d}_0)`` of the integrated local-mode matrix. -- **Resistive interchange ``D_R``** (`locstab/dr`) — the Glasser–Greene–Johnson resistive +- **Resistive interchange ``D_R``** (`LocalStability/dr`) — the Glasser–Greene–Johnson resistive interchange criterion ``D_R = D_I + (H - 1/2)^2``. The ``D_I`` term is the same - ``\det(\bar{d}_0)`` value reported in `locstab/di`; ``H`` is computed from the legacy + ``\det(\bar{d}_0)`` value reported in `LocalStability/di`; ``H`` is computed from the legacy Mercier/GGJ flux-surface averages of the field and metric quantities. ``D_R > 0`` indicates resistive interchange instability. -- **Ballooning ``\Delta'``** (`locstab/ballooning_Delta_prime`) — the high-``n`` ballooning +- **Ballooning ``\Delta'``** (`LocalStability/ballooning_Delta_prime`) — the high-``n`` ballooning stability index, obtained by integrating the ballooning equation along the field line and taking the jump in the logarithmic derivative of the solution between the two asymptotic ends. !!! note "Two different Δ' quantities" - `locstab/ballooning_Delta_prime` is the **local high-``n`` ballooning** index and is + `LocalStability/ballooning_Delta_prime` is the **local high-``n`` ballooning** index and is distinct from the **resistive tearing** ``\Delta'`` described in the next section, which - is written under `singular/` and `perturbed_equilibrium/singular_coupling/delta_prime`. + is written under `SingularSurfaces/` and `PerturbedEquilibrium/SingularCoupling/delta_prime`. They measure different instabilities; do not confuse them. ### s–α diagram @@ -231,7 +231,7 @@ where ``\Phi_R[j]`` is the forward FM product from ``\psi_{R,j-1}`` to the junct ``\Phi_L[j]`` is the backward crossing FM from ``\psi_{L,j}`` to the junction. The matrix is only populated by the parallel FM path and is written to the HDF5 output -under `singular/delta_prime_matrix`. +under `SingularSurfaces/delta_prime_matrix`. ## Configuration reference diff --git a/docs/src/workflow.md b/docs/src/workflow.md index d872dcea6..6929ddc3c 100644 --- a/docs/src/workflow.md +++ b/docs/src/workflow.md @@ -171,16 +171,17 @@ Example configuration files are provided in: ## Output File: `gpec.h5` -All results are written to a single HDF5 file (default: `gpec.h5`). The file is organized into groups corresponding to pipeline stages: +All results are written to a single HDF5 file (default: `gpec.h5`). The top-level groups are organized by physics topic (see the schema conventions in `docs/development/hdf5-conventions.md`): | Group | Contents | |---|---| -| `input/` | Copy of the input configuration and equilibrium data | -| `info/` | Run metadata (version, timestamp, git hash) | -| `equil/` | Equilibrium profiles: q(ψ), pressure, current density, β | -| `splines/` | Spline coefficients for field quantities | -| `locstab/` | Local stability: Mercier criterion, shear | -| `integration/` | ODE integration results: energy matrices, eigenvalues | -| `singular/` | Per-surface data: ψ_s, m/n, Δ', small solution coefficients | -| `vacuum/` | Vacuum response matrices: wv, grri, grre | -| `perturbed/` | Perturbed equilibrium: ξ, b in mode space, island diagnostics | +| `Info/` | Run metadata: git version, mode-number ranges, ψ limit | +| `Input/` | Self-contained rerun snapshot: merged TOML blob, raw equilibrium/forcing/coil inputs | +| `Equilibrium/` | Equilibrium scalars (β, q₀, q95, …), 1-D profiles (`Profiles/`), 2-D geometry (`Geometry/`) | +| `ForceFreeStates/` | Stability solve: `Solutions/{ForwardIntegration,GalerkinIntegration}`, `EulerLagrangeMatrices/`, `FreeBoundaryStability/`, `EdgeScan/` | +| `LocalStability/` | Mercier D_I, resistive interchange D_R, ballooning Δ' profiles | +| `SingularSurfaces/` | Per-rational-surface data: ψ_s, q, m/n, GGJ coefficients, Δ' matrices (`GalerkinDeltaPrime/`), kinetic surfaces (`Kinetic/`) | +| `PerturbedEquilibrium/` | Plasma response: `ForcingModes/`, `Response/`, `ResponseMatrices/`, `SingularCoupling/`, `Energies/` | +| `KineticForces/` | NTV torque per method: energy integrals, kinetic matrices | +| `Tearing/` | SLAYER/GGJ inner-layer growth rates: `PerSurface/`, `Roots/`, `LayerWidths/`, `Diagnostics/`, `Scan/` | +| `SurfaceGeometries/` | Plasma and wall surface point clouds for visualization | diff --git a/examples/DIIID-like_SLAYER_example/gpec.toml b/examples/DIIID-like_SLAYER_example/gpec.toml index 2374ea514..d11d92cc1 100644 --- a/examples/DIIID-like_SLAYER_example/gpec.toml +++ b/examples/DIIID-like_SLAYER_example/gpec.toml @@ -57,7 +57,7 @@ singfac_min = 1e-4 # Fractional distance from rational q at which id ucrit = 1e4 # Maximum fraction of solutions allowed before re-normalized # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 1 # serial/bit-deterministic BVP — keeps the regression Δ' (and hence γ) reproducible populate_dense_xi = false # No PerturbedEquilibrium here; the dense EL pass has no consumer (auto-disabled under force_termination anyway). SLAYER needs only delta_prime_matrix from the parallel BVP. truncate_at_dW_peak = false # Edge-dW scan stays diagnostic; integration domain set by qhigh / psihigh / dmlim diff --git a/examples/DIIID-like_gal_resistive_example/gpec.toml b/examples/DIIID-like_gal_resistive_example/gpec.toml index 62b7930f5..25005e7be 100644 --- a/examples/DIIID-like_gal_resistive_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_example/gpec.toml @@ -45,7 +45,7 @@ singfac_min = 1e-4 # Fractional distance from rational q at which id ucrit = 1e4 # Maximum fraction of solutions allowed before re-normalized # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = false # Dense axis-basis ξ for the FFS HDF5 output. Not needed here: no [PerturbedEquilibrium] section, and the gal-matched path builds its own dense ξ. Set true only for a shooting-fed PE run. set_psilim_via_dmlim = false # Keep psilim at psihigh (do not truncate at last_rational_q + dmlim) diff --git a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml index a6c6612a9..b75e4347b 100644 --- a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml @@ -45,7 +45,7 @@ singfac_min = 1e-4 # Fractional distance from rational q at which id ucrit = 1e4 # Maximum fraction of solutions allowed before re-normalized # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = false # Dense axis-basis ξ for the FFS HDF5 output. Not needed here: no [PerturbedEquilibrium] section, and the gal-matched path builds its own dense ξ. Set true only for a shooting-fed PE run. set_psilim_via_dmlim = false # Keep psilim at psihigh (do not truncate at last_rational_q + dmlim) diff --git a/examples/DIIID-like_ideal_example/gpec.toml b/examples/DIIID-like_ideal_example/gpec.toml index a36d21271..62d7d97e6 100644 --- a/examples/DIIID-like_ideal_example/gpec.toml +++ b/examples/DIIID-like_ideal_example/gpec.toml @@ -50,7 +50,7 @@ singfac_min = 1e-4 # Fractional distance from rational q at which id ucrit = 1e4 # Column-norm threshold that triggers solution renormalization # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = true # Append serial-EL pass so dense ξ is stored — REQUIRED with a [PerturbedEquilibrium] section set_psilim_via_dmlim = true # Truncate at (last_rational_q + dmlim)/n — TRUE for diverted equilibria (q → ∞ at separatrix) diff --git a/examples/LAR_beta_scan/gpec.toml b/examples/LAR_beta_scan/gpec.toml index 6e5c664f6..8ac649d06 100644 --- a/examples/LAR_beta_scan/gpec.toml +++ b/examples/LAR_beta_scan/gpec.toml @@ -52,7 +52,7 @@ singfac_min = 1e-4 # Fractional distance from rational q at which id ucrit = 1e4 # Column-norm threshold that triggers solution renormalization sing_order = 6 # Order of the singular-surface (Frobenius) series expansion -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = false # Append serial-EL pass for dense ξ; not needed without [PerturbedEquilibrium] (default false) set_psilim_via_dmlim = false # FALSE for limited/analytical equilibria — rationals sparse, dmlim would chop too much edge diff --git a/examples/LAR_beta_scan/run_scan.jl b/examples/LAR_beta_scan/run_scan.jl index 13e8c40cf..95a924a62 100644 --- a/examples/LAR_beta_scan/run_scan.jl +++ b/examples/LAR_beta_scan/run_scan.jl @@ -72,12 +72,13 @@ end function extract_results(h5_path::String) h5open(h5_path, "r") do f - ep = read(f, "vacuum/ep"); ev = read(f, "vacuum/ev"); et = read(f, "vacuum/et") - msing = read(f, "singular/msing") - m_sing = read(f, "singular/m") - dp_mat = haskey(f, "singular/delta_prime_matrix") ? read(f, "singular/delta_prime_matrix") : nothing - qlim = haskey(f, "info/qlim") ? read(f, "info/qlim") : read(f, "equil/qmax") - q0 = read(f, "equil/q0"); qmax = read(f, "equil/qmax") + fbs = "ForceFreeStates/FreeBoundaryStability" + ep = read(f, "$fbs/eigenmode_plasma_energies"); ev = read(f, "$fbs/eigenmode_vacuum_energies"); et = read(f, "$fbs/eigenmode_energies") + msing = read(f, "SingularSurfaces/msing") + m_sing = read(f, "SingularSurfaces/m") + dp_mat = haskey(f, "SingularSurfaces/delta_prime_matrix") ? read(f, "SingularSurfaces/delta_prime_matrix") : nothing + qlim = haskey(f, "Info/qlim") ? read(f, "Info/qlim") : read(f, "Equilibrium/qmax") + q0 = read(f, "Equilibrium/q0"); qmax = read(f, "Equilibrium/qmax") dp_21 = NaN + NaN*im; dp_31 = NaN + NaN*im if dp_mat !== nothing && msing > 0 diff --git a/examples/LAR_epsilon_scan/gpec.toml b/examples/LAR_epsilon_scan/gpec.toml index b5cc871ee..f0d174ecb 100644 --- a/examples/LAR_epsilon_scan/gpec.toml +++ b/examples/LAR_epsilon_scan/gpec.toml @@ -53,7 +53,7 @@ singfac_min = 1e-4 # Fractional distance from rational q at which id ucrit = 1e4 # Column-norm threshold that triggers solution renormalization sing_order = 6 # Order of the singular-surface (Frobenius) series expansion -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = false # Append serial-EL pass for dense ξ; not needed without [PerturbedEquilibrium] (default false) set_psilim_via_dmlim = false # FALSE for limited/analytical equilibria — rationals sparse, dmlim would chop too much edge diff --git a/examples/LAR_epsilon_scan/run_scan.jl b/examples/LAR_epsilon_scan/run_scan.jl index 643b71194..73f52b7c4 100644 --- a/examples/LAR_epsilon_scan/run_scan.jl +++ b/examples/LAR_epsilon_scan/run_scan.jl @@ -79,12 +79,13 @@ end function extract_results(h5_path::String) h5open(h5_path, "r") do f - ep = read(f, "vacuum/ep"); ev = read(f, "vacuum/ev"); et = read(f, "vacuum/et") - msing = read(f, "singular/msing") - m_sing = read(f, "singular/m") - dp_mat = haskey(f, "singular/delta_prime_matrix") ? read(f, "singular/delta_prime_matrix") : nothing - qlim = haskey(f, "info/qlim") ? read(f, "info/qlim") : read(f, "equil/qmax") - q0 = read(f, "equil/q0"); qmax = read(f, "equil/qmax") + fbs = "ForceFreeStates/FreeBoundaryStability" + ep = read(f, "$fbs/eigenmode_plasma_energies"); ev = read(f, "$fbs/eigenmode_vacuum_energies"); et = read(f, "$fbs/eigenmode_energies") + msing = read(f, "SingularSurfaces/msing") + m_sing = read(f, "SingularSurfaces/m") + dp_mat = haskey(f, "SingularSurfaces/delta_prime_matrix") ? read(f, "SingularSurfaces/delta_prime_matrix") : nothing + qlim = haskey(f, "Info/qlim") ? read(f, "Info/qlim") : read(f, "Equilibrium/qmax") + q0 = read(f, "Equilibrium/q0"); qmax = read(f, "Equilibrium/qmax") dp_21 = NaN + NaN*im; dp_31 = NaN + NaN*im if dp_mat !== nothing && msing > 0 diff --git a/examples/LAR_ideal_match_test/gpec.toml b/examples/LAR_ideal_match_test/gpec.toml index 07f67030f..d150cbac2 100644 --- a/examples/LAR_ideal_match_test/gpec.toml +++ b/examples/LAR_ideal_match_test/gpec.toml @@ -50,7 +50,7 @@ ucrit = 1e4 # Maximum fraction of solutions allowed before r sing_order = 6 # Power-series order for the singular-surface asymptotics save_interval = 3 # Save every Nth ODE step (1=all, 10=every 10th). Always saves near rational surfaces. -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 is about +20% speedup) populate_dense_xi = false # Dense axis-basis xi for the FFS HDF5 output. Not needed here: the gal-matched path builds its own dense xi. set_psilim_via_dmlim = false # Keep psilim at psihigh (do not truncate at last_rational_q + dmlim) diff --git a/examples/LAR_resistive_match_test/gpec.toml b/examples/LAR_resistive_match_test/gpec.toml index 7fb9f565e..9900ad8e7 100644 --- a/examples/LAR_resistive_match_test/gpec.toml +++ b/examples/LAR_resistive_match_test/gpec.toml @@ -51,7 +51,7 @@ ucrit = 1e4 # Maximum fraction of solutions allowed before r sing_order = 6 # Power-series order for the singular-surface asymptotics save_interval = 3 # Save every Nth ODE step (1=all, 10=every 10th). Always saves near rational surfaces. -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 is about +20% speedup) populate_dense_xi = false # Dense axis-basis xi for the FFS HDF5 output. Not needed here: the gal-matched path builds its own dense xi. set_psilim_via_dmlim = false # Keep psilim at psihigh (do not truncate at last_rational_q + dmlim) diff --git a/examples/Solovev_ideal_example/gpec.toml b/examples/Solovev_ideal_example/gpec.toml index 2ed0654c6..66ba0d48b 100644 --- a/examples/Solovev_ideal_example/gpec.toml +++ b/examples/Solovev_ideal_example/gpec.toml @@ -72,7 +72,7 @@ ucrit = 1e3 # Column-norm threshold that triggers solution ren save_interval = 3 # Save every Nth ODE step (1=all). Always saves near rational surfaces. # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = true # Append serial-EL pass so dense ξ is stored — REQUIRED with a [PerturbedEquilibrium] section set_psilim_via_dmlim = false # FALSE for limited/analytical equilibria — rationals sparse, dmlim would chop too much edge diff --git a/examples/Solovev_ideal_example_3D/gpec.toml b/examples/Solovev_ideal_example_3D/gpec.toml index 991cfc4dc..70123040b 100644 --- a/examples/Solovev_ideal_example_3D/gpec.toml +++ b/examples/Solovev_ideal_example_3D/gpec.toml @@ -38,7 +38,7 @@ ucrit = 1e3 # Column-norm threshold that triggers solution ren save_interval = 3 # Save every Nth ODE step (1=all). Always saves near rational surfaces. # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = false # Append serial-EL pass for dense ξ; not needed without [PerturbedEquilibrium] (default false) set_psilim_via_dmlim = false # FALSE for limited/analytical equilibria — rationals sparse, dmlim would chop too much edge diff --git a/examples/Solovev_kinetic_NTV_example/gpec.toml b/examples/Solovev_kinetic_NTV_example/gpec.toml index f1d3ee96c..c5fb3b3ba 100644 --- a/examples/Solovev_kinetic_NTV_example/gpec.toml +++ b/examples/Solovev_kinetic_NTV_example/gpec.toml @@ -70,7 +70,7 @@ force_wv_symmetry = true # Enforce symmetry of the vacuum energy matrix save_interval = 3 # Save every Nth ODE step (1=all). Always saves near rational surfaces. # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = true # Append serial-EL pass so dense ξ is stored — REQUIRED with a [PerturbedEquilibrium] section set_psilim_via_dmlim = false # FALSE for limited/analytical equilibria — rationals sparse, dmlim would chop too much edge diff --git a/regression-harness/cases/diiid_n1.toml b/regression-harness/cases/diiid_n1.toml index 0e0336a99..5827b6942 100644 --- a/regression-harness/cases/diiid_n1.toml +++ b/regression-harness/cases/diiid_n1.toml @@ -10,7 +10,7 @@ example_dir = "examples/DIIID-like_ideal_example" # Energies — leading generalized (W,N) pencil eigenvalues at the final truncation (psilim): power-normalized (⟨|ξ|²⟩ = 1 metric) and invariant to the working-coordinate Jacobian. [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "total energy Re(et[1])" @@ -18,7 +18,7 @@ noise_threshold = 1e-10 order = 10 [quantities.et_imag] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "imag_first" label = "total energy Im(et[1])" @@ -26,7 +26,7 @@ noise_threshold = 1e-10 order = 11 [quantities.ep_real] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "real_first" label = "plasma energy Re(ep[1])" @@ -34,7 +34,7 @@ noise_threshold = 1e-10 order = 12 [quantities.ev_real] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "real_first" label = "vacuum energy Re(ev[1])" @@ -42,7 +42,7 @@ noise_threshold = 1e-10 order = 13 [quantities.vacuum_eigenvalue] -h5path = "FreeBoundaryStability/vacuum_eigenvalue" +h5path = "ForceFreeStates/FreeBoundaryStability/vacuum_eigenvalue" type = "real_scalar" extract = "value" label = "vacuum matrix min eigenvalue" @@ -51,7 +51,7 @@ order = 14 # Energies — full eigenvalue arrays [quantities.ep_all] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "all_complex" label = "plasma energy (all)" @@ -59,7 +59,7 @@ noise_threshold = 1e-10 order = 20 [quantities.ev_all] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "all_complex" label = "vacuum energy (all)" @@ -67,7 +67,7 @@ noise_threshold = 1e-10 order = 21 [quantities.et_all] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "all_complex" label = "total energy (all)" @@ -76,7 +76,7 @@ order = 22 # Integration [quantities.nstep] -h5path = "integration/nstep" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep" type = "int_scalar" extract = "value" label = "ODE steps (saved)" @@ -84,7 +84,7 @@ noise_threshold = 0 order = 30 [quantities.nstep_total] -h5path = "integration/nstep_total" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep_total" type = "int_scalar" extract = "value" label = "ODE steps (total)" @@ -93,7 +93,7 @@ order = 31 # Equilibrium [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" @@ -101,7 +101,7 @@ noise_threshold = 1e-12 order = 40 [quantities.q95] -h5path = "equil/q95" +h5path = "Equilibrium/q95" type = "real_scalar" extract = "value" label = "q95" @@ -109,7 +109,7 @@ noise_threshold = 1e-12 order = 41 [quantities.betat] -h5path = "equil/betat" +h5path = "Equilibrium/betat" type = "real_scalar" extract = "value" label = "beta_t" @@ -117,7 +117,7 @@ noise_threshold = 1e-12 order = 42 [quantities.betan] -h5path = "equil/betan" +h5path = "Equilibrium/betan" type = "real_scalar" extract = "value" label = "beta_n" @@ -125,7 +125,7 @@ noise_threshold = 1e-12 order = 43 [quantities.li1] -h5path = "equil/li1" +h5path = "Equilibrium/li1" type = "real_scalar" extract = "value" label = "internal inductance li1" @@ -133,7 +133,7 @@ noise_threshold = 1e-12 order = 44 [quantities.li2] -h5path = "equil/li2" +h5path = "Equilibrium/li2" type = "real_scalar" extract = "value" label = "internal inductance li2" @@ -141,7 +141,7 @@ noise_threshold = 1e-12 order = 45 [quantities.li3] -h5path = "equil/li3" +h5path = "Equilibrium/li3" type = "real_scalar" extract = "value" label = "internal inductance li3" @@ -149,7 +149,7 @@ noise_threshold = 1e-12 order = 46 [quantities.betap1] -h5path = "equil/betap1" +h5path = "Equilibrium/betap1" type = "real_scalar" extract = "value" label = "poloidal beta betap1" @@ -157,7 +157,7 @@ noise_threshold = 1e-12 order = 47 [quantities.betap2] -h5path = "equil/betap2" +h5path = "Equilibrium/betap2" type = "real_scalar" extract = "value" label = "poloidal beta betap2" @@ -165,7 +165,7 @@ noise_threshold = 1e-12 order = 48 [quantities.betap3] -h5path = "equil/betap3" +h5path = "Equilibrium/betap3" type = "real_scalar" extract = "value" label = "poloidal beta betap3" @@ -173,7 +173,7 @@ noise_threshold = 1e-12 order = 49 [quantities.betaj] -h5path = "equil/betaj" +h5path = "Equilibrium/betaj" type = "real_scalar" extract = "value" label = "current beta betaj" @@ -181,7 +181,7 @@ noise_threshold = 1e-12 order = 57 [quantities.volume] -h5path = "equil/volume" +h5path = "Equilibrium/volume" type = "real_scalar" extract = "value" label = "plasma volume" @@ -189,7 +189,7 @@ noise_threshold = 1e-12 order = 58 [quantities.crnt] -h5path = "equil/crnt" +h5path = "Equilibrium/crnt" type = "real_scalar" extract = "value" label = "plasma current" @@ -197,7 +197,7 @@ noise_threshold = 1e-12 order = 59 [quantities.bt0] -h5path = "equil/bt0" +h5path = "Equilibrium/bt0" type = "real_scalar" extract = "value" label = "toroidal field bt0" @@ -205,7 +205,7 @@ noise_threshold = 1e-12 order = 62 [quantities.bwall] -h5path = "equil/bwall" +h5path = "Equilibrium/bwall" type = "real_scalar" extract = "value" label = "wall field bwall" @@ -213,7 +213,7 @@ noise_threshold = 1e-12 order = 63 [quantities.aratio] -h5path = "equil/aratio" +h5path = "Equilibrium/aratio" type = "real_scalar" extract = "value" label = "aspect ratio" @@ -221,7 +221,7 @@ noise_threshold = 1e-12 order = 64 [quantities.kappa] -h5path = "equil/kappa" +h5path = "Equilibrium/kappa" type = "real_scalar" extract = "value" label = "elongation kappa" @@ -230,7 +230,7 @@ order = 65 # Singular surfaces [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" @@ -238,7 +238,7 @@ noise_threshold = 0 order = 50 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" @@ -246,7 +246,7 @@ noise_threshold = 1e-8 order = 51 [quantities.sing_q] -h5path = "singular/q" +h5path = "SingularSurfaces/q" type = "real_vector" extract = "all_real" label = "singular q values" @@ -255,7 +255,7 @@ order = 52 # Mode numbers [quantities.mpert] -h5path = "info/mpert" +h5path = "Info/mpert" type = "int_scalar" extract = "value" label = "mpert" @@ -263,7 +263,7 @@ noise_threshold = 0 order = 60 [quantities.npert] -h5path = "info/npert" +h5path = "Info/npert" type = "int_scalar" extract = "value" label = "npert" @@ -276,7 +276,7 @@ order = 61 # Per-surface Δ' is now de-emphasized — see PR 178 notes — and SingularCoupling # instead reads this BVP matrix diagonal. [quantities.delta_prime] -h5path = "singular/delta_prime_matrix" +h5path = "SingularSurfaces/delta_prime_matrix" type = "complex_matrix" extract = "diagonal_complex" label = "delta prime (BVP diagonal)" @@ -284,7 +284,7 @@ noise_threshold = 1e-8 order = 80 [quantities.island_half_width] -h5path = "perturbed_equilibrium/singular_coupling/island_half_width" +h5path = "PerturbedEquilibrium/SingularCoupling/island_half_width" type = "real_vector" extract = "all_real" label = "island half-widths" @@ -292,7 +292,7 @@ noise_threshold = 1e-8 order = 81 [quantities.chirikov_parameter] -h5path = "perturbed_equilibrium/singular_coupling/chirikov_parameter" +h5path = "PerturbedEquilibrium/SingularCoupling/chirikov_parameter" type = "real_vector" extract = "all_real" label = "Chirikov parameter" @@ -300,7 +300,7 @@ noise_threshold = 1e-8 order = 82 [quantities.resonant_area_weighted_field] -h5path = "perturbed_equilibrium/singular_coupling/resonant_area_weighted_field" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_area_weighted_field" type = "complex_vector" extract = "norm" label = "||resonant area-weighted field||" @@ -308,7 +308,7 @@ noise_threshold = 1e-8 order = 83 [quantities.resonant_area_weighted_field_all] -h5path = "perturbed_equilibrium/singular_coupling/resonant_area_weighted_field" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_area_weighted_field" type = "complex_vector" extract = "all_complex" label = "resonant area-weighted field b^r" @@ -316,7 +316,7 @@ noise_threshold = 1e-8 # Perturbed equilibrium: energies [quantities.pe_plasma_energy] -h5path = "perturbed_equilibrium/energies/plasma_energy" +h5path = "PerturbedEquilibrium/Energies/plasma_energy" type = "real_scalar" extract = "value" label = "PE plasma energy" @@ -324,7 +324,7 @@ noise_threshold = 1e-10 order = 91 [quantities.pe_vacuum_energy] -h5path = "perturbed_equilibrium/energies/vacuum_energy" +h5path = "PerturbedEquilibrium/Energies/vacuum_energy" type = "real_scalar" extract = "value" label = "PE vacuum energy" @@ -332,7 +332,7 @@ noise_threshold = 1e-10 order = 92 [quantities.pe_surface_energy] -h5path = "perturbed_equilibrium/energies/surface_energy" +h5path = "PerturbedEquilibrium/Energies/surface_energy" type = "real_scalar" extract = "value" label = "PE surface energy" @@ -340,7 +340,7 @@ noise_threshold = 1e-10 order = 93 [quantities.pe_toroidal_torque] -h5path = "perturbed_equilibrium/energies/toroidal_torque" +h5path = "PerturbedEquilibrium/Energies/toroidal_torque" type = "real_scalar" extract = "value" label = "PE toroidal torque" @@ -353,7 +353,7 @@ order = 94 # section. Threshold is a noise floor for the adaptive-quadrature result; tune if # run-to-run jitter appears. [quantities.ntv_torque_fgar] -h5path = "kinetic_forces/fgar/total_torque" +h5path = "KineticForces/fgar/total_torque" type = "real_scalar" extract = "value" label = "NTV torque FGAR [N·m]" @@ -364,7 +364,7 @@ order = 95 # Tracked at a tighter threshold — it isolates the energy component the # bounce-averaging chain moves most. [quantities.ntv_energy_fgar] -h5path = "kinetic_forces/fgar/total_energy" +h5path = "KineticForces/fgar/total_energy" type = "real_scalar" extract = "value" label = "NTV kinetic energy dW FGAR [J]" @@ -373,7 +373,7 @@ order = 96 # Profiles (checksums) [quantities.q_profile] -h5path = "splines/profiles/q" +h5path = "Equilibrium/Profiles/q" type = "real_vector" extract = "checksum" label = "q profile (checksum)" @@ -381,7 +381,7 @@ noise_threshold = 0 order = 70 [quantities.pressure_profile] -h5path = "splines/profiles/mu0p" +h5path = "Equilibrium/Profiles/mu0p" type = "real_vector" extract = "checksum" label = "pressure profile (checksum)" @@ -391,7 +391,7 @@ order = 71 # Local stability profiles (local_stability_flag = true). Checksums hash the full # profile so they are robust to NaN edge-surface entries and catch any change. [quantities.locstab_di] -h5path = "locstab/di" +h5path = "LocalStability/di" type = "real_vector" extract = "checksum" label = "Mercier D_I profile (checksum)" @@ -399,7 +399,7 @@ noise_threshold = 0 order = 72 [quantities.locstab_dr] -h5path = "locstab/dr" +h5path = "LocalStability/dr" type = "real_vector" extract = "checksum" label = "resistive interchange D_R profile (checksum)" @@ -407,7 +407,7 @@ noise_threshold = 0 order = 73 [quantities.locstab_ballooning_dp] -h5path = "locstab/ballooning_Delta_prime" +h5path = "LocalStability/ballooning_Delta_prime" type = "real_vector" extract = "checksum" label = "ballooning Delta' profile (checksum)" diff --git a/regression-harness/cases/diiid_slayer_n1.toml b/regression-harness/cases/diiid_slayer_n1.toml index 13167efe5..e6d4e2749 100644 --- a/regression-harness/cases/diiid_slayer_n1.toml +++ b/regression-harness/cases/diiid_slayer_n1.toml @@ -7,7 +7,7 @@ example_dir = "examples/DIIID-like_SLAYER_example" # Per-surface SLAYER layer parameters (geometry + dimensionless) # --------------------------------------------------------------------- [quantities.slayer_ising] -h5path = "slayer/per_surface/ising" +h5path = "Tearing/PerSurface/ising" type = "real_vector" extract = "all_real" label = "SLAYER surface indices" @@ -15,7 +15,7 @@ noise_threshold = 0 order = 10 [quantities.slayer_m] -h5path = "slayer/per_surface/m" +h5path = "Tearing/PerSurface/m" type = "real_vector" extract = "all_real" label = "SLAYER poloidal m" @@ -23,7 +23,7 @@ noise_threshold = 0 order = 11 [quantities.slayer_n] -h5path = "slayer/per_surface/n" +h5path = "Tearing/PerSurface/n" type = "real_vector" extract = "all_real" label = "SLAYER toroidal n" @@ -31,7 +31,7 @@ noise_threshold = 0 order = 12 [quantities.slayer_rs] -h5path = "slayer/per_surface/rs" +h5path = "Tearing/PerSurface/rs" type = "real_vector" extract = "all_real" label = "SLAYER minor radius rs" @@ -39,7 +39,7 @@ noise_threshold = 1e-10 order = 13 [quantities.slayer_sval_r] -h5path = "slayer/per_surface/sval_r" +h5path = "Tearing/PerSurface/sval_r" type = "real_vector" extract = "all_real" label = "SLAYER r-based shear" @@ -47,7 +47,7 @@ noise_threshold = 1e-10 order = 14 [quantities.slayer_lu] -h5path = "slayer/per_surface/lu" +h5path = "Tearing/PerSurface/lu" type = "real_vector" extract = "all_real" label = "SLAYER Lundquist S" @@ -55,7 +55,7 @@ noise_threshold = 1e-8 order = 15 [quantities.slayer_D_norm] -h5path = "slayer/per_surface/D_norm" +h5path = "Tearing/PerSurface/D_norm" type = "real_vector" extract = "all_real" label = "SLAYER D_norm" @@ -63,7 +63,7 @@ noise_threshold = 1e-10 order = 16 [quantities.slayer_P_perp] -h5path = "slayer/per_surface/P_perp" +h5path = "Tearing/PerSurface/P_perp" type = "real_vector" extract = "all_real" label = "SLAYER P_perp" @@ -71,7 +71,7 @@ noise_threshold = 1e-8 order = 17 [quantities.slayer_tauk] -h5path = "slayer/per_surface/tauk" +h5path = "Tearing/PerSurface/tauk" type = "real_vector" extract = "all_real" label = "SLAYER tauk" @@ -79,7 +79,7 @@ noise_threshold = 1e-12 order = 18 [quantities.slayer_iota_e] -h5path = "slayer/per_surface/iota_e" +h5path = "Tearing/PerSurface/iota_e" type = "real_vector" extract = "all_real" label = "SLAYER iota_e" @@ -98,7 +98,7 @@ order = 19 # edge), so those are deliberately not golden-tracked. # --------------------------------------------------------------------- [quantities.slayer_Q_re] -h5path = "slayer/roots/Q_root_real" +h5path = "Tearing/Roots/Q_root_real" type = "real_vector" extract = "first_3" label = "SLAYER Re(Q_root) [2/1,3/1,4/1]" @@ -106,7 +106,7 @@ noise_threshold = 1e-4 order = 30 [quantities.slayer_Q_im] -h5path = "slayer/roots/Q_root_imag" +h5path = "Tearing/Roots/Q_root_imag" type = "real_vector" extract = "first_3" label = "SLAYER Im(Q_root) [2/1,3/1,4/1]" @@ -114,7 +114,7 @@ noise_threshold = 1e-4 order = 31 [quantities.slayer_omega_Hz] -h5path = "slayer/roots/omega_Hz" +h5path = "Tearing/Roots/omega_Hz" type = "real_vector" extract = "first_3" label = "SLAYER ω_Hz [2/1,3/1,4/1]" @@ -122,7 +122,7 @@ noise_threshold = 1.0 order = 32 [quantities.slayer_gamma_Hz] -h5path = "slayer/roots/gamma_Hz" +h5path = "Tearing/Roots/gamma_Hz" type = "real_vector" extract = "first_3" label = "SLAYER γ_Hz [2/1,3/1,4/1]" @@ -132,7 +132,7 @@ order = 33 # no_root flag (1 = extraction failed). Pinned for the inner three surfaces; # this case guards that the 2/1, 3/1, 4/1 keep finding a real root. [quantities.slayer_no_root] -h5path = "slayer/roots/no_root" +h5path = "Tearing/Roots/no_root" type = "real_vector" extract = "first_3" label = "SLAYER no_root flags [2/1,3/1,4/1]" @@ -143,7 +143,7 @@ order = 34 # Settings (catches accidental config drift) # --------------------------------------------------------------------- [quantities.slayer_enabled] -h5path = "slayer/enabled" +h5path = "Tearing/enabled" type = "int_scalar" extract = "value" label = "SLAYER enabled flag" diff --git a/regression-harness/cases/efit_fixedbdy_separatrix.toml b/regression-harness/cases/efit_fixedbdy_separatrix.toml index 01688e5ee..6f5d023bf 100644 --- a/regression-harness/cases/efit_fixedbdy_separatrix.toml +++ b/regression-harness/cases/efit_fixedbdy_separatrix.toml @@ -13,7 +13,7 @@ description = "Fixed-boundary EFIT (box hugs LCFS), Newton separatrix-find regre kind = "computed" [quantities.psio] -h5path = "equil/psio" +h5path = "Equilibrium/psio" type = "real_scalar" extract = "value" label = "psio (total poloidal flux)" @@ -21,7 +21,7 @@ noise_threshold = 1e-12 order = 10 [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" @@ -29,7 +29,7 @@ noise_threshold = 1e-12 order = 11 [quantities.q95] -h5path = "equil/q95" +h5path = "Equilibrium/q95" type = "real_scalar" extract = "value" label = "q95" @@ -37,7 +37,7 @@ noise_threshold = 1e-12 order = 12 [quantities.betat] -h5path = "equil/betat" +h5path = "Equilibrium/betat" type = "real_scalar" extract = "value" label = "beta_t" @@ -45,7 +45,7 @@ noise_threshold = 1e-12 order = 13 [quantities.betan] -h5path = "equil/betan" +h5path = "Equilibrium/betan" type = "real_scalar" extract = "value" label = "beta_n" diff --git a/regression-harness/cases/gal_resistive_diiid.toml b/regression-harness/cases/gal_resistive_diiid.toml index 9ec61109e..5990baad4 100644 --- a/regression-harness/cases/gal_resistive_diiid.toml +++ b/regression-harness/cases/gal_resistive_diiid.toml @@ -5,7 +5,7 @@ example_dir = "examples/DIIID-like_gal_resistive_example" # Number of resonant surfaces in the gal domain [quantities.gal_msing] -h5path = "galerkin/msing" +h5path = "ForceFreeStates/Solutions/GalerkinIntegration/msing" type = "int_scalar" extract = "value" label = "gal # singular surfaces" @@ -13,7 +13,7 @@ noise_threshold = 0 order = 10 [quantities.gal_sing_q] -h5path = "galerkin/sing_q" +h5path = "SingularSurfaces/GalerkinDeltaPrime/sing_q" type = "real_vector" extract = "all_real" label = "gal singular q values" @@ -22,7 +22,7 @@ order = 11 # PEST-3 Δ matching matrix — per-surface diagonal (the physics-meaningful tearing Δ′) [quantities.gal_pest3_delta_diag] -h5path = "galerkin/pest3_Delta" +h5path = "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta" type = "complex_matrix" extract = "diagonal_complex" label = "gal PEST3 Δ diagonal" @@ -31,7 +31,7 @@ order = 20 # Full outer Δ′ matrix (nsol × 2·msing) — Frobenius norm catches any element drift [quantities.gal_delta_norm] -h5path = "galerkin/delta" +h5path = "SingularSurfaces/GalerkinDeltaPrime/delta" type = "complex_matrix" extract = "norm" label = "||gal Δ′ matrix||" @@ -40,7 +40,7 @@ order = 21 # rpec coil-response block (mpert × 2·msing) — Frobenius norm [quantities.gal_delta_coil_norm] -h5path = "galerkin/delta_coil" +h5path = "SingularSurfaces/GalerkinDeltaPrime/delta_coil" type = "complex_matrix" extract = "norm" label = "||gal Δ_coil block||" @@ -49,7 +49,7 @@ order = 22 # Mercier index per surface [quantities.gal_di] -h5path = "galerkin/di" +h5path = "SingularSurfaces/GalerkinDeltaPrime/di" type = "real_vector" extract = "all_real" label = "gal D_I per surface" @@ -58,7 +58,7 @@ order = 30 # Resonant exponents α per surface [quantities.gal_alpha] -h5path = "galerkin/alpha" +h5path = "SingularSurfaces/GalerkinDeltaPrime/alpha" type = "complex_vector" extract = "all_complex" label = "gal α per surface" @@ -73,7 +73,7 @@ order = 31 # Inner-layer matching data Δ(Q) per surface (resist_eval geometry + GGJ inner solver). (msing × 2) [quantities.gal_match_deltar_norm] -h5path = "galerkin/match/deltar" +h5path = "ForceFreeStates/Solutions/GalerkinIntegration/Match/deltar" type = "complex_matrix" extract = "norm" label = "||gal inner-layer Δ||" @@ -82,7 +82,7 @@ order = 41 # Outer-region matched coefficients cout (2·msing × mcoil) — the 4·msing matching assembly + solve. [quantities.gal_match_cout_norm] -h5path = "galerkin/match/cout" +h5path = "ForceFreeStates/Solutions/GalerkinIntegration/Match/cout" type = "complex_matrix" extract = "norm" label = "||gal match cout||" @@ -91,7 +91,7 @@ order = 42 # Matching linear-solve residual ‖mat·cof − rmat‖/‖rmat‖ — health check (should stay ~machine eps). [quantities.gal_match_residual] -h5path = "galerkin/match/residual" +h5path = "ForceFreeStates/Solutions/GalerkinIntegration/Match/residual" type = "real_scalar" extract = "value" label = "gal match residual" diff --git a/regression-harness/cases/gal_resistive_pe.toml b/regression-harness/cases/gal_resistive_pe.toml index 7a26d7563..1031a9673 100644 --- a/regression-harness/cases/gal_resistive_pe.toml +++ b/regression-harness/cases/gal_resistive_pe.toml @@ -9,7 +9,7 @@ example_dir = "examples/DIIID-like_gal_resistive_pe_example" # Surface locations / count (sanity) [quantities.pe_rational_q] -h5path = "perturbed_equilibrium/singular_coupling/rational_q" +h5path = "PerturbedEquilibrium/SingularCoupling/rational_q" type = "real_vector" extract = "all_real" label = "rational q values" @@ -18,7 +18,7 @@ order = 10 # Island half-widths per rational surface — the headline driven quantity [quantities.pe_island_half_width] -h5path = "perturbed_equilibrium/singular_coupling/island_half_width" +h5path = "PerturbedEquilibrium/SingularCoupling/island_half_width" type = "real_vector" extract = "all_real" label = "island half-widths" @@ -27,7 +27,7 @@ order = 20 # Chirikov overlap parameter per surface [quantities.pe_chirikov] -h5path = "perturbed_equilibrium/singular_coupling/chirikov_parameter" +h5path = "PerturbedEquilibrium/SingularCoupling/chirikov_parameter" type = "real_vector" extract = "all_real" label = "Chirikov parameter" @@ -36,7 +36,7 @@ order = 21 # Resonant flux Φ_res per surface (applied) — norm + per-surface complex values [quantities.pe_resonant_flux_norm] -h5path = "perturbed_equilibrium/singular_coupling/resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_flux" type = "complex_vector" extract = "norm" label = "||resonant flux||" @@ -44,7 +44,7 @@ noise_threshold = 1e-6 order = 30 [quantities.pe_resonant_flux_all] -h5path = "perturbed_equilibrium/singular_coupling/resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_flux" type = "complex_vector" extract = "all_complex" label = "resonant flux Phi_res" @@ -53,7 +53,7 @@ order = 31 # Penetrated (reconnected) resonant field per surface — the resistive shielding signature [quantities.pe_penetrated_field] -h5path = "perturbed_equilibrium/singular_coupling/penetrated_field" +h5path = "PerturbedEquilibrium/SingularCoupling/penetrated_field" type = "complex_vector" extract = "all_complex" label = "penetrated field" @@ -62,7 +62,7 @@ order = 32 # Per-surface Δ' from the PE singular-coupling analysis [quantities.pe_delta_prime] -h5path = "perturbed_equilibrium/singular_coupling/delta_prime" +h5path = "PerturbedEquilibrium/SingularCoupling/delta_prime" type = "complex_vector" extract = "all_complex" label = "PE Δ' per surface" @@ -71,7 +71,7 @@ order = 33 # Resonant-flux coupling matrix (n_rational × numpert_total) — Frobenius norm, catches broader drift [quantities.pe_C_resonant_flux_norm] -h5path = "perturbed_equilibrium/singular_coupling/C_resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/C_resonant_flux" type = "complex_matrix" extract = "norm" label = "||C resonant flux||" diff --git a/regression-harness/cases/solovev_kinetic_calculated.toml b/regression-harness/cases/solovev_kinetic_calculated.toml index e5ebb29ef..3f10c8599 100644 --- a/regression-harness/cases/solovev_kinetic_calculated.toml +++ b/regression-harness/cases/solovev_kinetic_calculated.toml @@ -9,28 +9,28 @@ example_dir = "examples/Solovev_kinetic_calculated_example" # Energies — leading eigenvalues [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "total energy Re(et[1])" noise_threshold = 1e-10 [quantities.et_imag] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "imag_first" label = "total energy Im(et[1])" noise_threshold = 1e-10 [quantities.ep_real] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "real_first" label = "plasma energy Re(ep[1])" noise_threshold = 1e-10 [quantities.ev_real] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "real_first" label = "vacuum energy Re(ev[1])" @@ -38,7 +38,7 @@ noise_threshold = 1e-10 # Energies — full eigenvalue arrays [quantities.et_all] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "all_complex" label = "total energy (all)" @@ -46,14 +46,14 @@ noise_threshold = 1e-10 # Integration [quantities.nstep] -h5path = "integration/nstep" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep" type = "int_scalar" extract = "value" label = "ODE steps (saved)" noise_threshold = 0 [quantities.nstep_total] -h5path = "integration/nstep_total" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep_total" type = "int_scalar" extract = "value" label = "ODE steps (total)" @@ -61,14 +61,14 @@ noise_threshold = 0 # Equilibrium [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" noise_threshold = 1e-12 [quantities.q95] -h5path = "equil/q95" +h5path = "Equilibrium/q95" type = "real_scalar" extract = "value" label = "q95" @@ -76,44 +76,36 @@ noise_threshold = 1e-12 # Singular surfaces [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" noise_threshold = 0 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" noise_threshold = 1e-8 [quantities.sing_q] -h5path = "singular/q" +h5path = "SingularSurfaces/q" type = "real_vector" extract = "all_real" label = "singular q values" noise_threshold = 1e-8 -# Kinetic-specific -[quantities.kinetic_factor] -h5path = "kinetic/kinetic_factor" -type = "real_scalar" -extract = "value" -label = "kinetic_factor" -noise_threshold = 0 - # Mode numbers [quantities.mpert] -h5path = "info/mpert" +h5path = "Info/mpert" type = "int_scalar" extract = "value" label = "mpert" noise_threshold = 0 [quantities.npert] -h5path = "info/npert" +h5path = "Info/npert" type = "int_scalar" extract = "value" label = "npert" diff --git a/regression-harness/cases/solovev_kinetic_ntv.toml b/regression-harness/cases/solovev_kinetic_ntv.toml index 18f5a8144..cfdac6492 100644 --- a/regression-harness/cases/solovev_kinetic_ntv.toml +++ b/regression-harness/cases/solovev_kinetic_ntv.toml @@ -12,7 +12,7 @@ example_dir = "examples/Solovev_kinetic_NTV_example" # converges to rtol_psi=1e-2, but the result is deterministic for fixed code + inputs, so # the noise floor is set near FP noise and any real movement is surfaced. [quantities.ntv_torque] -h5path = "kinetic_forces/fgar/total_torque" +h5path = "KineticForces/fgar/total_torque" type = "real_vector" extract = "all_real" label = "NTV torque fgar [Re, Im]" @@ -20,7 +20,7 @@ noise_threshold = 1e-8 order = 10 [quantities.ntv_psi_nsteps] -h5path = "kinetic_forces/fgar/psi_nsteps" +h5path = "KineticForces/fgar/psi_nsteps" type = "int_scalar" extract = "value" label = "NTV ψ quadrature evaluations" @@ -29,7 +29,7 @@ order = 11 # Stability anchors — confirm the upstream FFS/PE stages feeding the NTV diagnostic. [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "root-area-weighted total energy Re(et[1])" @@ -37,7 +37,7 @@ noise_threshold = 1e-10 order = 20 [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" @@ -45,7 +45,7 @@ noise_threshold = 0 order = 21 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" @@ -53,7 +53,7 @@ noise_threshold = 1e-8 order = 22 [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" diff --git a/regression-harness/cases/solovev_kinetic_nuzero.toml b/regression-harness/cases/solovev_kinetic_nuzero.toml index e51efe873..26d671329 100644 --- a/regression-harness/cases/solovev_kinetic_nuzero.toml +++ b/regression-harness/cases/solovev_kinetic_nuzero.toml @@ -9,28 +9,28 @@ example_dir = "examples/Solovev_kinetic_calculated_example" # Energies — leading eigenvalues [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "total energy Re(et[1])" noise_threshold = 1e-10 [quantities.et_imag] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "imag_first" label = "total energy Im(et[1])" noise_threshold = 1e-10 [quantities.ep_real] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "real_first" label = "plasma energy Re(ep[1])" noise_threshold = 1e-10 [quantities.ev_real] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "real_first" label = "vacuum energy Re(ev[1])" @@ -38,7 +38,7 @@ noise_threshold = 1e-10 # Energies — full eigenvalue arrays [quantities.et_all] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "all_complex" label = "total energy (all)" @@ -46,14 +46,14 @@ noise_threshold = 1e-10 # Integration [quantities.nstep] -h5path = "integration/nstep" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep" type = "int_scalar" extract = "value" label = "ODE steps (saved)" noise_threshold = 0 [quantities.nstep_total] -h5path = "integration/nstep_total" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep_total" type = "int_scalar" extract = "value" label = "ODE steps (total)" @@ -61,14 +61,14 @@ noise_threshold = 0 # Equilibrium [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" noise_threshold = 1e-12 [quantities.q95] -h5path = "equil/q95" +h5path = "Equilibrium/q95" type = "real_scalar" extract = "value" label = "q95" @@ -76,44 +76,36 @@ noise_threshold = 1e-12 # Singular surfaces [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" noise_threshold = 0 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" noise_threshold = 1e-8 [quantities.sing_q] -h5path = "singular/q" +h5path = "SingularSurfaces/q" type = "real_vector" extract = "all_real" label = "singular q values" noise_threshold = 1e-8 -# Kinetic-specific -[quantities.kinetic_factor] -h5path = "kinetic/kinetic_factor" -type = "real_scalar" -extract = "value" -label = "kinetic_factor" -noise_threshold = 0 - # Mode numbers [quantities.mpert] -h5path = "info/mpert" +h5path = "Info/mpert" type = "int_scalar" extract = "value" label = "mpert" noise_threshold = 0 [quantities.npert] -h5path = "info/npert" +h5path = "Info/npert" type = "int_scalar" extract = "value" label = "npert" diff --git a/regression-harness/cases/solovev_multi_n.toml b/regression-harness/cases/solovev_multi_n.toml index e8cbaa21e..68aebae5c 100644 --- a/regression-harness/cases/solovev_multi_n.toml +++ b/regression-harness/cases/solovev_multi_n.toml @@ -9,7 +9,7 @@ example_dir = "examples/Solovev_ideal_example_multi_n" # Energies — leading generalized (W,N) pencil eigenvalues at the final truncation (psilim): power-normalized (⟨|ξ|²⟩ = 1 metric) and invariant to the working-coordinate Jacobian. [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "total energy Re(et[1])" @@ -17,7 +17,7 @@ noise_threshold = 1e-10 order = 10 [quantities.et_imag] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "imag_first" label = "total energy Im(et[1])" @@ -25,7 +25,7 @@ noise_threshold = 1e-10 order = 11 [quantities.vacuum_eigenvalue] -h5path = "FreeBoundaryStability/vacuum_eigenvalue" +h5path = "ForceFreeStates/FreeBoundaryStability/vacuum_eigenvalue" type = "real_scalar" extract = "value" label = "vacuum matrix min eigenvalue" @@ -34,7 +34,7 @@ order = 14 # Energies — full eigenvalue arrays [quantities.ep_all] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "all_complex" label = "plasma energy (all)" @@ -42,7 +42,7 @@ noise_threshold = 1e-10 order = 20 [quantities.ev_all] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "all_complex" label = "vacuum energy (all)" @@ -50,7 +50,7 @@ noise_threshold = 1e-10 order = 21 [quantities.et_all] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "all_complex" label = "total energy (all)" @@ -59,7 +59,7 @@ order = 22 # Integration [quantities.nstep] -h5path = "integration/nstep" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep" type = "int_scalar" extract = "value" label = "ODE steps (saved)" @@ -67,7 +67,7 @@ noise_threshold = 0 order = 30 [quantities.nstep_total] -h5path = "integration/nstep_total" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep_total" type = "int_scalar" extract = "value" label = "ODE steps (total)" @@ -76,7 +76,7 @@ order = 31 # Equilibrium [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" @@ -84,7 +84,7 @@ noise_threshold = 1e-12 order = 40 [quantities.q95] -h5path = "equil/q95" +h5path = "Equilibrium/q95" type = "real_scalar" extract = "value" label = "q95" @@ -93,7 +93,7 @@ order = 41 # Singular surfaces [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" @@ -101,7 +101,7 @@ noise_threshold = 0 order = 50 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" @@ -110,7 +110,7 @@ order = 51 # Mode numbers [quantities.mpert] -h5path = "info/mpert" +h5path = "Info/mpert" type = "int_scalar" extract = "value" label = "mpert" @@ -118,7 +118,7 @@ noise_threshold = 0 order = 60 [quantities.npert] -h5path = "info/npert" +h5path = "Info/npert" type = "int_scalar" extract = "value" label = "npert" @@ -127,7 +127,7 @@ order = 61 # Profiles (checksums) [quantities.q_profile] -h5path = "splines/profiles/q" +h5path = "Equilibrium/Profiles/q" type = "real_vector" extract = "checksum" label = "q profile (checksum)" diff --git a/regression-harness/cases/solovev_n1.toml b/regression-harness/cases/solovev_n1.toml index d6c255ef3..d6fab13e3 100644 --- a/regression-harness/cases/solovev_n1.toml +++ b/regression-harness/cases/solovev_n1.toml @@ -9,7 +9,7 @@ example_dir = "examples/Solovev_ideal_example" # Energies — leading generalized (W,N) pencil eigenvalues at the final truncation (psilim): power-normalized (⟨|ξ|²⟩ = 1 metric) and invariant to the working-coordinate Jacobian. [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "total energy Re(et[1])" @@ -17,7 +17,7 @@ noise_threshold = 1e-10 order = 10 [quantities.et_imag] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "imag_first" label = "total energy Im(et[1])" @@ -25,7 +25,7 @@ noise_threshold = 1e-10 order = 11 [quantities.ep_real] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "real_first" label = "plasma energy Re(ep[1])" @@ -33,7 +33,7 @@ noise_threshold = 1e-10 order = 12 [quantities.ev_real] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "real_first" label = "vacuum energy Re(ev[1])" @@ -41,7 +41,7 @@ noise_threshold = 1e-10 order = 13 [quantities.vacuum_eigenvalue] -h5path = "FreeBoundaryStability/vacuum_eigenvalue" +h5path = "ForceFreeStates/FreeBoundaryStability/vacuum_eigenvalue" type = "real_scalar" extract = "value" label = "vacuum matrix min eigenvalue" @@ -50,7 +50,7 @@ order = 14 # Energies — full eigenvalue arrays [quantities.ep_all] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "all_complex" label = "plasma energy (all)" @@ -58,7 +58,7 @@ noise_threshold = 1e-10 order = 20 [quantities.ev_all] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "all_complex" label = "vacuum energy (all)" @@ -66,7 +66,7 @@ noise_threshold = 1e-10 order = 21 [quantities.et_all] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "all_complex" label = "total energy (all)" @@ -75,7 +75,7 @@ order = 22 # Integration [quantities.nstep] -h5path = "integration/nstep" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep" type = "int_scalar" extract = "value" label = "ODE steps (saved)" @@ -83,7 +83,7 @@ noise_threshold = 0 order = 30 [quantities.nstep_total] -h5path = "integration/nstep_total" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep_total" type = "int_scalar" extract = "value" label = "ODE steps (total)" @@ -92,7 +92,7 @@ order = 31 # Equilibrium [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" @@ -100,7 +100,7 @@ noise_threshold = 1e-12 order = 40 [quantities.q95] -h5path = "equil/q95" +h5path = "Equilibrium/q95" type = "real_scalar" extract = "value" label = "q95" @@ -108,7 +108,7 @@ noise_threshold = 1e-12 order = 41 [quantities.betat] -h5path = "equil/betat" +h5path = "Equilibrium/betat" type = "real_scalar" extract = "value" label = "beta_t" @@ -116,7 +116,7 @@ noise_threshold = 1e-12 order = 42 [quantities.betan] -h5path = "equil/betan" +h5path = "Equilibrium/betan" type = "real_scalar" extract = "value" label = "beta_n" @@ -125,7 +125,7 @@ order = 43 # Singular surfaces [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" @@ -133,7 +133,7 @@ noise_threshold = 0 order = 50 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" @@ -141,7 +141,7 @@ noise_threshold = 1e-8 order = 51 [quantities.sing_q] -h5path = "singular/q" +h5path = "SingularSurfaces/q" type = "real_vector" extract = "all_real" label = "singular q values" @@ -150,7 +150,7 @@ order = 52 # Mode numbers [quantities.mpert] -h5path = "info/mpert" +h5path = "Info/mpert" type = "int_scalar" extract = "value" label = "mpert" @@ -158,7 +158,7 @@ noise_threshold = 0 order = 60 [quantities.npert] -h5path = "info/npert" +h5path = "Info/npert" type = "int_scalar" extract = "value" label = "npert" @@ -167,7 +167,7 @@ order = 61 # Profiles (checksums for fast comparison of large arrays) [quantities.q_profile] -h5path = "splines/profiles/q" +h5path = "Equilibrium/Profiles/q" type = "real_vector" extract = "checksum" label = "q profile (checksum)" @@ -175,7 +175,7 @@ noise_threshold = 0 order = 70 [quantities.pressure_profile] -h5path = "splines/profiles/mu0p" +h5path = "Equilibrium/Profiles/mu0p" type = "real_vector" extract = "checksum" label = "pressure profile (checksum)" diff --git a/regression-harness/src/extractor.jl b/regression-harness/src/extractor.jl index f82ca8fe2..c028dba81 100644 --- a/regression-harness/src/extractor.jl +++ b/regression-harness/src/extractor.jl @@ -2,6 +2,70 @@ HDF5 quantity extraction engine. """ +# Legacy-path fallback: outputs written before the module-mirroring CamelCase schema +# use the old group names on the right. Case TOMLs always carry the new paths; when a +# path is missing (the output came from a pre-rename ref), the translated legacy path +# is retried so cross-commit comparisons and --ref-range scans work across the +# boundary. First matching prefix wins — keep more-specific entries first. +const LEGACY_PREFIX_MAP = [ + "Input/RawInputs/Equilibrium" => "input/raw_inputs/equilibrium", + "Input/RawInputs/ForcingTerms" => "input/raw_inputs/forcing_terms", + "Input/RawInputs/Coils" => "input/raw_inputs/coils", + "Input/" => "input/", + "Info/" => "info/", + "Equilibrium/Profiles/" => "splines/profiles/", + "Equilibrium/Geometry/" => "splines/rzphi/", + "Equilibrium/" => "equil/", + "ForceFreeStates/Solutions/ForwardIntegration/" => "integration/", + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/" => "galerkin/solution/", + "ForceFreeStates/Solutions/GalerkinIntegration/Match/InnerParams/" => "galerkin/match/inner_params/", + "ForceFreeStates/Solutions/GalerkinIntegration/Match/Inner/" => "galerkin/match/inner/", + "ForceFreeStates/Solutions/GalerkinIntegration/Match/" => "galerkin/match/", + "ForceFreeStates/Solutions/GalerkinIntegration/" => "galerkin/", + "ForceFreeStates/EulerLagrangeMatrices/Ideal/" => "matrices/ideal/", + "ForceFreeStates/EulerLagrangeMatrices/Kinetic/" => "matrices/kinetic/", + "ForceFreeStates/EulerLagrangeMatrices/" => "matrices/", + "ForceFreeStates/FreeBoundaryStability/" => "FreeBoundaryStability/", + "ForceFreeStates/EdgeScan/" => "EdgeScan/", + "LocalStability/" => "locstab/", + "SingularSurfaces/GalerkinDeltaPrime/" => "galerkin/", + "SingularSurfaces/Kinetic/" => "singular/kinetic/", + "SingularSurfaces/" => "singular/", + "PerturbedEquilibrium/ForcingModes/" => "perturbed_equilibrium/forcing_modes/", + "PerturbedEquilibrium/ResponseMatrices/" => "perturbed_equilibrium/response_matrices/", + "PerturbedEquilibrium/Response/" => "perturbed_equilibrium/response/", + "PerturbedEquilibrium/SingularCoupling/" => "perturbed_equilibrium/singular_coupling/", + "PerturbedEquilibrium/Energies/" => "perturbed_equilibrium/energies/", + "PerturbedEquilibrium/" => "perturbed_equilibrium/", + "KineticForces/" => "kinetic_forces/", + "Tearing/PerSurface/DpMatrix/" => "slayer/per_surface/dp_matrix/", + "Tearing/PerSurface/" => "slayer/per_surface/", + "Tearing/Roots/" => "slayer/roots/", + "Tearing/LayerWidths/" => "slayer/layer_widths/", + "Tearing/Diagnostics/ValidRoots/" => "slayer/diagnostics/valid_roots/", + "Tearing/Diagnostics/Poles/" => "slayer/diagnostics/poles/", + "Tearing/Diagnostics/FilteredRoots/" => "slayer/diagnostics/filtered_roots/", + "Tearing/Scan/Surface_" => "slayer/scan/surface_", + "Tearing/" => "slayer/", +] + +""" +Translate a new-schema h5path to its pre-rename legacy equivalent, or return +`nothing` when no mapping applies. +""" +function legacy_h5path(path::String) + for (new, old) in LEGACY_PREFIX_MAP + if startswith(path, new) + legacy = replace(path, new => old; count=1) + # Structural renames inside KineticForces (not plain prefix swaps). + legacy = replace(legacy, "/EnergyIntegrals/" => "/records/") + legacy = replace(legacy, r"^kinetic_forces/(\w+)/KineticMatrices/" => s"kinetic_forces/matrices_\1/") + return legacy + end + end + return nothing +end + """ Extract all quantities from a gpec.h5 file according to case spec. Returns a Vector{ExtractedQuantity}. @@ -19,8 +83,13 @@ function extract_quantities(h5path::String, qty_specs::Vector{QuantitySpec}, run continue end - # Check if the H5 path exists - if !haskey(fid, spec.h5path) + # Resolve the H5 path, falling back to the pre-rename legacy schema. + path = spec.h5path + if !haskey(fid, path) + legacy = legacy_h5path(path) + path = (legacy !== nothing && haskey(fid, legacy)) ? legacy : nothing + end + if path === nothing push!(results, ExtractedQuantity( spec.name, spec.label, nothing, nothing, nothing, @@ -28,7 +97,7 @@ function extract_quantities(h5path::String, qty_specs::Vector{QuantitySpec}, run continue end - raw = read(fid[spec.h5path]) + raw = read(fid[path]) eq = apply_extraction(spec, raw) push!(results, eq) end @@ -91,7 +160,7 @@ function apply_extraction(spec::QuantitySpec, raw)::ExtractedQuantity elseif spec.extract == "diagonal_complex" # Extract the diagonal of a square matrix as a complex array. - # Use for tracking per-surface BVP Δ' from singular/delta_prime_matrix. + # Use for tracking per-surface BVP Δ' from SingularSurfaces/delta_prime_matrix. ndims(raw) == 2 && size(raw, 1) == size(raw, 2) || error("diagonal_complex requires a square 2-D matrix; got size $(size(raw))") diag_vec = [raw[i, i] for i in 1:size(raw, 1)] diff --git a/regression-harness/src/runner.jl b/regression-harness/src/runner.jl index ecb7b760f..4ff877244 100644 --- a/regression-harness/src/runner.jl +++ b/regression-harness/src/runner.jl @@ -127,11 +127,11 @@ t_start = time() pe = Equilibrium.setup_equilibrium(cfg) elapsed = time() - t_start h5open(ARGS[1], "w") do fid - fid["equil/psio"] = pe.psio - fid["equil/q0"] = pe.params.q0 - fid["equil/q95"] = pe.params.q95 - fid["equil/betat"] = pe.params.betat - fid["equil/betan"] = pe.params.betan + fid["Equilibrium/psio"] = pe.psio + fid["Equilibrium/q0"] = pe.params.q0 + fid["Equilibrium/q95"] = pe.params.q95 + fid["Equilibrium/betat"] = pe.params.betat + fid["Equilibrium/betan"] = pe.params.betan end open(ARGS[2], "w") do f println(f, elapsed) diff --git a/regression-harness/src/types.jl b/regression-harness/src/types.jl index 0d0a3e638..a388c2422 100644 --- a/regression-harness/src/types.jl +++ b/regression-harness/src/types.jl @@ -7,7 +7,7 @@ Specification for a single quantity to extract from gpec.h5. """ struct QuantitySpec name::String - h5path::String # HDF5 dataset path (e.g. "FreeBoundaryStability/eigenmode_energies"), empty for runtime + h5path::String # HDF5 dataset path (e.g. "ForceFreeStates/FreeBoundaryStability/eigenmode_energies"), empty for runtime type::String # "complex_vector", "real_vector", "real_scalar", "int_scalar", "real_matrix", "runtime" extract::String # "value", "real_first", "imag_first", "abs_first", "norm", "all_real", "all_complex", "checksum" label::String # Human-readable label for reports diff --git a/src/Analysis/Equilibrium.jl b/src/Analysis/Equilibrium.jl index c82c419cb..9777907f7 100644 --- a/src/Analysis/Equilibrium.jl +++ b/src/Analysis/Equilibrium.jl @@ -30,8 +30,8 @@ A `Plots.jl` plot object. """ function plot_qprofile(h5path; show_singular=true, save_path=nothing) xs, q, q0, q95 = h5open(h5path, "r") do fid - read(fid["splines/profiles/xs"]), read(fid["splines/profiles/q"]), - read(fid["equil/q0"]), read(fid["equil/q95"]) + read(fid["Equilibrium/Profiles/xs"]), read(fid["Equilibrium/Profiles/q"]), + read(fid["Equilibrium/q0"]), read(fid["Equilibrium/q95"]) end p = plot( @@ -51,7 +51,7 @@ function plot_qprofile(h5path; show_singular=true, save_path=nothing) if show_singular msing, psi_sing, q_sing = h5open(h5path, "r") do fid - read(fid["singular/msing"]), read(fid["singular/psi"]), read(fid["singular/q"]) + read(fid["SingularSurfaces/msing"]), read(fid["SingularSurfaces/psi"]), read(fid["SingularSurfaces/q"]) end for s in 1:msing vline!(p, [psi_sing[s]]; linestyle=:dash, color=:red, label=nothing) @@ -83,8 +83,8 @@ A `Plots.jl` plot object. """ function plot_pressure_profile(h5path; save_path=nothing) xs, mu0p, msing, psi_sing = h5open(h5path, "r") do fid - read(fid["splines/profiles/xs"]), read(fid["splines/profiles/mu0p"]), - read(fid["singular/msing"]), read(fid["singular/psi"]) + read(fid["Equilibrium/Profiles/xs"]), read(fid["Equilibrium/Profiles/mu0p"]), + read(fid["SingularSurfaces/msing"]), read(fid["SingularSurfaces/psi"]) end p = plot( @@ -124,8 +124,8 @@ A `Plots.jl` plot object. """ function plot_f_profile(h5path; save_path=nothing) xs, twopif, msing, psi_sing = h5open(h5path, "r") do fid - read(fid["splines/profiles/xs"]), read(fid["splines/profiles/2piF"]), - read(fid["singular/msing"]), read(fid["singular/psi"]) + read(fid["Equilibrium/Profiles/xs"]), read(fid["Equilibrium/Profiles/2piF"]), + read(fid["SingularSurfaces/msing"]), read(fid["SingularSurfaces/psi"]) end p = plot( @@ -171,10 +171,10 @@ A `Plots.jl` plot object. """ function plot_flux_surfaces(h5path; n_psi=11, n_theta=18, save_path=nothing) rcoords, offset_data, xs_rz, ys_rz, ro, zo, msing, psi_sing, q_sing = h5open(h5path, "r") do fid - read(fid["splines/rzphi/rcoords"]), read(fid["splines/rzphi/offset"]), - read(fid["splines/rzphi/xs"]), read(fid["splines/rzphi/ys"]), - read(fid["equil/ro"]), read(fid["equil/zo"]), - read(fid["singular/msing"]), read(fid["singular/psi"]), read(fid["singular/q"]) + read(fid["Equilibrium/Geometry/rcoords"]), read(fid["Equilibrium/Geometry/offset"]), + read(fid["Equilibrium/Geometry/xs"]), read(fid["Equilibrium/Geometry/ys"]), + read(fid["Equilibrium/ro"]), read(fid["Equilibrium/zo"]), + read(fid["SingularSurfaces/msing"]), read(fid["SingularSurfaces/psi"]), read(fid["SingularSurfaces/q"]) end n_psi_grid = length(xs_rz) @@ -361,9 +361,9 @@ A `Plots.jl` plot object. """ function plot_equilibrium_summary(h5path; save_path=nothing) q0, q95, betat, betan, kappa, li1 = h5open(h5path, "r") do fid - read(fid["equil/q0"]), read(fid["equil/q95"]), - read(fid["equil/betat"]), read(fid["equil/betan"]), - read(fid["equil/kappa"]), read(fid["equil/li1"]) + read(fid["Equilibrium/q0"]), read(fid["Equilibrium/q95"]), + read(fid["Equilibrium/betat"]), read(fid["Equilibrium/betan"]), + read(fid["Equilibrium/kappa"]), read(fid["Equilibrium/li1"]) end title_str = "q0=$(round(q0,digits=2)) q95=$(round(q95,digits=2)) βₜ=$(round(betat,digits=3)) βₙ=$(round(betan,digits=3)) κ=$(round(kappa,digits=2)) li1=$(round(li1,digits=3))" diff --git a/src/Analysis/ForceFreeStates.jl b/src/Analysis/ForceFreeStates.jl index 1bbee56bf..c92d6e698 100644 --- a/src/Analysis/ForceFreeStates.jl +++ b/src/Analysis/ForceFreeStates.jl @@ -33,8 +33,8 @@ A `Plots.jl` plot object. """ function plot_mode_displacement(h5path; modes=1:5, save_path=nothing) mlow, xi_psi, psi, et = h5open(h5path, "r") do fid - read(fid["info/mlow"]), read(fid["integration/xi_psi"]), - read(fid["integration/psi"]), read(fid["FreeBoundaryStability/eigenmode_energies"]) + read(fid["Info/mlow"]), read(fid["ForceFreeStates/Solutions/ForwardIntegration/xi_psi"]), + read(fid["ForceFreeStates/Solutions/ForwardIntegration/psi"]), read(fid["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) end mpert = size(xi_psi, 1) @@ -81,7 +81,7 @@ A `Plots.jl` plot object. """ function plot_fixed_boundary_stability_criterion(h5path; save_path=nothing) psi, crit = h5open(h5path, "r") do fid - read(fid["integration/psi"]), read(fid["integration/crit"]) + read(fid["ForceFreeStates/Solutions/ForwardIntegration/psi"]), read(fid["ForceFreeStates/Solutions/ForwardIntegration/crit"]) end p = plot( @@ -129,7 +129,7 @@ function plot_energy_eigenvectors(h5path; matrix_type=:total, save_path=nothing) error("matrix_type=$matrix_type not supported; only :total has eigenvector matrix stored in HDF5 (ep/ev are eigenvalue vectors, not matrices)") wt, psio, mlow = h5open(h5path, "r") do fid - read(fid["FreeBoundaryStability/W_freeboundary_eigenmodes"]), read(fid["equil/psio"]), read(fid["info/mlow"]) + read(fid["ForceFreeStates/FreeBoundaryStability/W_freeboundary_eigenmodes"]), read(fid["Equilibrium/psio"]), read(fid["Info/mlow"]) end isempty(wt) && error("No vacuum data in $h5path; rerun with vac_flag = true") @@ -191,16 +191,16 @@ A `Plots.jl` plot object, or `nothing` if no `EdgeScan/` group is present in the """ function plot_edge_stability_scan(h5path; save_path=nothing, ylims=(-2, 3), kwargs...) has_scan, q, et, ep, ev, evonly, qlim = h5open(h5path, "r") do fid - if !haskey(fid, "EdgeScan/psi") + if !haskey(fid, "ForceFreeStates/EdgeScan/psi") return false, Float64[], ComplexF64[], ComplexF64[], ComplexF64[], Float64[], NaN end true, - read(fid["EdgeScan/q"]), - read(fid["EdgeScan/total_energy"]), - read(fid["EdgeScan/plasma_energy"]), - read(fid["EdgeScan/vacuum_energy"]), - read(fid["EdgeScan/vacuum_eigenvalue"]), - read(fid["info/qlim"]) + read(fid["ForceFreeStates/EdgeScan/q"]), + read(fid["ForceFreeStates/EdgeScan/total_energy"]), + read(fid["ForceFreeStates/EdgeScan/plasma_energy"]), + read(fid["ForceFreeStates/EdgeScan/vacuum_energy"]), + read(fid["ForceFreeStates/EdgeScan/vacuum_eigenvalue"]), + read(fid["Info/qlim"]) end if !has_scan @@ -264,9 +264,9 @@ A `Plots.jl` plot object. """ function plot_eigenvalues(h5path; matrix_type=:total, save_path=nothing) dataset = Dict( - :total => "FreeBoundaryStability/eigenmode_energies", - :plasma => "FreeBoundaryStability/eigenmode_plasma_energies", - :vacuum => "FreeBoundaryStability/eigenmode_vacuum_energies" + :total => "ForceFreeStates/FreeBoundaryStability/eigenmode_energies", + :plasma => "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies", + :vacuum => "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" ) haskey(dataset, matrix_type) || error("matrix_type must be :total, :plasma, or :vacuum") @@ -321,9 +321,9 @@ A `Plots.jl` plot object. """ function plot_delta_prime(h5path; save_path=nothing) msing, psi_sing, q_sing, ca_l, ca_r, psio, mn_index = h5open(h5path, "r") do fid - read(fid["singular/msing"]), read(fid["singular/psi"]), read(fid["singular/q"]), - read(fid["singular/ca_left"]), read(fid["singular/ca_right"]), - read(fid["equil/psio"]), read(fid["info/mn_index"]) + read(fid["SingularSurfaces/msing"]), read(fid["SingularSurfaces/psi"]), read(fid["SingularSurfaces/q"]), + read(fid["SingularSurfaces/ca_left"]), read(fid["SingularSurfaces/ca_right"]), + read(fid["Equilibrium/psio"]), read(fid["Info/mn_index"]) end msing == 0 && return plot(; title="No singular surfaces found", legend=false) @@ -374,8 +374,8 @@ end Plot the BALOO-style infinite-n ballooning stability diagram: the experimental pressure gradient α (solid) and the first stability boundary α_crit (dashed) versus normalized poloidal flux ψ_N. Surfaces where the experimental α lies above the boundary -are ballooning-unstable. Reads `locstab/psi`, `locstab/alpha`, and -`locstab/alpha_critical` (populated when ForceFreeStates runs with +are ballooning-unstable. Reads `LocalStability/psi`, `LocalStability/alpha`, and +`LocalStability/alpha_critical` (populated when ForceFreeStates runs with `local_stability_flag = true`). ### Arguments @@ -394,8 +394,8 @@ A `Plots.jl` plot object. """ function plot_ballooning_alpha_boundary(h5path; save_path=nothing, psi_min=0.0) psi, alpha, alpha_crit = h5open(h5path, "r") do fid - haskey(fid, "locstab/alpha") || return (Float64[], Float64[], Float64[]) - read(fid["locstab/psi"]), read(fid["locstab/alpha"]), read(fid["locstab/alpha_critical"]) + haskey(fid, "LocalStability/alpha") || return (Float64[], Float64[], Float64[]) + read(fid["LocalStability/psi"]), read(fid["LocalStability/alpha"]), read(fid["LocalStability/alpha_critical"]) end isempty(alpha) && return plot(; title="No local stability data (set local_stability_flag)", legend=false) @@ -424,7 +424,7 @@ end plot_cond_fbar(h5path; save_path=nothing, zoom=false) Plot `cond(F̄)` vs ψ from the kinetic-singular-surface scan stored in -`singular/kinetic/` (populated when ForceFreeStates runs with +`SingularSurfaces/Kinetic/` (populated when ForceFreeStates runs with `kinetic_factor > 0`, `singfac_min > 0`). `F̄` is the kinetic Euler-Lagrange matrix formed by Schur-reducing the six @@ -457,16 +457,16 @@ A `Plots.jl` plot object, or `nothing` if no kinetic scan is stored in the file. """ function plot_cond_fbar(h5path; save_path=nothing, zoom=false) scan_psi, scan_cond, thr, k_psi, i_psi, i_q, kmsing = h5open(h5path, "r") do fid - if !(haskey(fid, "singular") && haskey(fid["singular"], "kinetic")) + if !(haskey(fid, "SingularSurfaces") && haskey(fid["SingularSurfaces"], "Kinetic")) return Float64[], Float64[], 0.0, Float64[], Float64[], Float64[], 0 end - kg = fid["singular/kinetic"] + kg = fid["SingularSurfaces/Kinetic"] (read(kg["scan_psi"]), read(kg["scan_cond"]), read(kg["scan_threshold"]), read(kg["psi"]), - read(fid["singular/psi"]), - read(fid["singular/q"]), + read(fid["SingularSurfaces/psi"]), + read(fid["SingularSurfaces/q"]), read(kg["kmsing"])) end @@ -553,8 +553,8 @@ A `Plots.jl` plot object. """ function plot_ffs_summary(h5path; save_path=nothing) has_vac = h5open(h5path, "r") do fid - haskey(fid, "FreeBoundaryStability/W_freeboundary_eigenmodes") && - !isempty(read(fid["FreeBoundaryStability/W_freeboundary_eigenmodes"])) + haskey(fid, "ForceFreeStates/FreeBoundaryStability/W_freeboundary_eigenmodes") && + !isempty(read(fid["ForceFreeStates/FreeBoundaryStability/W_freeboundary_eigenmodes"])) end p_crit = plot_fixed_boundary_stability_criterion(h5path) diff --git a/src/Analysis/PerturbedEquilibrium.jl b/src/Analysis/PerturbedEquilibrium.jl index f1219a2e9..da005a5c2 100644 --- a/src/Analysis/PerturbedEquilibrium.jl +++ b/src/Analysis/PerturbedEquilibrium.jl @@ -2,7 +2,7 @@ PerturbedEquilibrium Post-processing and visualization functions for GPEC perturbed equilibrium results stored -in the `perturbed_equilibrium/` group of a GPEC HDF5 output file. +in the `PerturbedEquilibrium/` group of a GPEC HDF5 output file. """ module PerturbedEquilibrium @@ -10,6 +10,10 @@ using HDF5 using LaTeXStrings using Plots +# HDF5 group prefixes shared by every reader in this module. +const PE_SINGULAR_COUPLING = "PerturbedEquilibrium/SingularCoupling/" +const PE_RESPONSE = "PerturbedEquilibrium/Response/" + # Check that a PE dataset exists and is non-empty. function _has_pe_data(h5path, key) h5open(h5path, "r") do fid @@ -25,7 +29,7 @@ per toroidal mode n. Integer-valued q rational surfaces are annotated. The reson `b^r = Φ^r/A^r` is the resonant flux normalized by the scalar surface area, in tesla [Pharr 2026]. Requires the perturbed equilibrium module to have been run and -`singular_coupling/resonant_area_weighted_field` to be present in the HDF5 file. +`SingularCoupling/resonant_area_weighted_field` to be present in the HDF5 file. ### Arguments @@ -40,7 +44,7 @@ Requires the perturbed equilibrium module to have been run and A `Plots.jl` plot object. """ function plot_resonant_area_weighted_field_amplitude(h5path; save_path=nothing) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "resonant_area_weighted_field") || return plot(; title="No resonant area-weighted field data — run with perturbed equilibrium enabled", legend=false) @@ -76,7 +80,7 @@ end Scatter plot of island half-width `w/2` per rational surface vs ψ_N. Integer-valued q rational surfaces are annotated. -Requires `singular_coupling/island_half_width` in the HDF5 file. +Requires `SingularCoupling/island_half_width` in the HDF5 file. ### Arguments @@ -91,7 +95,7 @@ Requires `singular_coupling/island_half_width` in the HDF5 file. A `Plots.jl` plot object. """ function plot_island_widths(h5path; save_path=nothing) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "island_half_width") || return plot(; title="No island width data — run with perturbed equilibrium enabled", legend=false) @@ -130,7 +134,7 @@ Scatter plot of the Chirikov overlap parameter per rational surface vs ψ_N, wit horizontal reference line at K = 1 (island overlap threshold). Points are colored red when K > 1. Integer-valued q rational surfaces are annotated. -Requires `singular_coupling/chirikov_parameter` in the HDF5 file. +Requires `SingularCoupling/chirikov_parameter` in the HDF5 file. ### Arguments @@ -145,7 +149,7 @@ Requires `singular_coupling/chirikov_parameter` in the HDF5 file. A `Plots.jl` plot object. """ function plot_chirikov_parameter(h5path; save_path=nothing) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "chirikov_parameter") || return plot(; title="No Chirikov data — run with perturbed equilibrium enabled", legend=false) @@ -184,15 +188,15 @@ end plot_driven_delta_prime(h5path; save_path=nothing) Scatter plot of `Re(Δ')` per rational surface vs ψ_N, computed by the perturbed -equilibrium module (from `perturbed_equilibrium/singular_coupling/delta_prime`). +equilibrium module (from `PerturbedEquilibrium/SingularCoupling/delta_prime`). One marker series per toroidal mode n. Integer-valued q rational surfaces are annotated. This is the forcing-driven Δ' (response to the applied perturbation amplitudes in `intr.forcing_modes`); for the equilibrium-intrinsic Δ' from the STRIDE BVP, -read `singular/delta_prime_matrix` from the HDF5 directly. +read `SingularSurfaces/delta_prime_matrix` from the HDF5 directly. -Requires `perturbed_equilibrium/singular_coupling/delta_prime` in the HDF5 file. +Requires `PerturbedEquilibrium/SingularCoupling/delta_prime` in the HDF5 file. ### Arguments @@ -207,7 +211,7 @@ Requires `perturbed_equilibrium/singular_coupling/delta_prime` in the HDF5 file. A `Plots.jl` plot object. """ function plot_driven_delta_prime(h5path; save_path=nothing) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "delta_prime") || return plot(; title="No PE Δ' data — run with perturbed equilibrium enabled", legend=false) @@ -293,7 +297,7 @@ end # Internal helper — resonant current scatter plot function _plot_resonant_current(h5path) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "resonant_current") || return plot(; title="No resonant current data", legend=false) @@ -360,7 +364,7 @@ function plot_mode_spectrogram(h5path; component=:xi_psi, save_path=nothing) haskey(comp_map, component) || error("component must be one of :xi_psi, :b_psi, :b_theta, :b_zeta") - base = "perturbed_equilibrium/response/" + base = PE_RESPONSE dataset_path = base * comp_map[component] _has_pe_data(h5path, dataset_path) || @@ -368,10 +372,10 @@ function plot_mode_spectrogram(h5path; component=:xi_psi, save_path=nothing) data, psi_response, mlow, mhigh, nhigh, q95, rational_psi = h5open(h5path, "r") do fid read(fid[dataset_path]), - read(fid["integration/psi"]), - read(fid["info/mlow"]), read(fid["info/mhigh"]), read(fid["info/nhigh"]), - read(fid["equil/q95"]), - read(fid["perturbed_equilibrium/singular_coupling/rational_psi"]) + read(fid["ForceFreeStates/Solutions/ForwardIntegration/psi"]), + read(fid["Info/mlow"]), read(fid["Info/mhigh"]), read(fid["Info/nhigh"]), + read(fid["Equilibrium/q95"]), + read(fid[PE_SINGULAR_COUPLING * "rational_psi"]) end mpert = mhigh - mlow + 1 @@ -452,13 +456,13 @@ end # Internal helper — |b_psi(m)| spectrum at the outermost psi surface function _plot_bpsi_edge_spectrum(h5path) - base = "perturbed_equilibrium/response/" + base = PE_RESPONSE _has_pe_data(h5path, base * "b_psi_area_weighted") || return plot(; title="No b_psi data — run with perturbed equilibrium enabled", legend=false) data, mlow, mhigh = h5open(h5path, "r") do fid read(fid[base * "b_psi_area_weighted"]), - read(fid["info/mlow"]), read(fid["info/mhigh"]) + read(fid["Info/mlow"]), read(fid["Info/mhigh"]) end mpert = mhigh - mlow + 1 diff --git a/src/Analysis/PerturbedEquilibriumModes.jl b/src/Analysis/PerturbedEquilibriumModes.jl index 198ad8693..b5e5a8e45 100644 --- a/src/Analysis/PerturbedEquilibriumModes.jl +++ b/src/Analysis/PerturbedEquilibriumModes.jl @@ -21,7 +21,7 @@ the HDF5 file. # Arguments - `h5_file::String`: Path to gpec.h5 output file -- `variable::String`: HDF5 dataset path, e.g. `"perturbed_equilibrium/response/xi_R"` +- `variable::String`: HDF5 dataset path, e.g. `"PerturbedEquilibrium/Response/xi_R"` # Keyword arguments - `mtheta::Int`: theta grid resolution (default: `max(2*(|mlow|+mpert), 512)`) @@ -45,10 +45,10 @@ function modes_to_theta(h5_file::String, variable::String; modes = read(f, variable) # (npsi, numpert_total) npsi, numpert_total = size(modes) - mlow = read(f, "info/mlow") - nlow = read(f, "info/nlow") - mpert = read(f, "info/mpert") - npert = read(f, "info/npert") + mlow = read(f, "Info/mlow") + nlow = read(f, "Info/nlow") + mpert = read(f, "Info/mpert") + npert = read(f, "Info/npert") @assert numpert_total == mpert * npert "Expected numpert_total=$(mpert*npert), got $numpert_total" n_vals = [nlow + k - 1 for k in 1:npert] @@ -66,19 +66,19 @@ function modes_to_theta(h5_file::String, variable::String; if !keep_sfl_phi # Reconstruct ν spline from stored grid + nodal values (FastInterpolations v0.4 API) - rzphi_xs = read(f, "splines/rzphi/xs") - rzphi_ys = read(f, "splines/rzphi/ys") - nu_vals = read(f, "splines/rzphi/nu") + rzphi_xs = read(f, "Equilibrium/Geometry/xs") + rzphi_ys = read(f, "Equilibrium/Geometry/ys") + nu_vals = read(f, "Equilibrium/Geometry/nu") nu_spline = cubic_interp( (rzphi_xs, rzphi_ys), nu_vals; bc=(CubicFit(), PeriodicBC()), extrap=(ExtendExtrap(), WrapExtrap()) ) - psi_grid = read(f, "integration/psi") + psi_grid = read(f, "ForceFreeStates/Solutions/ForwardIntegration/psi") - bt_sign = haskey(f, "equil/bt_sign") ? read(f, "equil/bt_sign") : 1 - crnt = haskey(f, "equil/crnt") ? read(f, "equil/crnt") : 1.0 + bt_sign = haskey(f, "Equilibrium/bt_sign") ? read(f, "Equilibrium/bt_sign") : 1 + crnt = haskey(f, "Equilibrium/crnt") ? read(f, "Equilibrium/crnt") : 1.0 helicity = bt_sign * Int(sign(crnt)) hint = (Ref(1), Ref(1)) diff --git a/src/Equilibrium/ReadEquilibrium.jl b/src/Equilibrium/ReadEquilibrium.jl index 973ae882d..8cc221b7a 100644 --- a/src/Equilibrium/ReadEquilibrium.jl +++ b/src/Equilibrium/ReadEquilibrium.jl @@ -397,7 +397,7 @@ end build_direct_from_ingest(config::EquilibriumConfig, ingest::DirectIngest) -> DirectRunInput Rebuild a `DirectRunInput` from a [`DirectIngest`](@ref) captured by `read_efit`/`read_imas` -(or restored from `input/raw_inputs/equilibrium/` inside `gpec.h5`). Inverse of that capture: +(or restored from `Input/RawInputs/Equilibrium/` inside `gpec.h5`). Inverse of that capture: reconstructs the splines so the rerun path skips the g-file/IMAS parse, reusing the existing solver dispatch. """ diff --git a/src/ForceFreeStates/Ballooning.jl b/src/ForceFreeStates/Ballooning.jl index d24f9ff81..2de0e2511 100644 --- a/src/ForceFreeStates/Ballooning.jl +++ b/src/ForceFreeStates/Ballooning.jl @@ -87,8 +87,8 @@ flux surface [Glasser-Greene-Johnson; Glasser Phys. Plasmas 23, 112506 field and metric quantities. The main local-stability scan takes `D_I` from the `det(d0bar)` calculation -reported as `locstab/di`, then combines it with this surface-average `H` to -form `locstab/dr`. This avoids recomputing a separate surface-average `D_I` +reported as `LocalStability/di`, then combines it with this surface-average `H` to +form `LocalStability/dr`. This avoids recomputing a separate surface-average `D_I` inside the `D_R` path. """ function resistive_interchange_h(flux_surface_index::Int, plasma_eq::Equilibrium.PlasmaEquilibrium) diff --git a/src/ForceFreeStates/EulerLagrange.jl b/src/ForceFreeStates/EulerLagrange.jl index 1f1197bf6..af73d37da 100644 --- a/src/ForceFreeStates/EulerLagrange.jl +++ b/src/ForceFreeStates/EulerLagrange.jl @@ -141,7 +141,7 @@ end Main driver for integrating the Euler-Lagrange equations across the plasma and detecting singular surfaces. Formerly `ode_run`. Has the same functionality as `ode_run` in the Fortran code, with the addition of -a single dump to the `euler.h5` file at the end of integration instead of multiple dumps +a single dump to the `gpec.h5` file at the end of integration instead of multiple dumps to `euler.bin` throughout the integration. We have made the control logic more clear by pre-computing all integration chunks upfront and using a for loop to iterate through them, eliminating the while-loop logic and making integration bounds explicit at each step. diff --git a/src/ForceFreeStates/Galerkin/GalerkinSolve.jl b/src/ForceFreeStates/Galerkin/GalerkinSolve.jl index aa033a363..e7e73fd50 100644 --- a/src/ForceFreeStates/Galerkin/GalerkinSolve.jl +++ b/src/ForceFreeStates/Galerkin/GalerkinSolve.jl @@ -230,55 +230,60 @@ end """ write_galerkin!(out_h5, result::GalerkinResult) -Write the Galerkin Δ′ outputs into the open HDF5 file under the `galerkin/` group. Replaces the Fortran -`delta_gw`/`pest3_data` ASCII/binary outputs. +Write the Galerkin outputs into the open HDF5 file. The integrator's solution functions and +RPEC matching data go under `ForceFreeStates/Solutions/GalerkinIntegration/`; the per-surface +Δ′/PEST-3 matching results consolidate with the other rational-surface stability results under +`SingularSurfaces/GalerkinDeltaPrime/`. Replaces the Fortran `delta_gw`/`pest3_data` +ASCII/binary outputs. """ function write_galerkin!(out_h5, result::GalerkinResult) - out_h5["galerkin/msing"] = result.msing + gal = "ForceFreeStates/Solutions/GalerkinIntegration" + gdp = "SingularSurfaces/GalerkinDeltaPrime" + out_h5["$gal/msing"] = result.msing result.msing == 0 && return nothing - out_h5["galerkin/delta"] = result.delta - out_h5["galerkin/pest3_A"] = result.Ap - out_h5["galerkin/pest3_B"] = result.Bp - out_h5["galerkin/pest3_Gamma"] = result.Gammap - out_h5["galerkin/pest3_Delta"] = result.Deltap - out_h5["galerkin/sing_psi"] = result.sing_psi - out_h5["galerkin/sing_q"] = result.sing_q - out_h5["galerkin/sing_m"] = result.sing_m - out_h5["galerkin/sing_n"] = result.sing_n - out_h5["galerkin/di"] = result.di - out_h5["galerkin/alpha"] = result.alpha + out_h5["$gdp/delta"] = result.delta + out_h5["$gdp/pest3_A"] = result.Ap + out_h5["$gdp/pest3_B"] = result.Bp + out_h5["$gdp/pest3_Gamma"] = result.Gammap + out_h5["$gdp/pest3_Delta"] = result.Deltap + out_h5["$gdp/sing_psi"] = result.sing_psi + out_h5["$gdp/sing_q"] = result.sing_q + out_h5["$gdp/sing_m"] = result.sing_m + out_h5["$gdp/sing_n"] = result.sing_n + out_h5["$gdp/di"] = result.di + out_h5["$gdp/alpha"] = result.alpha if !isempty(result.delta_coil) - out_h5["galerkin/delta_coil"] = result.delta_coil + out_h5["$gdp/delta_coil"] = result.delta_coil end if result.solution !== nothing sol = result.solution - out_h5["galerkin/solution/psi"] = sol.psi - out_h5["galerkin/solution/q"] = sol.q - out_h5["galerkin/solution/issing"] = collect(sol.issing) - out_h5["galerkin/solution/xi"] = sol.xi - out_h5["galerkin/solution/xi_deriv"] = sol.xi_deriv - isempty(sol.xi_cut) || (out_h5["galerkin/solution/xi_cut"] = sol.xi_cut) - isempty(sol.cut_range) || (out_h5["galerkin/solution/cut_range"] = sol.cut_range) + out_h5["$gal/Solution/psi"] = sol.psi + out_h5["$gal/Solution/q"] = sol.q + out_h5["$gal/Solution/issing"] = collect(sol.issing) + out_h5["$gal/Solution/xi"] = sol.xi + out_h5["$gal/Solution/xi_deriv"] = sol.xi_deriv + isempty(sol.xi_cut) || (out_h5["$gal/Solution/xi_cut"] = sol.xi_cut) + isempty(sol.cut_range) || (out_h5["$gal/Solution/cut_range"] = sol.cut_range) end if result.match !== nothing m = result.match - out_h5["galerkin/match/cout"] = m.cout - out_h5["galerkin/match/cin"] = m.cin - out_h5["galerkin/match/xi"] = m.xi - out_h5["galerkin/match/xi_deriv"] = m.xi_deriv - out_h5["galerkin/match/deltar"] = m.deltar - out_h5["galerkin/match/bpen"] = m.bpen - out_h5["galerkin/match/rpec_eig"] = m.rpec_eig + out_h5["$gal/Match/cout"] = m.cout + out_h5["$gal/Match/cin"] = m.cin + out_h5["$gal/Match/xi"] = m.xi + out_h5["$gal/Match/xi_deriv"] = m.xi_deriv + out_h5["$gal/Match/deltar"] = m.deltar + out_h5["$gal/Match/bpen"] = m.bpen + out_h5["$gal/Match/rpec_eig"] = m.rpec_eig # Per-surface inner-layer ξ_ψ(ψ) (match.f intotsol); ragged grids → one dataset pair per surface. for i in eachindex(m.inner_psi) - out_h5["galerkin/match/inner/psi_$i"] = m.inner_psi[i] - out_h5["galerkin/match/inner/xi_$i"] = m.inner_xi[i] - out_h5["galerkin/match/inner/b_$i"] = m.inner_b[i] + out_h5["$gal/Match/Inner/psi_$i"] = m.inner_psi[i] + out_h5["$gal/Match/Inner/xi_$i"] = m.inner_xi[i] + out_h5["$gal/Match/Inner/b_$i"] = m.inner_b[i] end - out_h5["galerkin/match/residual"] = m.residual + out_h5["$gal/Match/residual"] = m.residual if !isempty(m.inner_params) for f in (:E, :F, :G, :H, :K, :M, :taua, :taur, :v1) - out_h5["galerkin/match/inner_params/$(f)"] = [getfield(pp, f) for pp in m.inner_params] + out_h5["$gal/Match/InnerParams/$(f)"] = [getfield(pp, f) for pp in m.inner_params] end end end diff --git a/src/ForceFreeStates/ResistEval.jl b/src/ForceFreeStates/ResistEval.jl index 8bfc5a03f..04953b6c0 100644 --- a/src/ForceFreeStates/ResistEval.jl +++ b/src/ForceFreeStates/ResistEval.jl @@ -52,7 +52,7 @@ supporting flux-surface averages. | `v1_local` | dV/dψ at this surface | `H` here is identical to the `H` reported by `mercier_scan!` and stored -in `locstab/h` — the GGJ routine recomputes it for convenience. +in `LocalStability/h` — the GGJ routine recomputes it for convenience. `avg_B`, `B_max`, `B_min`, `f_trap`, `R_major`, and `eps_local` are used by `NeoclassicalResistivity.eta_neoclassical` to form the Sauter/Redl diff --git a/src/ForcingTerms/CoilGeometry.jl b/src/ForcingTerms/CoilGeometry.jl index cd6bcb17d..b2d19ab21 100644 --- a/src/ForcingTerms/CoilGeometry.jl +++ b/src/ForcingTerms/CoilGeometry.jl @@ -608,7 +608,7 @@ end # # A coil set is stored as one subgroup per set under a parent group, named by the # set's `name`. This mirrors the open-group idiom of `save_forcing_to_h5` so the -# data can live inside `gpec.h5` (e.g. `input/raw_inputs/coils/`) next to unrelated +# data can live inside `gpec.h5` (e.g. `Input/RawInputs/Coils/`) next to unrelated # content that the reader silently ignores. # # //x Float64[ncoil, s, nsec] (shape gives ncoil, s, nsec) diff --git a/src/ForcingTerms/ForcingTerms.jl b/src/ForcingTerms/ForcingTerms.jl index 1869053f9..de4e16801 100644 --- a/src/ForcingTerms/ForcingTerms.jl +++ b/src/ForcingTerms/ForcingTerms.jl @@ -254,7 +254,7 @@ end Populate `forcing_modes` from an already-open HDF5 group with datasets `n`, `m`, `amplitude_real`, `amplitude_imag`. Mirror of `save_forcing_to_h5` for the rerun path — deliberately accepts an open group rather than a file path so -the snapshot data can live inside `gpec.h5/input/raw_inputs/forcing_terms/`. +the snapshot data can live inside `gpec.h5/Input/RawInputs/ForcingTerms/`. """ function load_forcing_from_h5_group!(forcing_modes::Vector{ForcingMode}, group) n_array = read(group, "n") diff --git a/src/GeneralizedPerturbedEquilibrium.jl b/src/GeneralizedPerturbedEquilibrium.jl index b65e7aaad..7d3b4ca0e 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -59,6 +59,15 @@ include("Analysis/Analysis.jl") import .Analysis as Analysis export Analysis +# HDF5 paths read back by the file-based rerun (Rerun.jl); shared consts keep the +# writer and rerun reader from drifting apart. Schema conventions: +# docs/development/hdf5-conventions.md. +const H5_INPUT_TOML = "Input/gpec_toml_raw" +const H5_RAW_EQUILIBRIUM = "Input/RawInputs/Equilibrium" +const H5_RAW_FORCING = "Input/RawInputs/ForcingTerms" +const H5_RAW_COILS = "Input/RawInputs/Coils" +const H5_GIT_VERSION = "Info/git_version" + include("Rerun.jl") # Import ForceFreeStates types and functions needed for main @@ -154,12 +163,12 @@ analytic `*Config` or IMAS `dd` (TOML path), or `nothing` for file-based equilib already read from the source HDF5 snapshot, so `compute_perturbed_equilibrium` does not have to touch the original `forcing.dat` path. When `nothing`, the ForcingTerms data is loaded from disk at snapshot time (if PerturbedEquilibrium -is enabled) so it still ends up in `input/raw_inputs/forcing_terms/`. +is enabled) so it still ends up in `Input/RawInputs/ForcingTerms/`. `preloaded_coil_sets` similarly lets the rerun path inject coil geometry read from -`input/raw_inputs/coils/` so a coil run can be replayed (recomputing the field +`Input/RawInputs/Coils/` so a coil run can be replayed (recomputing the field against the current equilibrium) without the original `.dat`/`.h5` files. The coil -geometry actually used by the run is always written back into `input/raw_inputs/coils/`. +geometry actually used by the run is always written back into `Input/RawInputs/Coils/`. """ function main_from_inputs( inputs::Dict{String,Any}, @@ -290,7 +299,7 @@ function main_from_inputs( end # Forcing-data snapshot: when PerturbedEquilibrium is enabled, load forcing - # modes early so they can be written into `input/raw_inputs/forcing_terms/` + # modes early so they can be written into `Input/RawInputs/ForcingTerms/` # alongside the TOML blob. On the rerun path the caller passes the modes in # directly via `preloaded_forcing_modes`, bypassing the original file. Coil # forcing is recomputed from the `[[ForcingTerms.coil_set]]` TOML blob on @@ -529,7 +538,7 @@ function main_from_inputs( @info "SLAYER completed in $(@sprintf("%.3f", time() - slayer_start)) s" h5_filename = pe_file === nothing ? ctrl.HDF5_filename : pe_file h5_path = joinpath(intr.dir_path, h5_filename) - # Append the slayer/ group; create the file if no prior stage wrote + # Append the Tearing/ group; create the file if no prior stage wrote # it (e.g. write_outputs_to_HDF5 disabled) rather than failing on "r+". HDF5.h5open(h5_path, isfile(h5_path) ? "r+" : "w") do f Runner.write_slayer_hdf5!(f, result) @@ -718,7 +727,7 @@ function write_outputs_to_HDF5( h5open(joinpath(intr.dir_path, ctrl.HDF5_filename), "w") do out_h5 # Store git version for reproducibility - out_h5["info/git_version"] = git_version + out_h5[H5_GIT_VERSION] = git_version # Outer-region Galerkin Δ′ matrix (RDCON), if computed if gal_data !== nothing @@ -729,111 +738,111 @@ function write_outputs_to_HDF5( # ForceFreeStates/Equilibrium/Wall/PE control struct), plus the equilibrium ingest # arrays so a file-based rerun never needs the original g-file / CHEASE / IMAS source. if inputs !== nothing - out_h5["input/gpec_toml_raw"] = sprint(TOML.print, inputs) + out_h5[H5_INPUT_TOML] = sprint(TOML.print, inputs) end if equil.ingest !== nothing # analytic equilibria are regenerated from their TOML section - eq_group = "input/raw_inputs/equilibrium" - out_h5["$eq_group/ingest_kind"] = equil.ingest isa Equilibrium.DirectIngest ? "direct" : "inverse" + out_h5["$H5_RAW_EQUILIBRIUM/ingest_kind"] = equil.ingest isa Equilibrium.DirectIngest ? "direct" : "inverse" for f in fieldnames(typeof(equil.ingest)) - out_h5["$eq_group/$f"] = getfield(equil.ingest, f) + out_h5["$H5_RAW_EQUILIBRIUM/$f"] = getfield(equil.ingest, f) end end if forcing_modes !== nothing - forcing_group = create_group(out_h5, "input/raw_inputs/forcing_terms") + forcing_group = create_group(out_h5, H5_RAW_FORCING) ForcingTerms.save_forcing_to_h5(forcing_modes, forcing_group) end # Write derived run parameters - out_h5["info/mpert"] = intr.mpert - out_h5["info/mlow"] = intr.mlow - out_h5["info/mhigh"] = intr.mhigh - out_h5["info/npert"] = intr.npert - out_h5["info/nlow"] = intr.nlow - out_h5["info/nhigh"] = intr.nhigh + out_h5["Info/mpert"] = intr.mpert + out_h5["Info/mlow"] = intr.mlow + out_h5["Info/mhigh"] = intr.mhigh + out_h5["Info/npert"] = intr.npert + out_h5["Info/nlow"] = intr.nlow + out_h5["Info/nhigh"] = intr.nhigh m = [(i - 1) % intr.mpert + intr.mlow for i in 1:(intr.numpert_total)] n = [(i - 1) ÷ intr.mpert + intr.nlow for i in 1:(intr.numpert_total)] - out_h5["info/mn_index"] = hcat(m, n) # (N, 2) matrix - out_h5["info/psilim"] = intr.psilim - out_h5["info/qlim"] = intr.qlim - out_h5["info/q1lim"] = intr.q1lim + out_h5["Info/mn_index"] = hcat(m, n) # (N, 2) matrix + out_h5["Info/psilim"] = intr.psilim + out_h5["Info/qlim"] = intr.qlim + out_h5["Info/q1lim"] = intr.q1lim # Write derived equilibrium parameters for (key, val) in zip(fieldnames(Equilibrium.EquilibriumParameters), getfield.(Ref(equil.params), fieldnames(Equilibrium.EquilibriumParameters))) if val !== nothing # TODO: looks like ro, zo, psio, and b_norm are not set, so skipping those for now but should fix eventually - out_h5["equil/$key"] = val + out_h5["Equilibrium/$key"] = val end end - out_h5["equil/psio"] = equil.psio - out_h5["equil/ro"] = equil.ro - out_h5["equil/zo"] = equil.zo + out_h5["Equilibrium/psio"] = equil.psio + out_h5["Equilibrium/ro"] = equil.ro + out_h5["Equilibrium/zo"] = equil.zo - # Write spline arrays (using profiles with named splines) + # Write equilibrium profile and geometry arrays (from the named splines) profiles = equil.profiles - out_h5["splines/profiles/xs"] = profiles.xs - out_h5["splines/profiles/2piF"] = profiles.F_spline.y - out_h5["splines/profiles/mu0p"] = profiles.P_spline.y - out_h5["splines/profiles/dVdpsi"] = profiles.dVdpsi_spline.y - out_h5["splines/profiles/q"] = profiles.q_spline.y - out_h5["splines/rzphi/xs"] = equil.rzphi_xs - out_h5["splines/rzphi/ys"] = equil.rzphi_ys + out_h5["Equilibrium/Profiles/xs"] = profiles.xs + out_h5["Equilibrium/Profiles/2piF"] = profiles.F_spline.y + out_h5["Equilibrium/Profiles/mu0p"] = profiles.P_spline.y + out_h5["Equilibrium/Profiles/dVdpsi"] = profiles.dVdpsi_spline.y + out_h5["Equilibrium/Profiles/q"] = profiles.q_spline.y + out_h5["Equilibrium/Geometry/xs"] = equil.rzphi_xs + out_h5["Equilibrium/Geometry/ys"] = equil.rzphi_ys # Extract grid point values from interpolants for HDF5 output - out_h5["splines/rzphi/rcoords"] = equil.rzphi_rsquared.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/offset"] = equil.rzphi_offset.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/nu"] = equil.rzphi_nu.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/jac"] = equil.rzphi_jac.nodal_derivs.partials[1, :, :] + out_h5["Equilibrium/Geometry/rcoords"] = equil.rzphi_rsquared.nodal_derivs.partials[1, :, :] + out_h5["Equilibrium/Geometry/offset"] = equil.rzphi_offset.nodal_derivs.partials[1, :, :] + out_h5["Equilibrium/Geometry/nu"] = equil.rzphi_nu.nodal_derivs.partials[1, :, :] + out_h5["Equilibrium/Geometry/jac"] = equil.rzphi_jac.nodal_derivs.partials[1, :, :] # Write local stability data; always write all entries, using empty arrays when not computed. - # locstab/di = Mercier D_I (det(d0bar)); locstab/dr = resistive interchange D_R; - # locstab/ballooning_Delta_prime = high-n ballooning Δ' (distinct from the Riccati - # tearing Δ' under perturbed_equilibrium/singular_coupling/delta_prime). + # LocalStability/di = Mercier D_I (det(d0bar)); LocalStability/dr = resistive interchange D_R; + # LocalStability/ballooning_Delta_prime = high-n ballooning Δ' (distinct from the Riccati + # tearing Δ' under PerturbedEquilibrium/SingularCoupling/delta_prime). if ctrl.local_stability_flag locstab_xs = intr.locstab.cache.x - out_h5["locstab/di"] = intr.locstab.y[:, 1] ./ locstab_xs - out_h5["locstab/dr"] = intr.locstab.y[:, 2] ./ locstab_xs + out_h5["LocalStability/di"] = intr.locstab.y[:, 1] ./ locstab_xs + out_h5["LocalStability/dr"] = intr.locstab.y[:, 2] ./ locstab_xs else - out_h5["locstab/di"] = Float64[] - out_h5["locstab/dr"] = Float64[] + out_h5["LocalStability/di"] = Float64[] + out_h5["LocalStability/dr"] = Float64[] end - out_h5["singular/di0"] = (ctrl.local_stability_flag && !isempty(intr.sing)) ? - [intr.locstab(sing.psifac)[1] / sing.psifac for sing in intr.sing] : Float64[] - out_h5["locstab/ballooning_Delta_prime"] = ctrl.local_stability_flag ? intr.locstab.y[:, 4] : Float64[] + out_h5["SingularSurfaces/di0"] = (ctrl.local_stability_flag && !isempty(intr.sing)) ? + [intr.locstab(sing.psifac)[1] / sing.psifac for sing in intr.sing] : Float64[] + out_h5["LocalStability/ballooning_Delta_prime"] = ctrl.local_stability_flag ? intr.locstab.y[:, 4] : Float64[] # First ballooning stability boundary: experimental α vs critical α (BALOO-style). - out_h5["locstab/psi"] = ballooning_boundary.psi - out_h5["locstab/alpha"] = ballooning_boundary.alpha - out_h5["locstab/alpha_critical"] = ballooning_boundary.alpha_critical + out_h5["LocalStability/psi"] = ballooning_boundary.psi + out_h5["LocalStability/alpha"] = ballooning_boundary.alpha + out_h5["LocalStability/alpha_critical"] = ballooning_boundary.alpha_critical # Write integration data - out_h5["integration/nstep"] = odet.step # Number of saved solution snapshots - out_h5["integration/nstep_total"] = odet.total_steps # Total ODE solver steps taken - out_h5["integration/psi"] = odet.psi_store - out_h5["integration/q"] = odet.q_store - out_h5["integration/xi_psi"] = odet.u_store[:, :, 1, :] - out_h5["integration/u2"] = odet.u_store[:, :, 2, :] # TODO: what to name this? These are the "conjugate momenta" of u1 - out_h5["integration/dxi_psi"] = odet.du_store[:, :, 1, :] - out_h5["integration/xi_s"] = odet.xi_s_store - out_h5["integration/crit"] = odet.crit_store + fwd = "ForceFreeStates/Solutions/ForwardIntegration" + out_h5["$fwd/nstep"] = odet.step # Number of saved solution snapshots + out_h5["$fwd/nstep_total"] = odet.total_steps # Total ODE solver steps taken + out_h5["$fwd/psi"] = odet.psi_store + out_h5["$fwd/q"] = odet.q_store + out_h5["$fwd/xi_psi"] = odet.u_store[:, :, 1, :] + out_h5["$fwd/u2"] = odet.u_store[:, :, 2, :] # TODO: what to name this? These are the "conjugate momenta" of u1 + out_h5["$fwd/dxi_psi"] = odet.du_store[:, :, 1, :] + out_h5["$fwd/xi_s"] = odet.xi_s_store + out_h5["$fwd/crit"] = odet.crit_store # Write edge stability scan data (only present when psiedge < psilim). # Generalized (W, N) pencil energies — power-normalized, Jacobian-invariant; these are # the values findmax_dW_edge! uses to choose the truncation point. if !isempty(odet.edge_scan.psi) es = odet.edge_scan - out_h5["EdgeScan/psi"] = es.psi - out_h5["EdgeScan/q"] = es.q - out_h5["EdgeScan/total_energy"] = es.total_eigenvalue - out_h5["EdgeScan/plasma_energy"] = es.plasma_energy - out_h5["EdgeScan/vacuum_energy"] = es.vacuum_energy - out_h5["EdgeScan/vacuum_eigenvalue"] = es.vacuum_eigenvalue + out_h5["ForceFreeStates/EdgeScan/psi"] = es.psi + out_h5["ForceFreeStates/EdgeScan/q"] = es.q + out_h5["ForceFreeStates/EdgeScan/total_energy"] = es.total_eigenvalue + out_h5["ForceFreeStates/EdgeScan/plasma_energy"] = es.plasma_energy + out_h5["ForceFreeStates/EdgeScan/vacuum_energy"] = es.vacuum_energy + out_h5["ForceFreeStates/EdgeScan/vacuum_eigenvalue"] = es.vacuum_eigenvalue end # Write singular surface data - out_h5["singular/msing"] = intr.msing - out_h5["singular/psi"] = [sing.psifac for sing in intr.sing] - out_h5["singular/q"] = [sing.q for sing in intr.sing] - out_h5["singular/q1"] = [sing.q1 for sing in intr.sing] - out_h5["singular/ca_left"] = odet.ca_l - out_h5["singular/ca_right"] = odet.ca_r + out_h5["SingularSurfaces/msing"] = intr.msing + out_h5["SingularSurfaces/psi"] = [sing.psifac for sing in intr.sing] + out_h5["SingularSurfaces/q"] = [sing.q for sing in intr.sing] + out_h5["SingularSurfaces/q1"] = [sing.q1 for sing in intr.sing] + out_h5["SingularSurfaces/ca_left"] = odet.ca_l + out_h5["SingularSurfaces/ca_right"] = odet.ca_r if intr.msing > 0 # Mode numbers at each surface (jagged — pad with 0 to max_modes width) @@ -846,8 +855,8 @@ function write_outputs_to_HDF5( n_matrix[s, i] = sing.n[i] end end - out_h5["singular/m"] = m_matrix - out_h5["singular/n"] = n_matrix + out_h5["SingularSurfaces/m"] = m_matrix + out_h5["SingularSurfaces/n"] = n_matrix # Glasser-Greene-Johnson geometric coefficients + surface averages # (populated by ForceFreeStates.resist_eval_all! after sing_find!). @@ -856,17 +865,17 @@ function write_outputs_to_HDF5( # downstream consumers (Tearing.InnerLayer.GGJ.build_ggj_inputs) # can reconstruct τ_A / τ_R from any kinetic-profile source. if all(s -> s.restype !== nothing, intr.sing) - out_h5["singular/E"] = [s.restype.E for s in intr.sing] - out_h5["singular/F"] = [s.restype.F for s in intr.sing] - out_h5["singular/G"] = [s.restype.G for s in intr.sing] - out_h5["singular/H"] = [s.restype.H for s in intr.sing] - out_h5["singular/K"] = [s.restype.K for s in intr.sing] - out_h5["singular/M"] = [s.restype.M for s in intr.sing] - out_h5["singular/avg_bsq_over_dpsisq"] = [s.restype.avg_bsq_over_dpsisq for s in intr.sing] - out_h5["singular/avg_bsq"] = [s.restype.avg_bsq for s in intr.sing] - out_h5["singular/p_local"] = [s.restype.p_local for s in intr.sing] - out_h5["singular/p1_local"] = [s.restype.p1_local for s in intr.sing] - out_h5["singular/v1_local"] = [s.restype.v1_local for s in intr.sing] + out_h5["SingularSurfaces/E"] = [s.restype.E for s in intr.sing] + out_h5["SingularSurfaces/F"] = [s.restype.F for s in intr.sing] + out_h5["SingularSurfaces/G"] = [s.restype.G for s in intr.sing] + out_h5["SingularSurfaces/H"] = [s.restype.H for s in intr.sing] + out_h5["SingularSurfaces/K"] = [s.restype.K for s in intr.sing] + out_h5["SingularSurfaces/M"] = [s.restype.M for s in intr.sing] + out_h5["SingularSurfaces/avg_bsq_over_dpsisq"] = [s.restype.avg_bsq_over_dpsisq for s in intr.sing] + out_h5["SingularSurfaces/avg_bsq"] = [s.restype.avg_bsq for s in intr.sing] + out_h5["SingularSurfaces/p_local"] = [s.restype.p_local for s in intr.sing] + out_h5["SingularSurfaces/p1_local"] = [s.restype.p1_local for s in intr.sing] + out_h5["SingularSurfaces/v1_local"] = [s.restype.v1_local for s in intr.sing] end end @@ -875,15 +884,16 @@ function write_outputs_to_HDF5( # Write inter-surface Δ' matrix if computed (parallel FM path only). # Shape: [msing × msing] — PEST3-convention deltap (STRIDE BVP with vacuum coupling). if intr.msing > 0 && !isempty(intr.delta_prime_matrix) - out_h5["singular/delta_prime_matrix"] = intr.delta_prime_matrix + out_h5["SingularSurfaces/delta_prime_matrix"] = intr.delta_prime_matrix end # Edge coil-response matrix, stored (numpert_total × 2msing) = (edge mode, surface-side) to match - # the galerkin/delta_coil layout so H5Web heatmaps share axes (x = edge mode, y = surface-side). + # the SingularSurfaces/GalerkinDeltaPrime/delta_coil layout so H5Web heatmaps share axes + # (x = edge mode, y = surface-side). # Internal intr.delta_coil stays (2msing × numpert_total); transpose only at write. if intr.msing > 0 && !isempty(intr.delta_coil) dc = permutedims(intr.delta_coil) - out_h5["singular/delta_coil"] = dc + out_h5["SingularSurfaces/delta_coil"] = dc end # Write raw 2msing×2msing outer-region D' matrix in side-major ordering @@ -892,18 +902,18 @@ function write_outputs_to_HDF5( # Needed for the full det(D' − D(γ)) = 0 eigenvalue problem via # pest3_decompose to recover (A', B', Γ', Δ'). if intr.msing > 0 && !isempty(intr.delta_prime_raw) - out_h5["singular/delta_prime_raw"] = intr.delta_prime_raw + out_h5["SingularSurfaces/delta_prime_raw"] = intr.delta_prime_raw end # Write kinetic singular surface data (det(F̄) near-zeros) and the cond(F̄) scan # used to find them. Populated only when kinetic crossings were searched for. - out_h5["singular/kinetic/kmsing"] = intr.kmsing - out_h5["singular/kinetic/psi"] = [s.psifac for s in intr.kinsing] - out_h5["singular/kinetic/q"] = [s.q for s in intr.kinsing] - out_h5["singular/kinetic/q1"] = [s.q1 for s in intr.kinsing] - out_h5["singular/kinetic/scan_psi"] = intr.kinsing_scan_psi - out_h5["singular/kinetic/scan_cond"] = intr.kinsing_scan_cond - out_h5["singular/kinetic/scan_threshold"] = intr.kinsing_scan_threshold + out_h5["SingularSurfaces/Kinetic/kmsing"] = intr.kmsing + out_h5["SingularSurfaces/Kinetic/psi"] = [s.psifac for s in intr.kinsing] + out_h5["SingularSurfaces/Kinetic/q"] = [s.q for s in intr.kinsing] + out_h5["SingularSurfaces/Kinetic/q1"] = [s.q1 for s in intr.kinsing] + out_h5["SingularSurfaces/Kinetic/scan_psi"] = intr.kinsing_scan_psi + out_h5["SingularSurfaces/Kinetic/scan_cond"] = intr.kinsing_scan_cond + out_h5["SingularSurfaces/Kinetic/scan_threshold"] = intr.kinsing_scan_threshold # Write free-boundary stability data. The eigenmode energies are the generalized # eigenvalues of the pencil (W, N) with N the power-normalization (surface-norm) matrix: @@ -911,14 +921,15 @@ function write_outputs_to_HDF5( # working (Jacobian) coordinate. W_freeboundary_eigenmodes holds the generalized # eigenvectors, columns sorted most-unstable first, normalized to unit power norm with # the largest-magnitude entry made real-positive. - out_h5["FreeBoundaryStability/W_freeboundary"] = ctrl.vac_flag ? vac_data.wt0 : ComplexF64[] - out_h5["FreeBoundaryStability/W_plasma"] = ctrl.vac_flag ? vac_data.wp : ComplexF64[] - out_h5["FreeBoundaryStability/W_vacuum"] = ctrl.vac_flag ? vac_data.wv : ComplexF64[] - out_h5["FreeBoundaryStability/W_freeboundary_eigenmodes"] = ctrl.vac_flag ? vac_data.wt : ComplexF64[] - out_h5["FreeBoundaryStability/eigenmode_energies"] = ctrl.vac_flag ? vac_data.et : ComplexF64[] - out_h5["FreeBoundaryStability/eigenmode_plasma_energies"] = ctrl.vac_flag ? vac_data.ep : ComplexF64[] - out_h5["FreeBoundaryStability/eigenmode_vacuum_energies"] = ctrl.vac_flag ? vac_data.ev : ComplexF64[] - out_h5["FreeBoundaryStability/vacuum_eigenvalue"] = ctrl.vac_flag ? vac_data.vacuum_eigenvalue : NaN + fbs = "ForceFreeStates/FreeBoundaryStability" + out_h5["$fbs/W_freeboundary"] = ctrl.vac_flag ? vac_data.wt0 : ComplexF64[] + out_h5["$fbs/W_plasma"] = ctrl.vac_flag ? vac_data.wp : ComplexF64[] + out_h5["$fbs/W_vacuum"] = ctrl.vac_flag ? vac_data.wv : ComplexF64[] + out_h5["$fbs/W_freeboundary_eigenmodes"] = ctrl.vac_flag ? vac_data.wt : ComplexF64[] + out_h5["$fbs/eigenmode_energies"] = ctrl.vac_flag ? vac_data.et : ComplexF64[] + out_h5["$fbs/eigenmode_plasma_energies"] = ctrl.vac_flag ? vac_data.ep : ComplexF64[] + out_h5["$fbs/eigenmode_vacuum_energies"] = ctrl.vac_flag ? vac_data.ev : ComplexF64[] + out_h5["$fbs/vacuum_eigenvalue"] = ctrl.vac_flag ? vac_data.vacuum_eigenvalue : NaN # Cartesian surface point clouds used downstream for visualisation and # perturbed-equilibrium plotting. @@ -929,12 +940,6 @@ function write_outputs_to_HDF5( out_h5["SurfaceGeometries/Wall/y"] = ctrl.vac_flag ? vac_data.wall_pts[:, 2] : Float64[] out_h5["SurfaceGeometries/Wall/z"] = ctrl.vac_flag ? vac_data.wall_pts[:, 3] : Float64[] - # Write kinetic parameters when kinetic mode is enabled - if ctrl.kinetic_factor > 0 - out_h5["kinetic/kinetic_source"] = ctrl.kinetic_source - out_h5["kinetic/kinetic_factor"] = ctrl.kinetic_factor - end - # Write fundamental matrices on the ψ grid if ffit !== nothing xs = equil.rzphi_xs @@ -951,39 +956,39 @@ function write_outputs_to_HDF5( return arr end - out_h5["matrices/psi"] = xs + elm = "ForceFreeStates/EulerLagrangeMatrices" + out_h5["$elm/psi"] = xs # Ideal primitive matrices (A, B, C, D, E, H) # When kinetic mode is on, amats/bmats/cmats hold kinetic-modified values, # so we write those as the "effective" matrices and save raw kinetic # components separately below. - # Ideal primitive matrices (A, B, C, D, E, H) if ctrl.kinetic_factor > 0 # Use preserved ideal copies (before kinetic overwrite) - out_h5["matrices/ideal/A"] = _eval_mat_spline(ffit.amats_ideal) - out_h5["matrices/ideal/B"] = _eval_mat_spline(ffit.bmats_ideal) - out_h5["matrices/ideal/C"] = _eval_mat_spline(ffit.cmats_ideal) + out_h5["$elm/Ideal/A"] = _eval_mat_spline(ffit.amats_ideal) + out_h5["$elm/Ideal/B"] = _eval_mat_spline(ffit.bmats_ideal) + out_h5["$elm/Ideal/C"] = _eval_mat_spline(ffit.cmats_ideal) else - out_h5["matrices/ideal/A"] = _eval_mat_spline(ffit.amats) - out_h5["matrices/ideal/B"] = _eval_mat_spline(ffit.bmats) - out_h5["matrices/ideal/C"] = _eval_mat_spline(ffit.cmats) + out_h5["$elm/Ideal/A"] = _eval_mat_spline(ffit.amats) + out_h5["$elm/Ideal/B"] = _eval_mat_spline(ffit.bmats) + out_h5["$elm/Ideal/C"] = _eval_mat_spline(ffit.cmats) end - out_h5["matrices/ideal/D"] = _eval_mat_spline(ffit.dmats_prim) - out_h5["matrices/ideal/E"] = _eval_mat_spline(ffit.emats_prim) - out_h5["matrices/ideal/H"] = _eval_mat_spline(ffit.hmats) + out_h5["$elm/Ideal/D"] = _eval_mat_spline(ffit.dmats_prim) + out_h5["$elm/Ideal/E"] = _eval_mat_spline(ffit.emats_prim) + out_h5["$elm/Ideal/H"] = _eval_mat_spline(ffit.hmats) # Ideal derived matrices (F, K, G) - out_h5["matrices/ideal/F"] = _eval_mat_spline(ffit.fmats_lower) - out_h5["matrices/ideal/K"] = _eval_mat_spline(ffit.kmats) - out_h5["matrices/ideal/G"] = _eval_mat_spline(ffit.gmats) + out_h5["$elm/Ideal/F"] = _eval_mat_spline(ffit.fmats_lower) + out_h5["$elm/Ideal/K"] = _eval_mat_spline(ffit.kmats) + out_h5["$elm/Ideal/G"] = _eval_mat_spline(ffit.gmats) # Kinetic-modified matrices if ctrl.kinetic_factor > 0 - out_h5["matrices/kinetic/A"] = _eval_mat_spline(ffit.amats) - out_h5["matrices/kinetic/B"] = _eval_mat_spline(ffit.bmats) - out_h5["matrices/kinetic/C"] = _eval_mat_spline(ffit.cmats) - out_h5["matrices/kinetic/f0"] = _eval_mat_spline(ffit.f0mats) - out_h5["matrices/kinetic/K"] = _eval_mat_spline(ffit.kkmats) - out_h5["matrices/kinetic/G"] = _eval_mat_spline(ffit.gaats) + out_h5["$elm/Kinetic/A"] = _eval_mat_spline(ffit.amats) + out_h5["$elm/Kinetic/B"] = _eval_mat_spline(ffit.bmats) + out_h5["$elm/Kinetic/C"] = _eval_mat_spline(ffit.cmats) + out_h5["$elm/Kinetic/f0"] = _eval_mat_spline(ffit.f0mats) + out_h5["$elm/Kinetic/K"] = _eval_mat_spline(ffit.kkmats) + out_h5["$elm/Kinetic/G"] = _eval_mat_spline(ffit.gaats) end end end @@ -992,15 +997,15 @@ end """ _write_coil_snapshot!(h5_path::String, coil_sets::Vector{CoilSet}) -Append the coil geometry used by a run into `input/raw_inputs/coils/` of an existing +Append the coil geometry used by a run into `Input/RawInputs/Coils/` of an existing gpec.h5 file (opened in append mode), so the run can be replayed from the output alone. One subgroup per coil set; see `ForcingTerms.save_coils_to_h5`. """ function _write_coil_snapshot!(h5_path::String, coil_sets::Vector{ForcingTerms.CoilSet}) isfile(h5_path) || return nothing h5open(h5_path, "r+") do out_h5 - haskey(out_h5, "input/raw_inputs/coils") && return nothing - ForcingTerms.save_coils_to_h5(coil_sets, create_group(out_h5, "input/raw_inputs/coils")) + haskey(out_h5, H5_RAW_COILS) && return nothing + ForcingTerms.save_coils_to_h5(coil_sets, create_group(out_h5, H5_RAW_COILS)) end return nothing end diff --git a/src/KineticForces/KineticForcesStructs.jl b/src/KineticForces/KineticForcesStructs.jl index 7e533d8e9..6df40e6a8 100644 --- a/src/KineticForces/KineticForcesStructs.jl +++ b/src/KineticForces/KineticForcesStructs.jl @@ -491,7 +491,7 @@ end KineticForcesState Accumulated results from all KineticForces computations. -Written to gpec.h5 under the "kinetic_forces" group. +Written to gpec.h5 under the "KineticForces" group. """ @kwdef mutable struct KineticForcesState method_results::Dict{String, MethodResult} = Dict{String, MethodResult}() diff --git a/src/KineticForces/Output.jl b/src/KineticForces/Output.jl index d1fef4e09..502be6b77 100644 --- a/src/KineticForces/Output.jl +++ b/src/KineticForces/Output.jl @@ -9,14 +9,14 @@ then write to gpec.h5 in a single pass. """ write_to_hdf5!(h5file::HDF5.File, state::KineticForcesState) -Write all KineticForces results to the "kinetic_forces" group in gpec.h5. +Write all KineticForces results to the "KineticForces" group in gpec.h5. # Arguments - `h5file::HDF5.File`: Open HDF5 file handle - `state::KineticForcesState`: Accumulated computation results """ function write_to_hdf5!(h5file::HDF5.File, state::KineticForcesState) - g = create_group(h5file, "kinetic_forces") + g = create_group(h5file, "KineticForces") for (method_name, result) in state.method_results mg = create_group(g, method_name) @@ -50,9 +50,10 @@ function write_to_hdf5!(h5file::HDF5.File, state::KineticForcesState) end end - # Write kinetic matrices if present + # Write the six drift-kinetic coefficient matrices if present for (method_name, mat) in state.kinetic_matrices - mat_g = create_group(g, "matrices_$method_name") + method_g = haskey(g, method_name) ? g[method_name] : create_group(g, method_name) + mat_g = create_group(method_g, "KineticMatrices") for k in 1:6 mat_g["matrix_$k"] = mat[:, :, k] end @@ -70,7 +71,7 @@ This is the standard HDF5 ragged array pattern for storing variable-length data. - `records::Vector{EnergyIntegrationResult}`: Integration records to write """ function write_integration_records!(mg::HDF5.Group, records::Vector{EnergyIntegrationResult}) - rg = create_group(mg, "records") + rg = create_group(mg, "EnergyIntegrals") # Scalar fields per record rg["psi"] = [r.psi for r in records] diff --git a/src/PerturbedEquilibrium/Utils.jl b/src/PerturbedEquilibrium/Utils.jl index a7f06db8c..418a1ddd7 100644 --- a/src/PerturbedEquilibrium/Utils.jl +++ b/src/PerturbedEquilibrium/Utils.jl @@ -53,14 +53,14 @@ Write perturbed equilibrium results to HDF5 file (appends to existing ForceFreeS ## Output Structure ``` -perturbed_equilibrium/ -├── forcing_modes/ +PerturbedEquilibrium/ +├── ForcingModes/ │ ├── n # Toroidal mode numbers │ ├── m # Poloidal mode numbers │ └── amplitude # ComplexF64 forcing amplitudes ├── forcing_b / forcing_b_root_area / forcing_b_area # control-surface forcing spectrum (b, b̃, b̄) [numpert_total], tesla ├── response_b / response_b_root_area / response_b_area # control-surface response spectrum (b, b̃, b̄) [numpert_total], tesla -├── response/ +├── Response/ │ ├── psi_n # Radial abscissa ψ_N [npsi] shared by every response profile below │ ├── xi_psi # Radial displacement ξ^ψ = ξ·∇ψ (ComplexF64 [npsi, mpert]) │ ├── xi_psi_J # J·ξ^ψ Jacobian-weighted (from gpeq_contra) @@ -69,14 +69,14 @@ perturbed_equilibrium/ │ ├── xi_n # Physical normal displacement xi_n (ComplexF64 [npsi, mpert]) │ ├── b_theta │ └── b_zeta -├── response_matrices/ # [numpert_total × numpert_total], root-area-weighted field (b̃) space; R = S·A +├── ResponseMatrices/ # [numpert_total × numpert_total], root-area-weighted field (b̃) space; R = S·A │ ├── plasma_inductance # Λ̃ = R⁻¹·Λ·R⁻† │ ├── surface_inductance # L̃ = R⁻¹·L·R⁻† │ ├── permeability # P̃ = R⁻¹·P·R (P = Λ·L⁻¹) │ ├── reluctance # ϱ̃ = R†·ϱ·R │ ├── rootarea_to_area_weight_operator # S = Σ/√A at psilim; recover area-weighted field b̄ = S·b̃ │ └── surface_area # scalar A = ∫J|∇ψ|dθ; recover flux via Φ = A·b̄ -├── singular_coupling/ +├── SingularCoupling/ │ ├── C_resonant_area_weighted_field # [n_rational × numpert_total] coupling matrix (b̃-space input, resonant area-weighted field b^r=Φ^r/A^r [T]) │ ├── C_resonant_current │ ├── C_island_width_sq @@ -93,7 +93,7 @@ perturbed_equilibrium/ │ ├── rational_q │ ├── rational_m_res │ └── rational_n -└── energies/ +└── Energies/ ├── vacuum_energy ├── surface_energy ├── plasma_energy @@ -106,10 +106,10 @@ function write_outputs_to_HDF5( filename::String ) h5open(filename, "cw") do file - pe_group = haskey(file, "perturbed_equilibrium") ? file["perturbed_equilibrium"] : create_group(file, "perturbed_equilibrium") + pe_group = haskey(file, "PerturbedEquilibrium") ? file["PerturbedEquilibrium"] : create_group(file, "PerturbedEquilibrium") # Forcing modes - forcing_group = haskey(pe_group, "forcing_modes") ? pe_group["forcing_modes"] : create_group(pe_group, "forcing_modes") + forcing_group = haskey(pe_group, "ForcingModes") ? pe_group["ForcingModes"] : create_group(pe_group, "ForcingModes") forcing_group["n"] = [mode.n for mode in intr.forcing_modes] forcing_group["m"] = [mode.m for mode in intr.forcing_modes] forcing_group["amplitude"] = [mode.amplitude for mode in intr.forcing_modes] @@ -127,7 +127,7 @@ function write_outputs_to_HDF5( # root-area-weighted field (b̃) space. Recover the area-weighted field b̄ with the stored # operator S ≡ rootarea_to_area_weight (b̄ = S·b̃): e.g. L_b̄ = S·L̃·S†; recover flux with the # scalar surface_area A: Φ = A·b̄ (internally R = S·A, Φ = R·b̃). [Pharr 2026] - mat_group = haskey(pe_group, "response_matrices") ? pe_group["response_matrices"] : create_group(pe_group, "response_matrices") + mat_group = haskey(pe_group, "ResponseMatrices") ? pe_group["ResponseMatrices"] : create_group(pe_group, "ResponseMatrices") !isempty(state.plasma_inductance) && (mat_group["plasma_inductance"] = state.plasma_inductance) !isempty(state.surface_inductance) && (mat_group["surface_inductance"] = state.surface_inductance) !isempty(state.permeability) && (mat_group["permeability"] = state.permeability) @@ -136,7 +136,7 @@ function write_outputs_to_HDF5( (state.surface_area != 0.0) && (mat_group["surface_area"] = state.surface_area) # Response fields (ComplexF64 directly) - response_group = haskey(pe_group, "response") ? pe_group["response"] : create_group(pe_group, "response") + response_group = haskey(pe_group, "Response") ? pe_group["Response"] : create_group(pe_group, "Response") !isempty(state.psi_grid) && (response_group["psi_n"] = state.psi_grid) have_xi = !isnothing(state.xi_modes) have_b = have_xi && !isnothing(state.b_modes) @@ -192,7 +192,7 @@ function write_outputs_to_HDF5( end # Singular coupling - coupling_group = haskey(pe_group, "singular_coupling") ? pe_group["singular_coupling"] : create_group(pe_group, "singular_coupling") + coupling_group = haskey(pe_group, "SingularCoupling") ? pe_group["SingularCoupling"] : create_group(pe_group, "SingularCoupling") # Coupling matrices [n_rational × numpert_total] !isempty(state.C_resonant_area_weighted_field) && (coupling_group["C_resonant_area_weighted_field"] = state.C_resonant_area_weighted_field) @@ -219,7 +219,7 @@ function write_outputs_to_HDF5( !isempty(state.rational_n) && (coupling_group["rational_n"] = state.rational_n) # Energies - energy_group = haskey(pe_group, "energies") ? pe_group["energies"] : create_group(pe_group, "energies") + energy_group = haskey(pe_group, "Energies") ? pe_group["Energies"] : create_group(pe_group, "Energies") energy_group["vacuum_energy"] = state.vacuum_energy energy_group["surface_energy"] = state.surface_energy energy_group["plasma_energy"] = state.plasma_energy diff --git a/src/Rerun.jl b/src/Rerun.jl index 2a5a81237..4eab2a985 100644 --- a/src/Rerun.jl +++ b/src/Rerun.jl @@ -28,14 +28,13 @@ end read_equilibrium_ingest(in_h5) -> EquilibriumIngest Reconstruct the [`DirectIngest`](@ref)/[`InverseIngest`](@ref) stored under -`input/raw_inputs/equilibrium/` (the inverse of the field-by-field write in +`Input/RawInputs/Equilibrium/` (the inverse of the field-by-field write in `write_outputs_to_HDF5`). Returns `nothing` when the group is absent, which marks an analytic equilibrium — replayed from its TOML section rather than stored arrays. """ function read_equilibrium_ingest(in_h5) - group_path = "input/raw_inputs/equilibrium" - haskey(in_h5, group_path) || return nothing - group = in_h5[group_path] + haskey(in_h5, H5_RAW_EQUILIBRIUM) || return nothing + group = in_h5[H5_RAW_EQUILIBRIUM] kind = read(group, "ingest_kind") T = kind == "direct" ? Equilibrium.DirectIngest : kind == "inverse" ? Equilibrium.InverseIngest : @@ -209,29 +208,29 @@ function build_inputs_from_h5(args::Vector{String}) # ignores the frozen forcing-mode snapshot. use_coils = cli.coil_source == "coils" toml_raw, ingest, source_git, preloaded_forcing, preloaded_coils = h5open(source_h5, "r") do in_h5 - haskey(in_h5, "input/gpec_toml_raw") || - error("Source HDF5 $source_h5 has no input/gpec_toml_raw — produced by a pre-rerun version of GPEC") - forcing_modes = if use_coils || !haskey(in_h5, "input/raw_inputs/forcing_terms") + haskey(in_h5, H5_INPUT_TOML) || + error("Source HDF5 $source_h5 has no $H5_INPUT_TOML — produced by a pre-rerun version of GPEC") + forcing_modes = if use_coils || !haskey(in_h5, H5_RAW_FORCING) nothing else modes = ForcingTerms.ForcingMode[] - ForcingTerms.load_forcing_from_h5_group!(modes, in_h5["input/raw_inputs/forcing_terms"]) + ForcingTerms.load_forcing_from_h5_group!(modes, in_h5[H5_RAW_FORCING]) modes end coil_sets = if use_coils - haskey(in_h5, "input/raw_inputs/coils") || - error("--coil-source coils requested but $source_h5 has no input/raw_inputs/coils " * + haskey(in_h5, H5_RAW_COILS) || + error("--coil-source coils requested but $source_h5 has no $H5_RAW_COILS " * "(the source run did not use coils, or predates coil-snapshot support)") sets = ForcingTerms.CoilSet[] - ForcingTerms.load_coils_from_h5_group!(sets, in_h5["input/raw_inputs/coils"]) + ForcingTerms.load_coils_from_h5_group!(sets, in_h5[H5_RAW_COILS]) sets else nothing end ( - read(in_h5, "input/gpec_toml_raw"), + read(in_h5, H5_INPUT_TOML), read_equilibrium_ingest(in_h5), - haskey(in_h5, "info/git_version") ? read(in_h5, "info/git_version") : "unknown", + haskey(in_h5, H5_GIT_VERSION) ? read(in_h5, H5_GIT_VERSION) : "unknown", forcing_modes, coil_sets ) diff --git a/src/Tearing/Runner/HDF5Output.jl b/src/Tearing/Runner/HDF5Output.jl index 107a3be1a..2c1ee6611 100644 --- a/src/Tearing/Runner/HDF5Output.jl +++ b/src/Tearing/Runner/HDF5Output.jl @@ -5,17 +5,17 @@ # top-level GPEC runner wires that up; this file only defines the pure # writer. # -# Output layout (relative to the parent group the caller provides): +# Output layout (relative to the parent group the caller provides; +# settings are not echoed here — inputs live only under Input/): # -# slayer/ -# ├── settings/ -- control snapshot (strings, scalars) -# ├── per_surface/ -- struct-of-arrays for SLAYERParameters fields +# Tearing/ +# ├── PerSurface/ -- struct-of-arrays for SLAYERParameters fields # │ ├── psi, q, q1, ... # │ └── ... -# ├── roots/ -- Q_root (real, imag), omega_Hz, gamma_Hz -# ├── diagnostics/ -- all_valid_roots, poles, filtered_roots +# ├── Roots/ -- Q_root (real, imag), omega_Hz, gamma_Hz +# ├── Diagnostics/ -- ValidRoots, Poles, FilteredRoots # │ (flat-plus-offsets ragged encoding) -# └── scan/ -- optional: full Q/Δ scan data +# └── Scan/ -- optional: full Q/Δ scan data using HDF5 @@ -23,21 +23,20 @@ using HDF5 write_slayer_hdf5!(parent::Union{HDF5.File,HDF5.Group}, result::SLAYERResult) -Write `result` into a `slayer/` subgroup of `parent`. The subgroup is +Write `result` into a `Tearing/` subgroup of `parent`. The subgroup is created if missing and overwritten if it already exists (keeps the output file reproducible across reruns). """ function write_slayer_hdf5!(parent::Union{HDF5.File,HDF5.Group}, result::SLAYERResult) - if haskey(parent, "slayer") - delete_object(parent, "slayer") + if haskey(parent, "Tearing") + delete_object(parent, "Tearing") end - g = create_group(parent, "slayer") + g = create_group(parent, "Tearing") g["enabled"] = Int(result.enabled) result.enabled || return g # nothing else to write - _write_settings!(g, result.control) _write_per_surface!(g, result.params, result.dp_matrix) _write_roots!(g, result) _write_layer_widths!(g, result.layer_widths) @@ -48,53 +47,10 @@ function write_slayer_hdf5!(parent::Union{HDF5.File,HDF5.Group}, return g end -# ---------- settings snapshot ---------- -function _write_settings!(g, ctrl::SLAYERControl) - s = create_group(g, "settings") - s["inner_model"] = String(ctrl.inner_model) - s["scan_mode"] = String(ctrl.scan_mode) - s["coupling_mode"] = String(ctrl.coupling_mode) - s["dc_type"] = String(ctrl.dc_type) - s["msing_max"] = ctrl.msing_max - s["bt"] = ctrl.bt === nothing ? NaN : ctrl.bt - s["mu_i"] = ctrl.mu_i - s["zeff"] = ctrl.zeff - s["chi_perp"] = ctrl.chi_perp - s["chi_tor"] = ctrl.chi_tor - # NaN sentinel records the auto-derive (nothing) setting in a numeric dataset. - s["dr_val"] = ctrl.dr_val === nothing ? NaN : ctrl.dr_val - s["dgeo_val"] = ctrl.dgeo_val === nothing ? NaN : ctrl.dgeo_val - s["theta_sample"] = ctrl.theta_sample - s["resistivity_model"] = String(ctrl.resistivity_model) - s["lnLambda_form"] = String(ctrl.lnLambda_form) - s["Q_re_range"] = collect(ctrl.Q_re_range) - s["Q_im_range"] = collect(ctrl.Q_im_range) - s["nre"] = ctrl.nre - s["nim"] = ctrl.nim - s["amr_passes"] = ctrl.amr_passes - s["amr_max_cells"] = ctrl.amr_max_cells - # Multi-box stripe layout: flatten Vector{NTuple{4}} to an N×4 matrix - # (empty → 0×4) so a rerun can reconstruct the exact scan boxes. - s["boxes"] = isempty(ctrl.boxes) ? Matrix{Float64}(undef, 0, 4) : - permutedims(reduce(hcat, collect.(ctrl.boxes))) - s["multi_box_prescreen_n"] = ctrl.multi_box_prescreen_n - s["pole_threshold"] = ctrl.pole_threshold - s["pole_threshold_adaptive"] = Int(ctrl.pole_threshold_adaptive) - s["filter_above_poles"] = Int(ctrl.filter_above_poles) - s["filter_outside_re"] = Int(ctrl.filter_outside_re) - s["gap_kHz_threshold"] = ctrl.gap_kHz_threshold - s["polish_roots"] = Int(ctrl.polish_roots) - s["validity_rtol"] = ctrl.validity_rtol - s["profile_file"] = ctrl.profile_file - s["profile_group"] = ctrl.profile_group - s["store_scan"] = Int(ctrl.store_scan) - return nothing -end - # ---------- per-surface layer parameters ---------- function _write_per_surface!(g, params::AbstractVector{SLAYERParameters}, dp_matrix::Matrix{ComplexF64}) - ps = create_group(g, "per_surface") + ps = create_group(g, "PerSurface") # Scalar struct-of-arrays for all Float64 / Int fields for fname in (:ising, :m, :n) @@ -111,7 +67,7 @@ function _write_per_surface!(g, params::AbstractVector{SLAYERParameters}, ps["dc_type"] = String[String(p.dc_type) for p in params] # Full Δ' matrix, split real/imag - dp = create_group(ps, "dp_matrix") + dp = create_group(ps, "DpMatrix") dp["real"] = real.(dp_matrix) dp["imag"] = imag.(dp_matrix) return nothing @@ -121,12 +77,12 @@ end # and resistive/Alfvén times rather than the SLAYER dimensionless set. function _write_per_surface!(g, params::AbstractVector{GGJParameters}, dp_matrix::Matrix{ComplexF64}) - ps = create_group(g, "per_surface") + ps = create_group(g, "PerSurface") ps["ising"] = Int[p.ising for p in params] for fname in (:E, :F, :G, :H, :K, :M, :taua, :taur, :v1) ps[String(fname)] = Float64[getfield(p, fname) for p in params] end - dp = create_group(ps, "dp_matrix") + dp = create_group(ps, "DpMatrix") dp["real"] = real.(dp_matrix) dp["imag"] = imag.(dp_matrix) return nothing @@ -134,7 +90,7 @@ end # ---------- eigenvalue roots ---------- function _write_roots!(g, r::SLAYERResult) - roots = create_group(g, "roots") + roots = create_group(g, "Roots") roots["Q_root_real"] = real.(r.Q_root) roots["Q_root_imag"] = imag.(r.Q_root) roots["omega_Hz"] = r.omega_Hz @@ -151,7 +107,7 @@ end # ---------- resistive layer thickness (del_s Riccati) ---------- function _write_layer_widths!(g, widths::Vector{LayerWidths}) - lw = create_group(g, "layer_widths") + lw = create_group(g, "LayerWidths") for fname in (:ising, :m, :n) lw[String(fname)] = Int[getfield(w, fname) for w in widths] end @@ -168,7 +124,7 @@ end # ---------- diagnostics: valid roots, poles, filtered roots ---------- function _write_diagnostics!(g, r::SLAYERResult) - diag = create_group(g, "diagnostics") + diag = create_group(g, "Diagnostics") # Uncoupled: one GrowthRateResult per surface. Coupled: one total. extractions = if r.coupled_extraction !== nothing [r.coupled_extraction] @@ -176,11 +132,11 @@ function _write_diagnostics!(g, r::SLAYERResult) r.per_surface_extraction end - _write_ragged_complex!(diag, "valid_roots", + _write_ragged_complex!(diag, "ValidRoots", [gr.valid_roots for gr in extractions]) - _write_ragged_complex!(diag, "poles", + _write_ragged_complex!(diag, "Poles", [gr.poles for gr in extractions]) - _write_ragged_complex!(diag, "filtered_roots", + _write_ragged_complex!(diag, "FilteredRoots", [gr.filtered_roots for gr in extractions]) return nothing end @@ -207,9 +163,9 @@ end # ---------- full scan data (optional) ---------- function _write_scan_data!(g, r::SLAYERResult) - sc = create_group(g, "scan") + sc = create_group(g, "Scan") for (k, data) in enumerate(r.scan_data) - sk = create_group(sc, "surface_$(k)") + sk = create_group(sc, "Surface_$(k)") _write_single_scan!(sk, data) end return nothing diff --git a/src/Tearing/Runner/Runner.jl b/src/Tearing/Runner/Runner.jl index 49edcb4bd..919088065 100644 --- a/src/Tearing/Runner/Runner.jl +++ b/src/Tearing/Runner/Runner.jl @@ -19,7 +19,7 @@ # find_growth_rates # │ # ▼ -# SLAYERResult → HDF5 (`slayer/` group) +# SLAYERResult → HDF5 (`Tearing/` group) module Runner diff --git a/test/runtests.jl b/test/runtests.jl index 2036e610c..76eca69d1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -49,4 +49,5 @@ else include("./runtests_coils.jl") include("./runtests_imas.jl") include("./runtests_rerun_from_h5.jl") + include("./runtests_h5_schema.jl") end diff --git a/test/runtests_coils.jl b/test/runtests_coils.jl index c1be63cf6..8525ce70a 100644 --- a/test/runtests_coils.jl +++ b/test/runtests_coils.jl @@ -541,11 +541,11 @@ end mktempdir() do dir path = joinpath(dir, "snap.h5") HDF5.h5open(path, "w") do f - ForcingTerms.save_coils_to_h5(sets_in, HDF5.create_group(f, "input/raw_inputs/coils")) + ForcingTerms.save_coils_to_h5(sets_in, HDF5.create_group(f, "Input/RawInputs/Coils")) end sets_out = ForcingTerms.CoilSet[] HDF5.h5open(path, "r") do f - ForcingTerms.load_coils_from_h5_group!(sets_out, f["input/raw_inputs/coils"]) + ForcingTerms.load_coils_from_h5_group!(sets_out, f["Input/RawInputs/Coils"]) end @test length(sets_out) == 2 byname = Dict(s.name => s for s in sets_out) diff --git a/test/runtests_fullruns.jl b/test/runtests_fullruns.jl index b3f8daa82..ce6470385 100644 --- a/test/runtests_fullruns.jl +++ b/test/runtests_fullruns.jl @@ -25,7 +25,7 @@ using HDF5 # not numeric regression — the mode-converged physical value is pinned in the # regression harness (examples/Solovev_kinetic_calculated_example). Assert only # nerfed-grid-robust facts: finite and positive (Solovev is stable). - et = read(h5["FreeBoundaryStability/eigenmode_energies"]) + et = read(h5["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) @test isfinite(real(et[1])) @test real(et[1]) > 0 end @@ -43,7 +43,7 @@ using HDF5 # harness on the mode-converged deck, not here — et[1] is a near-marginal, # ill-conditioned, FP-reassociation-sensitive quantity on this grid (sign not even # robust across platforms), so no value is pinned. - et = read(h5["FreeBoundaryStability/eigenmode_energies"]) + et = read(h5["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) @test isfinite(real(et[1])) end rm(joinpath(ex4, "gpec.h5"); force=true) @@ -55,7 +55,7 @@ using HDF5 @test begin GeneralizedPerturbedEquilibrium.main([ex5]) h5open(joinpath(ex5, "gpec.h5"), "r") do h5 - et = read(h5["FreeBoundaryStability/eigenmode_energies"]) + et = read(h5["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) # Smoke test (nerfed mpsi=16, delta_m=0 deck): exercises the full self-consistent # KF→FFS kinetic-MHD path end-to-end. NO numeric value is pinned here — the prior # imag(et[1]) ≈ -0.711 rtol=0.08 pin was platform-fragile (failed on macOS aarch64 @@ -78,7 +78,7 @@ using HDF5 @test begin GeneralizedPerturbedEquilibrium.main([ex6]) h5open(joinpath(ex6, "gpec.h5"), "r") do h5 - et = read(h5["FreeBoundaryStability/eigenmode_energies"]) + et = read(h5["ForceFreeStates/FreeBoundaryStability/eigenmode_energies"]) # Smoke test (nerfed mpsi=16, delta_m=0 deck): exercises the collisionless # (nutype="zero") real-x-space energy-integral path end-to-end — the #281 fix — # without faulting/NaN (the bug this guards against). The precise collisionless diff --git a/test/runtests_h5_schema.jl b/test/runtests_h5_schema.jl new file mode 100644 index 000000000..4ad698b13 --- /dev/null +++ b/test/runtests_h5_schema.jl @@ -0,0 +1,66 @@ +using HDF5 + +# Schema-naming guard for gpec.h5 (anti-drift enforcement for +# docs/development/hdf5-conventions.md): every group-path component must be +# CamelCase or a whitelisted data-driven token. Dataset (leaf) names are not +# constrained here; the metadata contract is asserted separately. + +_is_camelcase(name) = occursin(r"^[A-Z][A-Za-z0-9]*$", name) + +# Data-driven group names are stored verbatim: anything inside the Input/ raw +# snapshot (coil-set names, ingest layout), KineticForces method tokens, and +# Scan surface indices. +function _group_name_ok(parent_path, name) + startswith(parent_path, "Input/") && return true + parent_path == "KineticForces" && return true + occursin(r"^Surface_\d+$", name) && return true + return _is_camelcase(name) +end + +function _collect_bad_groups(h5) + bad = String[] + function walk(node, prefix) + for k in keys(node) + child = node[k] + child isa HDF5.Group || continue + full = isempty(prefix) ? k : prefix * "/" * k + _group_name_ok(prefix, k) || push!(bad, full) + walk(child, full) + end + end + walk(h5, "") + return bad +end + +@testset "gpec.h5 schema naming" begin + template_dir = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example") + + mktempdir() do run_dir + for name in readdir(template_dir) + cp(joinpath(template_dir, name), joinpath(run_dir, name)) + end + toml_path = joinpath(run_dir, "gpec.toml") + write(toml_path, replace(read(toml_path, String), "write_outputs_to_HDF5 = false" => "write_outputs_to_HDF5 = true")) + + GeneralizedPerturbedEquilibrium.main([run_dir]) + h5_path = joinpath(run_dir, "gpec.h5") + @test isfile(h5_path) + + h5open(h5_path, "r") do h5 + bad = _collect_bad_groups(h5) + isempty(bad) || @error "non-CamelCase group paths in gpec.h5" bad + @test isempty(bad) + + # Retired/renamed legacy top-level groups must not reappear. + for legacy in ("info", "input", "equil", "splines", "integration", "locstab", + "singular", "matrices", "kinetic", "galerkin", "slayer", "kinetic_forces", + "perturbed_equilibrium", "vacuum", "FreeBoundaryStability", "EdgeScan") + @test !haskey(h5, legacy) + end + + # Inputs live only under Input/; spot-check the rerun-critical paths. + @test haskey(h5, "Input/gpec_toml_raw") + @test haskey(h5, "Info/git_version") + end + end +end diff --git a/test/runtests_parallel_integration.jl b/test/runtests_parallel_integration.jl index 307615f2d..1bd89d266 100644 --- a/test/runtests_parallel_integration.jl +++ b/test/runtests_parallel_integration.jl @@ -265,7 +265,7 @@ using TOML @test isapprox(et_par, et_std; rtol=0.02) # Per-surface Δ' assertions were removed: per-surface Δ' is a stub calculation # left in the code for future work but no longer reported, output, or tested. - # The STRIDE BVP Δ' matrix (`singular/delta_prime_matrix`) is the canonical + # The STRIDE BVP Δ' matrix (`SingularSurfaces/delta_prime_matrix`) is the canonical # Δ', regression-tested via the DIIID-like fixture which has well-conditioned # values; Solovev is near marginal stability and BVP Δ' is pathological there. end diff --git a/test/runtests_rerun_from_h5.jl b/test/runtests_rerun_from_h5.jl index 12344f5b6..2e364f2df 100644 --- a/test/runtests_rerun_from_h5.jl +++ b/test/runtests_rerun_from_h5.jl @@ -6,11 +6,11 @@ using TOML # for the direct (EFIT) and inverse (CHEASE) equilibrium kinds and the override flags. # Collect every leaf dataset path under an open HDF5 file, skipping the groups/paths that -# legitimately differ between a source run and its replay (`input/` is re-emitted with the -# rerun's own filename/TOML blob; `info/git_version` reflects the running commit). +# legitimately differ between a source run and its replay (`Input/` is re-emitted with the +# rerun's own filename/TOML blob; `Info/git_version` reflects the running commit). function _rerun_leaf_paths(h5) - skip_toplevel = Set(["input"]) - skip_paths = Set(["info/git_version"]) + skip_toplevel = Set(["Input"]) + skip_paths = Set(["Info/git_version"]) paths = String[] function walk(node, prefix) for k in keys(node) @@ -38,7 +38,7 @@ function _rerun_dataset_mismatches(source_h5, rerun_h5) for p in union(src, rer) n_compared += 1 # `isequal` (not `!=`) so a faithfully-reproduced NaN counts as a match — some - # datasets (e.g. ballooning `locstab/alpha_critical` where no boundary exists) are + # datasets (e.g. ballooning `LocalStability/alpha_critical` where no boundary exists) are # legitimately all-NaN, and `NaN != NaN` would otherwise flag them as drift. if !(p in src) || !(p in rer) || !isequal(read(hs, p), read(hr, p)) n_mismatched += 1 @@ -102,7 +102,7 @@ end @testset "--coil-source coils errors when source has no coil snapshot" begin # This Solovev source run used ASCII forcing, so there is no - # input/raw_inputs/coils group to replay from. + # Input/RawInputs/Coils group to replay from. mktempdir() do replay_dir @test_throws ErrorException GeneralizedPerturbedEquilibrium.main([ source_h5, "--output-dir", replay_dir, "--coil-source", "coils" @@ -123,7 +123,7 @@ end read_resfield(path) = h5open(path, "r") do h5 - key = "perturbed_equilibrium/singular_coupling/resonant_area_weighted_field" + key = "PerturbedEquilibrium/SingularCoupling/resonant_area_weighted_field" haskey(h5, key) ? read(h5, key) : ComplexF64[] end @@ -158,8 +158,8 @@ end # The coil geometry actually used must be captured in the snapshot. h5open(source_h5, "r") do h5 - @test haskey(h5, "input/raw_inputs/coils") - @test haskey(h5, "input/raw_inputs/coils/my_coils") + @test haskey(h5, "Input/RawInputs/Coils") + @test haskey(h5, "Input/RawInputs/Coils/my_coils") end src_field = read_resfield(source_h5) @test !isempty(src_field) @@ -197,9 +197,9 @@ function _roundtrip_ingest(ingest, kind) mktempdir() do d h5path = joinpath(d, "raw.h5") h5open(h5path, "w") do f - f["input/raw_inputs/equilibrium/ingest_kind"] = kind + f["Input/RawInputs/Equilibrium/ingest_kind"] = kind for nm in fieldnames(typeof(ingest)) - f["input/raw_inputs/equilibrium/$nm"] = getfield(ingest, nm) + f["Input/RawInputs/Equilibrium/$nm"] = getfield(ingest, nm) end end restored = h5open(GeneralizedPerturbedEquilibrium.read_equilibrium_ingest, h5path, "r") diff --git a/test/runtests_slayer_runner.jl b/test/runtests_slayer_runner.jl index c3ab86806..f74d774c9 100644 --- a/test/runtests_slayer_runner.jl +++ b/test/runtests_slayer_runner.jl @@ -174,45 +174,40 @@ write_slayer_hdf5!(f, r) end h5open(path, "r") do f - g = f["slayer"] + g = f["Tearing"] @test haskey(g, "enabled") && read(g["enabled"]) == 1 - @test haskey(g, "settings") - @test haskey(g, "per_surface") - @test haskey(g, "roots") - @test haskey(g, "diagnostics") - @test haskey(g, "scan") - - # Settings round-trip - @test read(g["settings/inner_model"]) == "slayer_fitzpatrick" - @test read(g["settings/scan_mode"]) == "brute_force" - @test read(g["settings/coupling_mode"]) == "coupled" - @test read(g["settings/nre"]) == 40 + # Settings are not echoed — inputs live only under Input/ (the merged TOML). + @test !haskey(g, "Settings") + @test haskey(g, "PerSurface") + @test haskey(g, "Roots") + @test haskey(g, "Diagnostics") + @test haskey(g, "Scan") # Per-surface arrays have the right length - @test length(read(g["per_surface/ising"])) == 2 - @test read(g["per_surface/ising"]) == [1, 2] - @test read(g["per_surface/lu"])[1] ≈ 1.0e7 - @test read(g["per_surface/lu"])[2] ≈ 2.0e7 + @test length(read(g["PerSurface/ising"])) == 2 + @test read(g["PerSurface/ising"]) == [1, 2] + @test read(g["PerSurface/lu"])[1] ≈ 1.0e7 + @test read(g["PerSurface/lu"])[2] ≈ 2.0e7 # Roots arrays - @test length(read(g["roots/Q_root_real"])) == 1 # coupled - @test length(read(g["roots/omega_Hz"])) == 1 + @test length(read(g["Roots/Q_root_real"])) == 1 # coupled + @test length(read(g["Roots/omega_Hz"])) == 1 # Layer-thickness diagnostic: one entry per surface, with # the physical thickness [m] and the drift scale. - @test length(read(g["layer_widths/delta_s_m"])) == 2 - @test all(read(g["layer_widths/delta_s_m"]) .>= 0) - @test haskey(g["layer_widths"], "dels_db_real") - @test haskey(g["layer_widths"], "d_beta") + @test length(read(g["LayerWidths/delta_s_m"])) == 2 + @test all(read(g["LayerWidths/delta_s_m"]) .>= 0) + @test haskey(g["LayerWidths"], "dels_db_real") + @test haskey(g["LayerWidths"], "d_beta") # Ragged diagnostics use flat+offsets encoding - @test haskey(g["diagnostics/valid_roots"], "flat_real") - @test haskey(g["diagnostics/valid_roots"], "flat_imag") - @test haskey(g["diagnostics/valid_roots"], "offsets") + @test haskey(g["Diagnostics/ValidRoots"], "flat_real") + @test haskey(g["Diagnostics/ValidRoots"], "flat_imag") + @test haskey(g["Diagnostics/ValidRoots"], "offsets") # Scan group present (store_scan=true) - @test haskey(g, "scan/surface_1") - @test read(g["scan/surface_1/kind"]) == "brute_force" + @test haskey(g, "Scan/Surface_1") + @test read(g["Scan/Surface_1/kind"]) == "brute_force" end end end @@ -226,10 +221,10 @@ write_slayer_hdf5!(f, r) end h5open(path, "r") do f - g = f["slayer"] + g = f["Tearing"] @test read(g["enabled"]) == 0 - @test !haskey(g, "settings") # no further groups - @test !haskey(g, "per_surface") + @test !haskey(g, "PerSurface") # no further groups + @test !haskey(g, "Roots") end end end diff --git a/test/test_data/regression_solovev_ideal_example/gpec.toml b/test/test_data/regression_solovev_ideal_example/gpec.toml index e6a97d2ec..a16ab6898 100644 --- a/test/test_data/regression_solovev_ideal_example/gpec.toml +++ b/test/test_data/regression_solovev_ideal_example/gpec.toml @@ -46,7 +46,7 @@ singfac_min = 1e-4 # Fractional distance from rational q at which ide ucrit = 1e3 # Column-norm threshold that triggers solution renormalization # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = true # Append serial-EL pass so dense ξ is stored — REQUIRED with a [PerturbedEquilibrium] section set_psilim_via_dmlim = false # FALSE for limited/analytical equilibria — rationals sparse, dmlim would chop too much edge diff --git a/test/test_data/regression_solovev_kinetic_example/gpec.toml b/test/test_data/regression_solovev_kinetic_example/gpec.toml index 4d8f03108..1c020a086 100644 --- a/test/test_data/regression_solovev_kinetic_example/gpec.toml +++ b/test/test_data/regression_solovev_kinetic_example/gpec.toml @@ -46,7 +46,7 @@ singfac_min = 1e-4 # Fractional distance from rational q at which ide ucrit = 1e3 # Column-norm threshold that triggers solution renormalization # Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details) -use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix) +use_parallel = true # Run parallel FM-propagator BVP path (unlocks SingularSurfaces/delta_prime_matrix) parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) populate_dense_xi = true # Append serial-EL pass so dense ξ is stored — REQUIRED with a [PerturbedEquilibrium] section set_psilim_via_dmlim = false # FALSE for limited/analytical equilibria — rationals sparse, dmlim would chop too much edge From 2b5967db3e0c397953cc58cbb2200683c4f5ff3b Mon Sep 17 00:00:00 2001 From: logan-nc Date: Fri, 14 Aug 2026 15:31:09 -0400 Subject: [PATCH 2/3] ALL - CLEANUP - Independent-review fixes for the schema rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix the last stale legacy-path tokens: the user-visible serial-EL @info log and a comment in Riccati.jl still said integration/xi_*; the populate_dense_xi docstring and use_parallel comment cited dead integration/singular/vacuum paths (docstring also trimmed to house style). - Repoint four dead tracked h5paths in gal_resistive_diiid's PE sibling case (gal_resistive_pe): resonant_flux/penetrated_field/C_resonant_flux leaves were renamed to *_area_weighted_field in the Pharr field-representation change but this case was missed then — they extracted as permanently missing. Labels updated to field terminology; quantity names unchanged. - Unit-test the data-driven whitelist in _group_name_ok directly (method tokens, Surface_, Input/ raw names) — the full-run walk only exercises an ideal deck where none of those branches fire. - Scope the naming rules in hdf5-conventions.md to gpec.h5 (the harness's synthetic ggj/* fixture is not subject to them). - Nits: ASCII-tree column alignment in the PE writer docstring; modernize a retired-path comment in diiid_n1.toml and drop its stale PR reference. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz --- docs/development/hdf5-conventions.md | 2 ++ regression-harness/cases/diiid_n1.toml | 5 ++--- .../cases/gal_resistive_pe.toml | 20 +++++++++---------- src/ForceFreeStates/ForceFreeStatesStructs.jl | 4 ++-- src/ForceFreeStates/Riccati.jl | 4 ++-- src/PerturbedEquilibrium/Utils.jl | 2 +- test/runtests_h5_schema.jl | 11 ++++++++++ 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/development/hdf5-conventions.md b/docs/development/hdf5-conventions.md index f62f8777d..f43854cde 100644 --- a/docs/development/hdf5-conventions.md +++ b/docs/development/hdf5-conventions.md @@ -13,6 +13,8 @@ Physics-topic groups elevated to top level (rather than nested under their produ ## Naming rules +These rules govern `gpec.h5` (and any future GPEC-produced HDF5 output); harness-internal synthetic fixtures (e.g. the `ggj/*` reference files written by `regression-harness/src/runner.jl`) are out of scope. + - **Groups are CamelCase at every level** (`ForceFreeStates/`, `PerSurface/`, `GalerkinDeltaPrime/`). - **Datasets (leaves) are snake_case** (`eigenmode_energies`, `delta_prime_matrix`). Established physics symbols keep their natural case (`E`, `F`, `Q_root_real`, `pest3_Delta`, `2piF`). - **Data-driven tokens are stored verbatim**: coil-set names under `Input/RawInputs/Coils/`, KineticForces method tokens (`fgar`, …), scan indices (`Surface_`, `psi_`). diff --git a/regression-harness/cases/diiid_n1.toml b/regression-harness/cases/diiid_n1.toml index 5827b6942..6b1161ab6 100644 --- a/regression-harness/cases/diiid_n1.toml +++ b/regression-harness/cases/diiid_n1.toml @@ -271,10 +271,9 @@ noise_threshold = 0 order = 61 # Tearing stability Δ' — canonical STRIDE BVP matrix diagonal (replaces the -# previous `perturbed_equilibrium/singular_coupling/delta_prime` track, which +# previous `PerturbedEquilibrium/SingularCoupling/delta_prime` track, which # was a per-surface stub computed by SingularCoupling from (rbwp1-lbwp1)/(2π·χ'). -# Per-surface Δ' is now de-emphasized — see PR 178 notes — and SingularCoupling -# instead reads this BVP matrix diagonal. +# Per-surface Δ' is de-emphasized; SingularCoupling reads this BVP matrix diagonal. [quantities.delta_prime] h5path = "SingularSurfaces/delta_prime_matrix" type = "complex_matrix" diff --git a/regression-harness/cases/gal_resistive_pe.toml b/regression-harness/cases/gal_resistive_pe.toml index 8158c41e0..a9623f92f 100644 --- a/regression-harness/cases/gal_resistive_pe.toml +++ b/regression-harness/cases/gal_resistive_pe.toml @@ -40,29 +40,29 @@ label = "Chirikov parameter" noise_threshold = 1e-8 order = 21 -# Resonant flux Φ_res per surface (applied) — norm + per-surface complex values +# Resonant area-weighted field b̄^r = Φ^r/A^r per surface (tesla) — norm + per-surface complex values [quantities.pe_resonant_flux_norm] -h5path = "PerturbedEquilibrium/SingularCoupling/resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_area_weighted_field" type = "complex_vector" extract = "norm" -label = "||resonant flux||" +label = "||resonant b field||" noise_threshold = 1e-6 order = 30 [quantities.pe_resonant_flux_all] -h5path = "PerturbedEquilibrium/SingularCoupling/resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_area_weighted_field" type = "complex_vector" extract = "all_complex" -label = "resonant flux Phi_res" +label = "resonant b field (all)" noise_threshold = 1e-8 order = 31 # Penetrated (reconnected) resonant field per surface — the resistive shielding signature [quantities.pe_penetrated_field] -h5path = "PerturbedEquilibrium/SingularCoupling/penetrated_field" +h5path = "PerturbedEquilibrium/SingularCoupling/penetrated_area_weighted_field" type = "complex_vector" extract = "all_complex" -label = "penetrated field" +label = "penetrated b field" noise_threshold = 1e-8 order = 32 @@ -75,12 +75,12 @@ label = "PE Δ' per surface" noise_threshold = 1e-8 order = 33 -# Resonant-flux coupling matrix (n_rational × numpert_total) — Frobenius norm, catches broader drift +# Resonant-field coupling matrix (n_rational × numpert_total) — Frobenius norm, catches broader drift [quantities.pe_C_resonant_flux_norm] -h5path = "PerturbedEquilibrium/SingularCoupling/C_resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/C_resonant_area_weighted_field" type = "complex_matrix" extract = "norm" -label = "||C resonant flux||" +label = "||C resonant b field||" noise_threshold = 1e-6 order = 40 diff --git a/src/ForceFreeStates/ForceFreeStatesStructs.jl b/src/ForceFreeStates/ForceFreeStatesStructs.jl index d9c4770d2..c495619f1 100644 --- a/src/ForceFreeStates/ForceFreeStatesStructs.jl +++ b/src/ForceFreeStates/ForceFreeStatesStructs.jl @@ -256,7 +256,7 @@ gpec.toml. - `use_riccati::Bool` - Use the dual Riccati reformulation S = U₁·U₂⁻¹ instead of the standard U₁/U₂ ODE. Reduces stiffness for faster integration. See Glasser (2018) Phys. Plasmas 25, 032507. - `use_parallel::Bool` - Parallel fundamental matrix (propagator) integration using `Threads.@threads`. Each chunk is integrated independently from identity IC and assembled serially. Requires `singfac_min != 0`. Uses the same chunk bounds as the standard path but sub-divides chunks for load balancing. Crossings use the Riccati-style algorithm (no Gaussian reduction). - `parallel_threads::Int` - Cap on the number of threads the parallel BVP uses. **Default `2`** parallelises the FM chunks across two threads (the BVP has ~10 chunks; 2 threads is enough to amortize them — speedup saturates here, raising to 4 adds scheduling overhead). Set `parallel_threads = 1` to run the FM chunks SERIALLY (no `Threads.@threads`), which is bit-deterministic and immune to the thread-schedule sensitivity that can cause intermittent BVP divergence on numerically delicate equilibria. The parallel path produces bit-identical Δ′ across thread counts; `parallel_threads = 2` is about 20% faster than serial and saturates the speedup. If a parallel run diverges, drop to `parallel_threads = 1` rather than switching `use_parallel = false` — the latter is silently wrong. Capped at `Threads.nthreads()`. - - `populate_dense_xi::Bool` - When `use_parallel = true`, append a serial Euler-Lagrange pass at the end of the propagator BVP and let it replace the `odet` returned to the main pipeline. This populates `u_store` / `du_store` / `xi_s_store` densely in the axis (EL) basis — the only convention the PerturbedEquilibrium / FieldReconstruction downstream code consumes correctly. Without it the parallel path stores only chunk-endpoint Riccati S matrices with diagnostic derivatives (see Riccati.jl docstring caveats), and HDF5 `integration/xi_psi`/`dxi_psi`/`xi_s` are unusable. Δ' (`singular/delta_prime_matrix`) is computed from the parallel BVP and is bit-identical between `populate_dense_xi=true` and `false`. Energies (`vacuum/ep`/`ev`/`et`) are computed by `free_run` from `odet`, so with `populate_dense_xi=true` they match what a pure serial run (`use_parallel=false`) would produce; with `populate_dense_xi=false` they use the parallel-pass Riccati `odet.u` instead (differs by the ~0.12 % Riccati-vs-axis algorithmic gap on DIIID-class cases). **Default `false`** to avoid paying the dense-pass cost on Δ'/vacuum/ideal-stability-only runs; **PerturbedEquilibrium-using configs must set `populate_dense_xi = true` explicitly** when `use_parallel = true` (otherwise PE silently reads Riccati-basis garbage). Auto-disabled when `force_termination = true` regardless of the user setting, since the dense pass has no downstream consumer in that case. Approximate cost when enabled: one extra serial EL integration (~1× the parallel BVP wall-clock for typical N). + - `populate_dense_xi::Bool` - When `use_parallel = true`, append a serial Euler-Lagrange pass after the propagator BVP so the returned `odet` carries dense axis-basis `u_store`/`du_store`/`xi_s_store` — the only convention PerturbedEquilibrium / FieldReconstruction consume correctly, and what fills HDF5 `ForceFreeStates/Solutions/ForwardIntegration/xi_*`. Δ' (`SingularSurfaces/delta_prime_matrix`) comes from the BVP and is identical either way; free-boundary energies (`ForceFreeStates/FreeBoundaryStability/eigenmode_*`) match a pure serial run when enabled (~0.12 % Riccati-vs-axis gap otherwise). **Default `false`** (skips the extra serial pass, ~1× BVP wall-clock); **PerturbedEquilibrium-using configs must set it `true`** when `use_parallel = true`, else PE reads Riccati-basis garbage. Auto-disabled when `force_termination = true`. - `extended_precision_bvp::Bool` - When `true` (default), promote the Δ' BVP linear system to `Complex{Double64}` (~31 digits) for the LU solve and PEST3 combination. Guards against catastrophic cancellation in the PEST3 four-term combination (dp_raw entries can be 10⁴–10⁵× larger than the result; the imaginary part of off-diagonal Δ' is particularly sensitive). Disabling (`false`) saves ~1.5–2× the BVP solve time but on DIIID-class equilibria the imaginary Δ' components can drift by factors of 2–5×; only disable for performance experiments on cases where Float64 has been validated against Double64. """ @kwdef struct ForceFreeStatesControl @@ -295,7 +295,7 @@ gpec.toml. save_interval::Int = 3 force_termination::Bool = false use_riccati::Bool = false - use_parallel::Bool = true # Default on: unlocks singular/delta_prime_matrix (STRIDE BVP Δ' matrix) used by SLAYER/GGJ downstream. + use_parallel::Bool = true # Default on: unlocks SingularSurfaces/delta_prime_matrix (STRIDE BVP Δ′ matrix) used by SLAYER/GGJ downstream. populate_dense_xi::Bool = false # When use_parallel=true, set to true ONLY if a PerturbedEquilibrium pipeline will consume dense ξ. Default false avoids the ~1× parallel-BVP serial-EL re-run for non-PE runs (Δ'/vacuum/ideal-stability only). See ForceFreeStatesControl docstring for the full trade-off (et[1] convention differs by ~0.12% on DIIID between populate=true vs false). extended_precision_bvp::Bool = true # Promote Δ' BVP to Complex{Double64}; default on (Float64 drifts the imaginary Δ' by 2–5× on DIIID-class cases). diff --git a/src/ForceFreeStates/Riccati.jl b/src/ForceFreeStates/Riccati.jl index 9e86aadcf..812bd5c6a 100644 --- a/src/ForceFreeStates/Riccati.jl +++ b/src/ForceFreeStates/Riccati.jl @@ -1676,7 +1676,7 @@ function parallel_eulerlagrange_integration( odet.nzero = evaluate_stability_criterion!(odet, equil.profiles) transform_u!(odet, intr) # no-op when ifix=0 (no Gaussian reduction) - # Replace BVP `odet` with a dense serial-EL pass so HDF5 `integration/xi_*` carries + # Replace BVP `odet` with a dense serial-EL pass so HDF5 `ForwardIntegration/xi_*` carries # valid DCON ξ in axis basis for PerturbedEquilibrium. Skipped when force_termination=true. if ctrl.populate_dense_xi && !ctrl.force_termination odet = _populate_dense_xi_via_serial_el!(odet, ctrl, equil, ffit, intr) @@ -1951,7 +1951,7 @@ function _populate_dense_xi_via_serial_el!( ) if ctrl.verbose - @info " S → ξ: serial EL dense pass for HDF5 integration/xi_*" + @info " S → ξ: serial EL dense pass for HDF5 ForceFreeStates/Solutions/ForwardIntegration/xi_*" end # Run the serial branch but suppress logging diff --git a/src/PerturbedEquilibrium/Utils.jl b/src/PerturbedEquilibrium/Utils.jl index 418a1ddd7..9f14bd457 100644 --- a/src/PerturbedEquilibrium/Utils.jl +++ b/src/PerturbedEquilibrium/Utils.jl @@ -69,7 +69,7 @@ PerturbedEquilibrium/ │ ├── xi_n # Physical normal displacement xi_n (ComplexF64 [npsi, mpert]) │ ├── b_theta │ └── b_zeta -├── ResponseMatrices/ # [numpert_total × numpert_total], root-area-weighted field (b̃) space; R = S·A +├── ResponseMatrices/ # [numpert_total × numpert_total], root-area-weighted field (b̃) space; R = S·A │ ├── plasma_inductance # Λ̃ = R⁻¹·Λ·R⁻† │ ├── surface_inductance # L̃ = R⁻¹·L·R⁻† │ ├── permeability # P̃ = R⁻¹·P·R (P = Λ·L⁻¹) diff --git a/test/runtests_h5_schema.jl b/test/runtests_h5_schema.jl index 4ad698b13..d0d7c66c4 100644 --- a/test/runtests_h5_schema.jl +++ b/test/runtests_h5_schema.jl @@ -32,6 +32,17 @@ function _collect_bad_groups(h5) return bad end +# The full-run walk below only exercises an ideal deck, which never produces the +# data-driven group names — pin the whitelist rules directly. +@testset "gpec.h5 schema naming: group-name rule" begin + @test _group_name_ok("KineticForces", "fgar") # method tokens verbatim + @test _group_name_ok("Tearing/Scan", "Surface_1") # scan indices verbatim + @test _group_name_ok("Input/RawInputs/Coils", "my_coils") # raw-snapshot names verbatim + @test _group_name_ok("", "SingularSurfaces") + @test !_group_name_ok("", "singular") + @test !_group_name_ok("SingularSurfaces", "kinetic") +end + @testset "gpec.h5 schema naming" begin template_dir = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example") From f76b31ce3f607fc349a04fe9c5cb532cfeea4c09 Mon Sep 17 00:00:00 2001 From: logan-nc Date: Fri, 14 Aug 2026 15:34:55 -0400 Subject: [PATCH 3/3] ALL - CLEANUP - Apply reviewer dispositions on the schema-rename PR Per review discussion: - Drop the regression-extractor legacy-path fallback (LEGACY_PREFIX_MAP): schema renames are clean breaks everywhere. Cross-boundary harness comparisons now rely on cached pre-rename quantities (stored by name); fresh extraction of pre-rename outputs reports missing, and developers re-baseline old refs with --force when they need that history. Conventions doc and CLAUDE.md pointer updated to state the new policy. - Revert the Analysis/PerturbedEquilibrium module path consts to the inline base-string style. - Drop the H5_* writer/rerun shared consts; paths are inlined again in both write_outputs_to_HDF5 and Rerun.jl with cross-reference comments marking the mirrored pair. - Simplify runtests_h5_schema.jl: the CamelCase walk already forbids every lowercase legacy group, so the 16-name absence loop is replaced by targeted asserts on the two moved CamelCase groups (FreeBoundaryStability, EdgeScan). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0129rSTCmYJDBbcH9khHqYnz --- CLAUDE.md | 2 +- docs/development/hdf5-conventions.md | 2 +- regression-harness/src/extractor.jl | 79 +++----------------------- src/Analysis/PerturbedEquilibrium.jl | 20 +++---- src/GeneralizedPerturbedEquilibrium.jl | 24 +++----- src/Rerun.jl | 23 ++++---- test/runtests_h5_schema.jl | 10 ++-- 7 files changed, 41 insertions(+), 119 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3fc0a0e43..2b4478aef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,7 +148,7 @@ Additional file hygiene (enforced by pre-commit hooks): ### HDF5 Output Conventions -The `gpec.h5` schema follows one physics-first convention (CamelCase groups at all levels, snake_case datasets, data-driven tokens verbatim, inputs only under `Input/`, five named top-level physics-topic exceptions). **Do not invent new group names or echo inputs into output groups** — read **[`docs/development/hdf5-conventions.md`](docs/development/hdf5-conventions.md)** before adding or moving any HDF5 output, and keep the regression-harness extractor's legacy map in sync on renames. +The `gpec.h5` schema follows one physics-first convention (CamelCase groups at all levels, snake_case datasets, data-driven tokens verbatim, inputs only under `Input/`, five named top-level physics-topic exceptions). **Do not invent new group names or echo inputs into output groups** — read **[`docs/development/hdf5-conventions.md`](docs/development/hdf5-conventions.md)** before adding or moving any HDF5 output; renames are clean breaks (update writer, readers, and harness case TOMLs together — there is no legacy-path shim). ### TOML Annotation Conventions diff --git a/docs/development/hdf5-conventions.md b/docs/development/hdf5-conventions.md index f43854cde..00c3fde09 100644 --- a/docs/development/hdf5-conventions.md +++ b/docs/development/hdf5-conventions.md @@ -50,4 +50,4 @@ Reserved (documented, not yet written): `ForceFreeStates/Solutions/RiccatiIntegr ## Back-compatibility policy -Schema renames are clean breaks in `src/` readers — no dual-path reads. The **only** legacy fallback lives in the regression harness (`regression-harness/src/extractor.jl`, `LEGACY_PREFIX_MAP`), so cross-commit comparisons and `--ref-range` scans keep working across a rename boundary. When renaming a path, update the writer, all readers, the case TOMLs, and add the new→old pair to that map. +Schema renames are clean breaks everywhere — no dual-path reads, no legacy-path translation layers. When renaming a path, update the writer, all readers, and the regression-harness case TOMLs in the same PR. Cross-commit harness comparisons across a rename boundary work only from already-cached quantities of the pre-rename refs (values are stored by quantity *name*, which renames preserve); fresh extraction of a pre-rename output reports the quantity as missing, and `--ref-range` scans crossing the boundary do the same. Developers should re-baseline old refs with `--force` before a rename lands if they need that history, and treat a schema rename as a `schema_version` bump readers can dispatch on. diff --git a/regression-harness/src/extractor.jl b/regression-harness/src/extractor.jl index c028dba81..0c27e55ce 100644 --- a/regression-harness/src/extractor.jl +++ b/regression-harness/src/extractor.jl @@ -2,73 +2,13 @@ HDF5 quantity extraction engine. """ -# Legacy-path fallback: outputs written before the module-mirroring CamelCase schema -# use the old group names on the right. Case TOMLs always carry the new paths; when a -# path is missing (the output came from a pre-rename ref), the translated legacy path -# is retried so cross-commit comparisons and --ref-range scans work across the -# boundary. First matching prefix wins — keep more-specific entries first. -const LEGACY_PREFIX_MAP = [ - "Input/RawInputs/Equilibrium" => "input/raw_inputs/equilibrium", - "Input/RawInputs/ForcingTerms" => "input/raw_inputs/forcing_terms", - "Input/RawInputs/Coils" => "input/raw_inputs/coils", - "Input/" => "input/", - "Info/" => "info/", - "Equilibrium/Profiles/" => "splines/profiles/", - "Equilibrium/Geometry/" => "splines/rzphi/", - "Equilibrium/" => "equil/", - "ForceFreeStates/Solutions/ForwardIntegration/" => "integration/", - "ForceFreeStates/Solutions/GalerkinIntegration/Solution/" => "galerkin/solution/", - "ForceFreeStates/Solutions/GalerkinIntegration/Match/InnerParams/" => "galerkin/match/inner_params/", - "ForceFreeStates/Solutions/GalerkinIntegration/Match/Inner/" => "galerkin/match/inner/", - "ForceFreeStates/Solutions/GalerkinIntegration/Match/" => "galerkin/match/", - "ForceFreeStates/Solutions/GalerkinIntegration/" => "galerkin/", - "ForceFreeStates/EulerLagrangeMatrices/Ideal/" => "matrices/ideal/", - "ForceFreeStates/EulerLagrangeMatrices/Kinetic/" => "matrices/kinetic/", - "ForceFreeStates/EulerLagrangeMatrices/" => "matrices/", - "ForceFreeStates/FreeBoundaryStability/" => "FreeBoundaryStability/", - "ForceFreeStates/EdgeScan/" => "EdgeScan/", - "LocalStability/" => "locstab/", - "SingularSurfaces/GalerkinDeltaPrime/" => "galerkin/", - "SingularSurfaces/Kinetic/" => "singular/kinetic/", - "SingularSurfaces/" => "singular/", - "PerturbedEquilibrium/ForcingModes/" => "perturbed_equilibrium/forcing_modes/", - "PerturbedEquilibrium/ResponseMatrices/" => "perturbed_equilibrium/response_matrices/", - "PerturbedEquilibrium/Response/" => "perturbed_equilibrium/response/", - "PerturbedEquilibrium/SingularCoupling/" => "perturbed_equilibrium/singular_coupling/", - "PerturbedEquilibrium/Energies/" => "perturbed_equilibrium/energies/", - "PerturbedEquilibrium/" => "perturbed_equilibrium/", - "KineticForces/" => "kinetic_forces/", - "Tearing/PerSurface/DpMatrix/" => "slayer/per_surface/dp_matrix/", - "Tearing/PerSurface/" => "slayer/per_surface/", - "Tearing/Roots/" => "slayer/roots/", - "Tearing/LayerWidths/" => "slayer/layer_widths/", - "Tearing/Diagnostics/ValidRoots/" => "slayer/diagnostics/valid_roots/", - "Tearing/Diagnostics/Poles/" => "slayer/diagnostics/poles/", - "Tearing/Diagnostics/FilteredRoots/" => "slayer/diagnostics/filtered_roots/", - "Tearing/Scan/Surface_" => "slayer/scan/surface_", - "Tearing/" => "slayer/", -] - -""" -Translate a new-schema h5path to its pre-rename legacy equivalent, or return -`nothing` when no mapping applies. -""" -function legacy_h5path(path::String) - for (new, old) in LEGACY_PREFIX_MAP - if startswith(path, new) - legacy = replace(path, new => old; count=1) - # Structural renames inside KineticForces (not plain prefix swaps). - legacy = replace(legacy, "/EnergyIntegrals/" => "/records/") - legacy = replace(legacy, r"^kinetic_forces/(\w+)/KineticMatrices/" => s"kinetic_forces/matrices_\1/") - return legacy - end - end - return nothing -end - """ Extract all quantities from a gpec.h5 file according to case spec. Returns a Vector{ExtractedQuantity}. + +Case TOMLs carry current-schema paths only; outputs from refs predating a schema +rename extract as "missing" unless their quantities are already cached. Re-baseline +old refs with `--force` after a rename rather than maintaining path translations. """ function extract_quantities(h5path::String, qty_specs::Vector{QuantitySpec}, runtime_s::Float64)::Vector{ExtractedQuantity} results = ExtractedQuantity[] @@ -83,13 +23,8 @@ function extract_quantities(h5path::String, qty_specs::Vector{QuantitySpec}, run continue end - # Resolve the H5 path, falling back to the pre-rename legacy schema. - path = spec.h5path - if !haskey(fid, path) - legacy = legacy_h5path(path) - path = (legacy !== nothing && haskey(fid, legacy)) ? legacy : nothing - end - if path === nothing + # Check if the H5 path exists + if !haskey(fid, spec.h5path) push!(results, ExtractedQuantity( spec.name, spec.label, nothing, nothing, nothing, @@ -97,7 +32,7 @@ function extract_quantities(h5path::String, qty_specs::Vector{QuantitySpec}, run continue end - raw = read(fid[path]) + raw = read(fid[spec.h5path]) eq = apply_extraction(spec, raw) push!(results, eq) end diff --git a/src/Analysis/PerturbedEquilibrium.jl b/src/Analysis/PerturbedEquilibrium.jl index da005a5c2..0e8675055 100644 --- a/src/Analysis/PerturbedEquilibrium.jl +++ b/src/Analysis/PerturbedEquilibrium.jl @@ -10,10 +10,6 @@ using HDF5 using LaTeXStrings using Plots -# HDF5 group prefixes shared by every reader in this module. -const PE_SINGULAR_COUPLING = "PerturbedEquilibrium/SingularCoupling/" -const PE_RESPONSE = "PerturbedEquilibrium/Response/" - # Check that a PE dataset exists and is non-empty. function _has_pe_data(h5path, key) h5open(h5path, "r") do fid @@ -44,7 +40,7 @@ Requires the perturbed equilibrium module to have been run and A `Plots.jl` plot object. """ function plot_resonant_area_weighted_field_amplitude(h5path; save_path=nothing) - base = PE_SINGULAR_COUPLING + base = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "resonant_area_weighted_field") || return plot(; title="No resonant area-weighted field data — run with perturbed equilibrium enabled", legend=false) @@ -95,7 +91,7 @@ Requires `SingularCoupling/island_half_width` in the HDF5 file. A `Plots.jl` plot object. """ function plot_island_widths(h5path; save_path=nothing) - base = PE_SINGULAR_COUPLING + base = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "island_half_width") || return plot(; title="No island width data — run with perturbed equilibrium enabled", legend=false) @@ -149,7 +145,7 @@ Requires `SingularCoupling/chirikov_parameter` in the HDF5 file. A `Plots.jl` plot object. """ function plot_chirikov_parameter(h5path; save_path=nothing) - base = PE_SINGULAR_COUPLING + base = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "chirikov_parameter") || return plot(; title="No Chirikov data — run with perturbed equilibrium enabled", legend=false) @@ -211,7 +207,7 @@ Requires `PerturbedEquilibrium/SingularCoupling/delta_prime` in the HDF5 file. A `Plots.jl` plot object. """ function plot_driven_delta_prime(h5path; save_path=nothing) - base = PE_SINGULAR_COUPLING + base = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "delta_prime") || return plot(; title="No PE Δ' data — run with perturbed equilibrium enabled", legend=false) @@ -297,7 +293,7 @@ end # Internal helper — resonant current scatter plot function _plot_resonant_current(h5path) - base = PE_SINGULAR_COUPLING + base = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "resonant_current") || return plot(; title="No resonant current data", legend=false) @@ -364,7 +360,7 @@ function plot_mode_spectrogram(h5path; component=:xi_psi, save_path=nothing) haskey(comp_map, component) || error("component must be one of :xi_psi, :b_psi, :b_theta, :b_zeta") - base = PE_RESPONSE + base = "PerturbedEquilibrium/Response/" dataset_path = base * comp_map[component] _has_pe_data(h5path, dataset_path) || @@ -375,7 +371,7 @@ function plot_mode_spectrogram(h5path; component=:xi_psi, save_path=nothing) read(fid["ForceFreeStates/Solutions/ForwardIntegration/psi"]), read(fid["Info/mlow"]), read(fid["Info/mhigh"]), read(fid["Info/nhigh"]), read(fid["Equilibrium/q95"]), - read(fid[PE_SINGULAR_COUPLING * "rational_psi"]) + read(fid["PerturbedEquilibrium/SingularCoupling/rational_psi"]) end mpert = mhigh - mlow + 1 @@ -456,7 +452,7 @@ end # Internal helper — |b_psi(m)| spectrum at the outermost psi surface function _plot_bpsi_edge_spectrum(h5path) - base = PE_RESPONSE + base = "PerturbedEquilibrium/Response/" _has_pe_data(h5path, base * "b_psi_area_weighted") || return plot(; title="No b_psi data — run with perturbed equilibrium enabled", legend=false) diff --git a/src/GeneralizedPerturbedEquilibrium.jl b/src/GeneralizedPerturbedEquilibrium.jl index 370e014a2..519dd952c 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -59,15 +59,6 @@ include("Analysis/Analysis.jl") import .Analysis as Analysis export Analysis -# HDF5 paths read back by the file-based rerun (Rerun.jl); shared consts keep the -# writer and rerun reader from drifting apart. Schema conventions: -# docs/development/hdf5-conventions.md. -const H5_INPUT_TOML = "Input/gpec_toml_raw" -const H5_RAW_EQUILIBRIUM = "Input/RawInputs/Equilibrium" -const H5_RAW_FORCING = "Input/RawInputs/ForcingTerms" -const H5_RAW_COILS = "Input/RawInputs/Coils" -const H5_GIT_VERSION = "Info/git_version" - include("Rerun.jl") # Import ForceFreeStates types and functions needed for main @@ -728,7 +719,7 @@ function write_outputs_to_HDF5( h5open(joinpath(intr.dir_path, ctrl.HDF5_filename), "w") do out_h5 # Store git version for reproducibility - out_h5[H5_GIT_VERSION] = git_version + out_h5["Info/git_version"] = git_version # Outer-region Galerkin Δ′ matrix (RDCON), if computed if gal_data !== nothing @@ -739,16 +730,17 @@ function write_outputs_to_HDF5( # ForceFreeStates/Equilibrium/Wall/PE control struct), plus the equilibrium ingest # arrays so a file-based rerun never needs the original g-file / CHEASE / IMAS source. if inputs !== nothing - out_h5[H5_INPUT_TOML] = sprint(TOML.print, inputs) + out_h5["Input/gpec_toml_raw"] = sprint(TOML.print, inputs) end if equil.ingest !== nothing # analytic equilibria are regenerated from their TOML section - out_h5["$H5_RAW_EQUILIBRIUM/ingest_kind"] = equil.ingest isa Equilibrium.DirectIngest ? "direct" : "inverse" + eq_group = "Input/RawInputs/Equilibrium" # read back by Rerun.read_equilibrium_ingest + out_h5["$eq_group/ingest_kind"] = equil.ingest isa Equilibrium.DirectIngest ? "direct" : "inverse" for f in fieldnames(typeof(equil.ingest)) - out_h5["$H5_RAW_EQUILIBRIUM/$f"] = getfield(equil.ingest, f) + out_h5["$eq_group/$f"] = getfield(equil.ingest, f) end end if forcing_modes !== nothing - forcing_group = create_group(out_h5, H5_RAW_FORCING) + forcing_group = create_group(out_h5, "Input/RawInputs/ForcingTerms") ForcingTerms.save_forcing_to_h5(forcing_modes, forcing_group) end @@ -1005,8 +997,8 @@ One subgroup per coil set; see `ForcingTerms.save_coils_to_h5`. function _write_coil_snapshot!(h5_path::String, coil_sets::Vector{ForcingTerms.CoilSet}) isfile(h5_path) || return nothing h5open(h5_path, "r+") do out_h5 - haskey(out_h5, H5_RAW_COILS) && return nothing - ForcingTerms.save_coils_to_h5(coil_sets, create_group(out_h5, H5_RAW_COILS)) + haskey(out_h5, "Input/RawInputs/Coils") && return nothing + ForcingTerms.save_coils_to_h5(coil_sets, create_group(out_h5, "Input/RawInputs/Coils")) end return nothing end diff --git a/src/Rerun.jl b/src/Rerun.jl index 4eab2a985..7d932addf 100644 --- a/src/Rerun.jl +++ b/src/Rerun.jl @@ -33,8 +33,9 @@ Reconstruct the [`DirectIngest`](@ref)/[`InverseIngest`](@ref) stored under analytic equilibrium — replayed from its TOML section rather than stored arrays. """ function read_equilibrium_ingest(in_h5) - haskey(in_h5, H5_RAW_EQUILIBRIUM) || return nothing - group = in_h5[H5_RAW_EQUILIBRIUM] + group_path = "Input/RawInputs/Equilibrium" # mirrors the write in write_outputs_to_HDF5 + haskey(in_h5, group_path) || return nothing + group = in_h5[group_path] kind = read(group, "ingest_kind") T = kind == "direct" ? Equilibrium.DirectIngest : kind == "inverse" ? Equilibrium.InverseIngest : @@ -208,29 +209,29 @@ function build_inputs_from_h5(args::Vector{String}) # ignores the frozen forcing-mode snapshot. use_coils = cli.coil_source == "coils" toml_raw, ingest, source_git, preloaded_forcing, preloaded_coils = h5open(source_h5, "r") do in_h5 - haskey(in_h5, H5_INPUT_TOML) || - error("Source HDF5 $source_h5 has no $H5_INPUT_TOML — produced by a pre-rerun version of GPEC") - forcing_modes = if use_coils || !haskey(in_h5, H5_RAW_FORCING) + haskey(in_h5, "Input/gpec_toml_raw") || + error("Source HDF5 $source_h5 has no Input/gpec_toml_raw — produced by a pre-rerun version of GPEC") + forcing_modes = if use_coils || !haskey(in_h5, "Input/RawInputs/ForcingTerms") nothing else modes = ForcingTerms.ForcingMode[] - ForcingTerms.load_forcing_from_h5_group!(modes, in_h5[H5_RAW_FORCING]) + ForcingTerms.load_forcing_from_h5_group!(modes, in_h5["Input/RawInputs/ForcingTerms"]) modes end coil_sets = if use_coils - haskey(in_h5, H5_RAW_COILS) || - error("--coil-source coils requested but $source_h5 has no $H5_RAW_COILS " * + haskey(in_h5, "Input/RawInputs/Coils") || + error("--coil-source coils requested but $source_h5 has no Input/RawInputs/Coils " * "(the source run did not use coils, or predates coil-snapshot support)") sets = ForcingTerms.CoilSet[] - ForcingTerms.load_coils_from_h5_group!(sets, in_h5[H5_RAW_COILS]) + ForcingTerms.load_coils_from_h5_group!(sets, in_h5["Input/RawInputs/Coils"]) sets else nothing end ( - read(in_h5, H5_INPUT_TOML), + read(in_h5, "Input/gpec_toml_raw"), read_equilibrium_ingest(in_h5), - haskey(in_h5, H5_GIT_VERSION) ? read(in_h5, H5_GIT_VERSION) : "unknown", + haskey(in_h5, "Info/git_version") ? read(in_h5, "Info/git_version") : "unknown", forcing_modes, coil_sets ) diff --git a/test/runtests_h5_schema.jl b/test/runtests_h5_schema.jl index d0d7c66c4..b8e4662a3 100644 --- a/test/runtests_h5_schema.jl +++ b/test/runtests_h5_schema.jl @@ -62,12 +62,10 @@ end isempty(bad) || @error "non-CamelCase group paths in gpec.h5" bad @test isempty(bad) - # Retired/renamed legacy top-level groups must not reappear. - for legacy in ("info", "input", "equil", "splines", "integration", "locstab", - "singular", "matrices", "kinetic", "galerkin", "slayer", "kinetic_forces", - "perturbed_equilibrium", "vacuum", "FreeBoundaryStability", "EdgeScan") - @test !haskey(h5, legacy) - end + # The CamelCase walk above already forbids every lowercase legacy group; these + # two moved under ForceFreeStates/ and are CamelCase, so pin them explicitly. + @test !haskey(h5, "FreeBoundaryStability") + @test !haskey(h5, "EdgeScan") # Inputs live only under Input/; spot-check the rerun-critical paths. @test haskey(h5, "Input/gpec_toml_raw")