diff --git a/CLAUDE.md b/CLAUDE.md index d649681e9..2b4478aef 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; renames are clean breaks (update writer, readers, and harness case TOMLs together — there is no legacy-path shim). + ### 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/plot_xi_eigenmode.jl b/benchmarks/plot_xi_eigenmode.jl index f39682caa..c4be18af7 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..00c3fde09 --- /dev/null +++ b/docs/development/hdf5-conventions.md @@ -0,0 +1,53 @@ +# 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 + +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_`). + +## 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 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/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 4e7e72b7b..3e0474482 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 46a52e049..a89b382f6 100644 --- a/examples/DIIID-like_SLAYER_example/gpec.toml +++ b/examples/DIIID-like_SLAYER_example/gpec.toml @@ -52,7 +52,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. set_psilim_via_dmlim = true # TRUE for diverted geqdsks — q → ∞ at separatrix, so dmlim truncation avoids the δW kink instability at negligible domain cost diff --git a/examples/DIIID-like_gal_resistive_example/gpec.toml b/examples/DIIID-like_gal_resistive_example/gpec.toml index e296d0585..f84d8c52a 100644 --- a/examples/DIIID-like_gal_resistive_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_example/gpec.toml @@ -51,7 +51,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 2d2cbe86b..e5536c75e 100644 --- a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml @@ -50,7 +50,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 451a7a81f..9a3bf9bc5 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 7812d4f9b..733a92c31 100644 --- a/examples/Solovev_kinetic_NTV_example/gpec.toml +++ b/examples/Solovev_kinetic_NTV_example/gpec.toml @@ -69,7 +69,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/regression-harness/cases/diiid_n1.toml b/regression-harness/cases/diiid_n1.toml index 0e0336a99..6b1161ab6 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" @@ -271,12 +271,11 @@ 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 = "singular/delta_prime_matrix" +h5path = "SingularSurfaces/delta_prime_matrix" type = "complex_matrix" extract = "diagonal_complex" label = "delta prime (BVP diagonal)" @@ -284,7 +283,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 +291,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 +299,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 +307,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 +315,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 +323,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 +331,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 +339,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 +352,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 +363,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 +372,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 +380,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 +390,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 +398,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 +406,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 c1009fcbf..6682b97de 100644 --- a/regression-harness/cases/diiid_slayer_n1.toml +++ b/regression-harness/cases/diiid_slayer_n1.toml @@ -10,7 +10,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" @@ -18,7 +18,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" @@ -26,7 +26,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" @@ -34,7 +34,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" @@ -42,7 +42,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" @@ -50,7 +50,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" @@ -58,7 +58,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" @@ -66,7 +66,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" @@ -74,7 +74,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" @@ -82,7 +82,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" @@ -99,7 +99,7 @@ order = 19 # numerically unreliable on the outermost surfaces (e.g. 5/1, 6/1, 7/1 near the # 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]" @@ -107,7 +107,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]" @@ -115,7 +115,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]" @@ -123,7 +123,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]" @@ -133,7 +133,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]" @@ -142,7 +142,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 6811d113a..e1fc42227 100644 --- a/regression-harness/cases/gal_resistive_diiid.toml +++ b/regression-harness/cases/gal_resistive_diiid.toml @@ -11,7 +11,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" @@ -19,7 +19,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" @@ -28,7 +28,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" @@ -37,7 +37,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||" @@ -46,7 +46,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||" @@ -55,7 +55,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" @@ -64,7 +64,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" @@ -79,7 +79,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 Δ||" @@ -88,7 +88,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||" @@ -97,7 +97,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 c2b059d7e..a9623f92f 100644 --- a/regression-harness/cases/gal_resistive_pe.toml +++ b/regression-harness/cases/gal_resistive_pe.toml @@ -15,7 +15,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" @@ -24,7 +24,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" @@ -33,54 +33,54 @@ 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" 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 = "perturbed_equilibrium/singular_coupling/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 = "perturbed_equilibrium/singular_coupling/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 = "perturbed_equilibrium/singular_coupling/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 # 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" 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 = "perturbed_equilibrium/singular_coupling/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/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 078b9c363..d18ac2be6 100644 --- a/regression-harness/cases/solovev_kinetic_nuzero.toml +++ b/regression-harness/cases/solovev_kinetic_nuzero.toml @@ -15,28 +15,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])" @@ -44,7 +44,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)" @@ -52,14 +52,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)" @@ -67,14 +67,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" @@ -82,44 +82,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..0c27e55ce 100644 --- a/regression-harness/src/extractor.jl +++ b/regression-harness/src/extractor.jl @@ -5,6 +5,10 @@ HDF5 quantity extraction engine. """ 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[] @@ -91,7 +95,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 be3e522ee..8fc4f8a68 100644 --- a/regression-harness/src/runner.jl +++ b/regression-harness/src/runner.jl @@ -134,11 +134,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 %RUNINFO% """ diff --git a/regression-harness/src/types.jl b/regression-harness/src/types.jl index 9417c5303..05c7159ce 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..0e8675055 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 @@ -25,7 +25,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 +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 = "perturbed_equilibrium/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) @@ -76,7 +76,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 +91,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 = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "island_half_width") || return plot(; title="No island width data — run with perturbed equilibrium enabled", legend=false) @@ -130,7 +130,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 +145,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 = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "chirikov_parameter") || return plot(; title="No Chirikov data — run with perturbed equilibrium enabled", legend=false) @@ -184,15 +184,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 +207,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 = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "delta_prime") || return plot(; title="No PE Δ' data — run with perturbed equilibrium enabled", legend=false) @@ -293,7 +293,7 @@ end # Internal helper — resonant current scatter plot function _plot_resonant_current(h5path) - base = "perturbed_equilibrium/singular_coupling/" + base = "PerturbedEquilibrium/SingularCoupling/" _has_pe_data(h5path, base * "resonant_current") || return plot(; title="No resonant current data", legend=false) @@ -360,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 = "perturbed_equilibrium/response/" + base = "PerturbedEquilibrium/Response/" dataset_path = base * comp_map[component] _has_pe_data(h5path, dataset_path) || @@ -368,10 +368,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["PerturbedEquilibrium/SingularCoupling/rational_psi"]) end mpert = mhigh - mlow + 1 @@ -452,13 +452,13 @@ end # Internal helper — |b_psi(m)| spectrum at the outermost psi surface function _plot_bpsi_edge_spectrum(h5path) - base = "perturbed_equilibrium/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) 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 73492a673..51da221be 100644 --- a/src/ForceFreeStates/Ballooning.jl +++ b/src/ForceFreeStates/Ballooning.jl @@ -100,8 +100,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/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/Galerkin/GalerkinSolve.jl b/src/ForceFreeStates/Galerkin/GalerkinSolve.jl index 8389ad9bc..3802bb857 100644 --- a/src/ForceFreeStates/Galerkin/GalerkinSolve.jl +++ b/src/ForceFreeStates/Galerkin/GalerkinSolve.jl @@ -229,55 +229,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/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/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 27279f51c..519dd952c 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -154,12 +154,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}, @@ -288,7 +288,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 @@ -525,7 +525,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) @@ -719,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["info/git_version"] = git_version + out_h5["Info/git_version"] = git_version # Outer-region Galerkin Δ′ matrix (RDCON), if computed if gal_data !== nothing @@ -730,111 +730,112 @@ 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["Input/gpec_toml_raw"] = sprint(TOML.print, inputs) end if equil.ingest !== nothing # analytic equilibria are regenerated from their TOML section - eq_group = "input/raw_inputs/equilibrium" + 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["$eq_group/$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, "Input/RawInputs/ForcingTerms") 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 locstab !== nothing locstab_xs = locstab.cache.x - out_h5["locstab/di"] = locstab.y[:, 1] ./ locstab_xs - out_h5["locstab/dr"] = locstab.y[:, 2] ./ locstab_xs + out_h5["LocalStability/di"] = locstab.y[:, 1] ./ locstab_xs + out_h5["LocalStability/dr"] = 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"] = (locstab !== nothing && !isempty(intr.sing)) ? - [locstab(sing.psifac)[1] / sing.psifac for sing in intr.sing] : Float64[] - out_h5["locstab/ballooning_Delta_prime"] = locstab !== nothing ? locstab.y[:, 4] : Float64[] + out_h5["SingularSurfaces/di0"] = (locstab !== nothing && !isempty(intr.sing)) ? + [locstab(sing.psifac)[1] / sing.psifac for sing in intr.sing] : Float64[] + out_h5["LocalStability/ballooning_Delta_prime"] = locstab !== nothing ? 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 - 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 + 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) @@ -847,8 +848,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!). @@ -857,17 +858,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 @@ -876,15 +877,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 @@ -893,18 +895,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: @@ -912,14 +914,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"] = free_energies !== nothing ? free_energies.wt0 : ComplexF64[] - out_h5["FreeBoundaryStability/W_plasma"] = free_energies !== nothing ? free_energies.wp : ComplexF64[] - out_h5["FreeBoundaryStability/W_vacuum"] = free_energies !== nothing ? free_energies.wv : ComplexF64[] - out_h5["FreeBoundaryStability/W_freeboundary_eigenmodes"] = free_energies !== nothing ? free_energies.wt : ComplexF64[] - out_h5["FreeBoundaryStability/eigenmode_energies"] = free_energies !== nothing ? free_energies.et : ComplexF64[] - out_h5["FreeBoundaryStability/eigenmode_plasma_energies"] = free_energies !== nothing ? free_energies.ep : ComplexF64[] - out_h5["FreeBoundaryStability/eigenmode_vacuum_energies"] = free_energies !== nothing ? free_energies.ev : ComplexF64[] - out_h5["FreeBoundaryStability/vacuum_eigenvalue"] = free_energies !== nothing ? free_energies.vacuum_eigenvalue : NaN + fbs = "ForceFreeStates/FreeBoundaryStability" + out_h5["$fbs/W_freeboundary"] = free_energies !== nothing ? free_energies.wt0 : ComplexF64[] + out_h5["$fbs/W_plasma"] = free_energies !== nothing ? free_energies.wp : ComplexF64[] + out_h5["$fbs/W_vacuum"] = free_energies !== nothing ? free_energies.wv : ComplexF64[] + out_h5["$fbs/W_freeboundary_eigenmodes"] = free_energies !== nothing ? free_energies.wt : ComplexF64[] + out_h5["$fbs/eigenmode_energies"] = free_energies !== nothing ? free_energies.et : ComplexF64[] + out_h5["$fbs/eigenmode_plasma_energies"] = free_energies !== nothing ? free_energies.ep : ComplexF64[] + out_h5["$fbs/eigenmode_vacuum_energies"] = free_energies !== nothing ? free_energies.ev : ComplexF64[] + out_h5["$fbs/vacuum_eigenvalue"] = free_energies !== nothing ? free_energies.vacuum_eigenvalue : NaN # Cartesian surface point clouds used downstream for visualisation and # perturbed-equilibrium plotting. @@ -930,12 +933,6 @@ function write_outputs_to_HDF5( out_h5["SurfaceGeometries/Wall/y"] = free_energies !== nothing ? free_energies.wall_pts[:, 2] : Float64[] out_h5["SurfaceGeometries/Wall/z"] = free_energies !== nothing ? free_energies.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 @@ -952,39 +949,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 @@ -993,15 +990,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, "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/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..9f14bd457 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..7d932addf 100644 --- a/src/Rerun.jl +++ b/src/Rerun.jl @@ -28,12 +28,12 @@ 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" + 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") @@ -209,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, "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, "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["input/raw_inputs/forcing_terms"]) + ForcingTerms.load_forcing_from_h5_group!(modes, in_h5["Input/RawInputs/ForcingTerms"]) 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, "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["input/raw_inputs/coils"]) + ForcingTerms.load_coils_from_h5_group!(sets, in_h5["Input/RawInputs/Coils"]) sets else nothing end ( - read(in_h5, "input/gpec_toml_raw"), + read(in_h5, "Input/gpec_toml_raw"), read_equilibrium_ingest(in_h5), - haskey(in_h5, "info/git_version") ? read(in_h5, "info/git_version") : "unknown", + haskey(in_h5, "Info/git_version") ? read(in_h5, "Info/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..b8e4662a3 --- /dev/null +++ b/test/runtests_h5_schema.jl @@ -0,0 +1,75 @@ +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 + +# 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") + + 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) + + # 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") + @test haskey(h5, "Info/git_version") + end + end +end diff --git a/test/runtests_parallel_integration.jl b/test/runtests_parallel_integration.jl index 666191d17..d23a00790 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