diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54e179385..94c59f489 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,5 +69,5 @@ repos: - id: toml-no-deprecated-keys name: 'TOML conventions: no deprecated config keys' language: pygrep - entry: '^(mer_flag|force_wv_symmetry|ode_flag|cyl_flag|mat_flag|power_bp|power_b|power_r|power_rc)\s*=' + entry: '^(mer_flag|force_wv_symmetry|ode_flag|cyl_flag|mat_flag|power_bp|power_b|power_r|power_rc|nstep|diagnose_ca|use_parallel|use_riccati|parallel_threads|psiedge|newq0|use_galgrid|reg_spot|dc_type|dr_val|dgeo_val|xnom|ynom|znom)\s*=' files: ^(examples/.*\.toml|test/test_data/.*\.toml)$ diff --git a/CLAUDE.md b/CLAUDE.md index d649681e9..3fc0a0e43 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -146,6 +146,10 @@ Additional file hygiene (enforced by pre-commit hooks): - Files must end with exactly one newline - LF line endings only (no CRLF) +### HDF5 Output Conventions + +The `gpec.h5` schema follows one physics-first convention (CamelCase groups at all levels, snake_case datasets, data-driven tokens verbatim, inputs only under `Input/`, five named top-level physics-topic exceptions). **Do not invent new group names or echo inputs into output groups** — read **[`docs/development/hdf5-conventions.md`](docs/development/hdf5-conventions.md)** before adding or moving any HDF5 output, and keep the regression-harness extractor's legacy map in sync on renames. + ### TOML Annotation Conventions Config-style TOML files (`examples/*/gpec.toml`, `examples/*/sol.toml`, `test/test_data/*` fixtures, `regression-harness/cases/*.toml`) follow one shared annotation style (header comment block, inline `# description` on every variable line sourced from the matching config struct's docstring, no Fortran references, no deprecated variables). **Do not invent a new convention** — read **[`docs/development/toml-conventions.md`](docs/development/toml-conventions.md)** in full before adding or editing one of these files. diff --git a/Project.toml b/Project.toml index 04edf6d0b..bb4d32cc5 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.1.0" [deps] AdaptiveArrayPools = "4f381ef7-9af0-4cbe-99d4-cf36d7b0f233" Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def" diff --git a/benchmarks/benchmark_against_fortran_run.jl b/benchmarks/benchmark_against_fortran_run.jl index b48cb39cc..fc73667d0 100644 --- a/benchmarks/benchmark_against_fortran_run.jl +++ b/benchmarks/benchmark_against_fortran_run.jl @@ -149,6 +149,7 @@ function parse_fortran_run(dir::String) eq_file = _find_string(eq_text, "eq_filename"; default="") jac_type = _find_string(eq_text, "jac_type"; default="hamada") grid_type = _find_string(eq_text, "grid_type"; default="ldp") + grid_type == "ldp" && (grid_type = "rational_packed") # map the legacy Fortran value to the Julia spelling psilow = _find_scalar(eq_text, "psilow"; default=1e-4) psihigh = _find_scalar(eq_text, "psihigh"; default=0.993) mpsi = _find_int(eq_text, "mpsi"; default=128) @@ -312,17 +313,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 +334,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 @@ -415,7 +417,7 @@ function write_gpec_toml_coil( # Match the Fortran sas_flag truncation: integration stops at q = qhigh # (= outermost rational q + dmlim). psiedge from dcon.in (=1.0 → no edge dW scan). @printf(io, "qhigh = %.4f\n", qhigh) - @printf(io, "psiedge = %.4f\n", p.psiedge) + @printf(io, "dW_edge_scan_start = %.4f\n", p.psiedge) println(io, "nn_low = $(p.nn)") println(io, "nn_high = $(p.nn)") println(io, "delta_mlow = $(p.delta_mlow)") @@ -475,7 +477,7 @@ function write_gpec_toml_file( println(io, "local_stability_flag = true") println(io, "vac_flag = true") @printf(io, "qlow = %.4f\n", p.qlow) - @printf(io, "psiedge = %.4f\n", p.psiedge) + @printf(io, "dW_edge_scan_start = %.4f\n", p.psiedge) println(io, "nn_low = $(p.nn)") println(io, "nn_high = $(p.nn)") println(io, "delta_mlow = $(p.delta_mlow)") @@ -1194,12 +1196,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_coil_ForcingTerms_against_fortran.jl b/benchmarks/benchmark_coil_ForcingTerms_against_fortran.jl index 22e2b1938..7dbd6b299 100644 --- a/benchmarks/benchmark_coil_ForcingTerms_against_fortran.jl +++ b/benchmarks/benchmark_coil_ForcingTerms_against_fortran.jl @@ -108,6 +108,7 @@ function parse_fortran_run(dir::String)::FortranRunParams psihigh = _find_scalar(eq_text, "psihigh"; default=0.99) mtheta_eq = _find_int(eq_text, "mtheta"; default=256) grid_type = _find_string(eq_text, "grid_type"; default="ldp") + grid_type == "ldp" && (grid_type = "rational_packed") # map the legacy Fortran value to the Julia spelling # Toroidal mode number and m-range expansion nn = _find_int(dcon_text, "nn"; default=1) diff --git a/benchmarks/benchmark_delta_prime_methods.jl b/benchmarks/benchmark_delta_prime_methods.jl index 4bf77f179..980841387 100644 --- a/benchmarks/benchmark_delta_prime_methods.jl +++ b/benchmarks/benchmark_delta_prime_methods.jl @@ -23,7 +23,7 @@ function setup_and_run_solovev() ex = joinpath(@__DIR__, "..", "test", "test_data", "regression_solovev_ideal_example") inputs = TOML.parsefile(joinpath(ex, "gpec.toml")) inputs["ForceFreeStates"]["verbose"] = false - inputs["ForceFreeStates"]["use_riccati"] = true + inputs["ForceFreeStates"]["integrator"] = "riccati" intr = FFS.ForceFreeStatesInternal(; dir_path=ex) ctrl = FFS.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) diff --git a/benchmarks/benchmark_diiid_ideal_ntv_torque.jl b/benchmarks/benchmark_diiid_ideal_ntv_torque.jl index 2263e78ce..627ec7546 100644 --- a/benchmarks/benchmark_diiid_ideal_ntv_torque.jl +++ b/benchmarks/benchmark_diiid_ideal_ntv_torque.jl @@ -118,12 +118,12 @@ function build_benchmark_tomldir(eq_file::String) eq_filename = "$eq_name" eq_type = "efit" jac_type = "hamada" -grid_type = "ldp" +grid_type = "rational_packed" psilow = 1e-4 psihigh = 0.993 mpsi = 128 mtheta = 256 -newq0 = 0 +q0_override = 0.0 etol = 1e-7 [Wall] @@ -134,7 +134,7 @@ local_stability_flag = true vac_flag = true force_termination = false -psiedge = 1.00 # No edge-scan truncation (dmlim mechanism removed in develop) +dW_edge_scan_start = 1.00 # No edge-scan truncation (dmlim mechanism removed in develop) qlow = 1.02 qhigh = 1e3 sing_start = 0 diff --git a/benchmarks/benchmark_diiid_kinetic_stability.jl b/benchmarks/benchmark_diiid_kinetic_stability.jl index b5465f1ad..0624d71b5 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 @@ -129,7 +129,7 @@ psilow = 0.01 psihigh = 0.993 mpsi = 128 mtheta = 256 -newq0 = 0 +q0_override = 0.0 etol = 1e-7 [Wall] @@ -140,7 +140,7 @@ local_stability_flag = true vac_flag = true force_termination = true # Skip PE+KF post-processing — we only need FFS eigenvalues -psiedge = 1.0 # No edge-scan truncation (dmlim mechanism removed in develop) +dW_edge_scan_start = 1.0 # No edge-scan truncation (dmlim mechanism removed in develop) qlow = 1.02 qhigh = 1e3 sing_start = 0 @@ -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_q_vs_iota_edge.jl b/benchmarks/benchmark_q_vs_iota_edge.jl index e258fb457..eadb4dd9c 100644 --- a/benchmarks/benchmark_q_vs_iota_edge.jl +++ b/benchmarks/benchmark_q_vs_iota_edge.jl @@ -21,7 +21,7 @@ const EXAMPLE_DIR = joinpath(@__DIR__, "..", "examples", "DIIID-like_ideal_examp # Dense ldp reference equilibrium: treat its q(ψ) as ground truth function reference_q() _, eq_config, additional_input = GPE.build_inputs_from_toml(EXAMPLE_DIR) - eq_config.grid_type = "ldp" + eq_config.grid_type = "rational_packed" eq_config.mpsi = 1024 equil = GPE.Equilibrium.setup_equilibrium(eq_config, additional_input) return equil, eq_config 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/benchmark_threads.jl b/benchmarks/benchmark_threads.jl index 048c64c66..9e067ca12 100644 --- a/benchmarks/benchmark_threads.jl +++ b/benchmarks/benchmark_threads.jl @@ -1,5 +1,5 @@ # Thread-scaling benchmark for the bidirectional parallel FM integration. -# Runs the Solovev (N=8) and DIIID-like (N=26) examples with use_parallel=true +# Runs the Solovev (N=8) and DIIID-like (N=26) examples with integrator="stride" # across 1, 2, 4, 8 threads and compares against the serial Riccati path. # # Usage (from JPEC_main root): @@ -7,11 +7,10 @@ using GeneralizedPerturbedEquilibrium, TOML, Printf, Statistics -function run_ffs(ex; use_parallel, use_riccati=false) +function run_ffs(ex; integrator) inputs = TOML.parsefile(joinpath(ex, "gpec.toml")) inputs["ForceFreeStates"]["verbose"] = false - inputs["ForceFreeStates"]["use_parallel"] = use_parallel - inputs["ForceFreeStates"]["use_riccati"] = use_riccati + inputs["ForceFreeStates"]["integrator"] = integrator inputs["ForceFreeStates"]["write_outputs_to_HDF5"] = false intr = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesInternal(; dir_path=ex) ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; @@ -36,17 +35,17 @@ function run_ffs(ex; use_parallel, use_riccati=false) return real(vac.et[1]), intr.numpert_total end -function timed_run(ex; use_parallel, use_riccati=false, nwarm=1, nrep=2) +function timed_run(ex; integrator, nwarm=1, nrep=2) # Warmup for _ in 1:nwarm - run_ffs(ex; use_parallel, use_riccati) + run_ffs(ex; integrator) end # Timed runs times = Float64[] local et1, N for _ in 1:nrep t0 = time() - et1, N = run_ffs(ex; use_parallel, use_riccati) + et1, N = run_ffs(ex; integrator) push!(times, time() - t0) end return mean(times), et1, N @@ -60,9 +59,9 @@ diiid_ex = joinpath(root, "examples", "DIIID-like_ideal_example") println("\n=== Thread-scaling benchmark ($(nthreads) thread(s)) ===\n") for (label, ex) in [("Solovev", sol_ex), ("DIIID-like", diiid_ex)] - t_std, et_std, N = timed_run(ex; use_parallel=false, use_riccati=false) - t_ric, et_ric, _ = timed_run(ex; use_parallel=false, use_riccati=true) - t_par, et_par, _ = timed_run(ex; use_parallel=true, use_riccati=false) + t_std, et_std, N = timed_run(ex; integrator="serial") + t_ric, et_ric, _ = timed_run(ex; integrator="riccati") + t_par, et_par, _ = timed_run(ex; integrator="stride") err_ric = abs(et_ric - et_std) / abs(et_std) * 100 err_par = abs(et_par - et_std) / abs(et_std) * 100 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/Project.toml b/docs/Project.toml index 12f07f702..83d3d84a8 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,5 +2,8 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" GeneralizedPerturbedEquilibrium = "462872dd-e066-4d2e-b993-6468b5239634" +[sources] +GeneralizedPerturbedEquilibrium = {path = ".."} + [compat] Documenter = "1.0" 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..6eae57642 --- /dev/null +++ b/docs/development/hdf5-conventions.md @@ -0,0 +1,64 @@ +# HDF5 Output Schema Conventions + +Conventions for the structure and naming of the `gpec.h5` output file. Every writer that adds a group or dataset must follow these rules; the naming is enforced by `test/runtests_h5_schema.jl`. + +## Governing principle: physics-first organization + +**The schema must be intuitive to a plasma physicist who is not a developer of this code.** The top level largely mirrors the TOML sections / major `src/` modules — which are themselves organized along physics lines — but **physics intuition wins whenever the two diverge**. Worked examples of that rule: + +- All per-rational-surface stability results consolidate under `SingularSurfaces/`, regardless of which algorithm produced them: the ideal BVP `delta_prime_matrix`, the GGJ coefficients, and the Galerkin outer-region Δ′/PEST-3 results (`GalerkinDeltaPrime/`) live side by side. Provenance is recorded in the subgroup name, not by scattering results across producer-owned groups. +- `EulerLagrangeMatrices` over a bare `Matrices` — group names must say what the data *is*, not which array it came from. Same reasoning renamed `records/` → `EnergyIntegrals/` and `matrices_/` → `KineticMatrices/`. + +Physics-topic groups elevated to top level (rather than nested under their producer): `Info/`, `Input/`, `SingularSurfaces/`, `LocalStability/`, `SurfaceGeometries/`. + +## Naming rules + +- **Groups are CamelCase at every level** (`ForceFreeStates/`, `PerSurface/`, `GalerkinDeltaPrime/`). +- **Datasets (leaves) are snake_case** (`eigenmode_energies`, `delta_prime_matrix`). Established physics symbols keep their natural case (`E`, `F`, `Q_root_real`, `pest3_Delta`, `2piF`). +- **Data-driven tokens are stored verbatim**: coil-set names under `Input/RawInputs/Coils/`, KineticForces method tokens (`fgar`, …), scan indices (`Surface_`, `psi_`). + +## Inputs live only under `Input/` + +`Input/gpec_toml_raw` stores the full merged TOML, and `Input/RawInputs/` stores the raw equilibrium/forcing/coil data — together they make `gpec.h5` a self-contained rerun snapshot (`Rerun.jl` reconstructs every control struct from them; the writer/reader path pair is locked by shared `H5_*` consts in `GeneralizedPerturbedEquilibrium.jl`). **Never echo TOML flags or control-struct values into any other group** — every group outside `Input/` is derived output. (The former `kinetic/` and `slayer/settings/` echoes were removed under this rule.) + +## Schema + +Top level (10 groups): + +| Group | Contents | +|---|---| +| `Info/` | Run metadata: `git_version`, mode-number ranges (`mpert`, `mlow`, …, `mn_index`), `psilim`, `qlim` | +| `Input/` | Rerun snapshot: `gpec_toml_raw`, `RawInputs/{Equilibrium, ForcingTerms, Coils/}` | +| `Equilibrium/` | Scalars (β, q₀, q95, …) plus `Profiles/` (1-D: xs, 2piF, mu0p, dVdpsi, q) and `Geometry/` (2-D: rcoords, offset, nu, jac) | +| `ForceFreeStates/` | `Solutions/ForwardIntegration/` (u-solutions), `Solutions/GalerkinIntegration/` (`Solution/`, `Match/`, `msing`), `EulerLagrangeMatrices/{Ideal,Kinetic}`, `FreeBoundaryStability/`, `EdgeScan/` | +| `LocalStability/` | Mercier `di`, resistive interchange `dr`, `ballooning_Delta_prime`, ballooning α boundary | +| `SingularSurfaces/` | Per-rational-surface data: ψ, q, m/n, GGJ coefficients, `delta_prime_matrix`/`delta_prime_raw`/`delta_coil`, `GalerkinDeltaPrime/`, `Kinetic/` | +| `PerturbedEquilibrium/` | `ForcingModes/`, `Response/`, `ResponseMatrices/`, `SingularCoupling/`, `Energies/`, control-surface spectra | +| `KineticForces/` | `/` (torque/energy profiles, `EnergyIntegrals/`, `KineticMatrices/`) | +| `Tearing/` | `PerSurface/` (+ `DpMatrix/`), `Roots/`, `LayerWidths/`, `Diagnostics/{ValidRoots,Poles,FilteredRoots}`, `Scan/Surface_/` | +| `SurfaceGeometries/` | `{Plasma,Wall}/{x,y,z}` point clouds | + +Reserved (documented, not yet written): `ForceFreeStates/Solutions/RiccatiIntegration/` — the third integrator backend slot alongside `ForwardIntegration` and `GalerkinIntegration`, matching the `[ForceFreeStates]` `integrator = "riccati"` option. + +## Metadata contract (self-describing datasets) + +Every dataset outside `Input/` (raw snapshot) and `GalerkinIntegration/Match/` (debug-only) must answer "what is this, in what units, plotted against what" without opening the source — enforced by `test/runtests_h5_schema.jl`: + +- **`long_name`** — plain-text physics description. +- **`units`** — SI string (`"T"`, `"Wb/rad"`, `"A"`, `"m"`, `"J"`, `"N*m"`, `"Hz"`, `"Ohm*m"`); `"1"` for dimensionless (CF convention). Normalized quantities state the normalization in `long_name` (e.g. the power-normalized stability energies are per unit ⟨|ξ|²⟩, not joules). +- **`dims`** — required on rank ≥ 2 datasets: a greppable string like `"(psi, m)"` listing axis names in **Julia (column-major) order, axis 1 first**. Note h5py/HDFView users see file dimensions in the reversed (row-major) order. +- **HDF5 Dimension Scales** (the netCDF-4 coordinate mechanism): shared coordinate datasets (`psi` grids, rational-surface `psi`, geometry `xs`/`ys`) are marked with `h5ds_set_scale` and attached per-axis with `h5ds_attach_scale`/`h5ds_set_label`, so h5py `.dims`, xarray, and HDFView resolve axes natively. The H5DS C API indexes file (row-major) dimensions: Julia axis `k` of an `N`-d dataset is C index `N - k`. + +Root-level file attributes: `schema_version` (currently `"2.0"`; bump on breaking schema changes — readers dispatch on it), `Conventions = "GPEC-HDF5-2.0"`, `references`, `title` (run description), `date_created` (ISO 8601 UTC). The code version stays in `Info/git_version`. + +Mechanism: writers stay table-driven — each writer keeps a `path => (; long_name, units, dims)` table next to it (`src/HDF5Schema.jl` for the main writer; alongside `write_galerkin!`, the PerturbedEquilibrium writer, `KineticForces/Output.jl`, and `Tearing/Runner/HDF5Output.jl` for the rest) and applies it post-write via `Utilities.HDF5Annotations.annotate!`. Entries for conditionally-written datasets are simply skipped when absent. When adding a dataset, add its table entry in the same commit — the schema test fails otherwise. + +## File-wide conventions + +- Complex numbers are stored as the native HDF5.jl compound type (readable by h5py as a compound dtype). +- `NaN` is the not-computed sentinel in numeric datasets (e.g. auto-derived settings, rootless growth-rate entries). +- Ragged (variable-length) data uses the flat-plus-`offsets` companion pattern (`offsets[k+1] - offsets[k]` = length of row `k`) rather than HDF5 VLEN types, e.g. `KineticForces//EnergyIntegrals/` and `Tearing/Diagnostics/*`. + +## Back-compatibility policy + +Schema renames are clean breaks in `src/` readers — no dual-path reads. The **only** legacy fallback lives in the regression harness (`regression-harness/src/extractor.jl`, `LEGACY_PREFIX_MAP`), so cross-commit comparisons and `--ref-range` scans keep working across a rename boundary. When renaming a path, update the writer, all readers, the case TOMLs, and add the new→old pair to that map. diff --git a/docs/src/ballooning.md b/docs/src/ballooning.md index 3313e5351..72697da20 100644 --- a/docs/src/ballooning.md +++ b/docs/src/ballooning.md @@ -909,5 +909,5 @@ Mercier calculation and the new `Bal.jl` calculation. The local-stability output now stores ballooning ``\Delta'`` in the fourth `locstab_fs` entry. In the HDF5 output this is written as -`locstab/ballooning_Delta_prime`, distinct from the tearing ``\Delta'`` outputs -under `singular/` and `perturbed_equilibrium/singular_coupling/`. +`LocalStability/ballooning_Delta_prime`, distinct from the tearing ``\Delta'`` outputs +under `SingularSurfaces/` and `PerturbedEquilibrium/SingularCoupling/`. diff --git a/docs/src/citations.md b/docs/src/citations.md index 143b9a493..ea5ddaee3 100644 --- a/docs/src/citations.md +++ b/docs/src/citations.md @@ -36,7 +36,7 @@ The primary reference for the `ForceFreeStates` module. Derives the Euler-Lagran > *Physics of Plasmas* **25**, 032507 (2018). > DOI: [10.1063/1.5007042](https://doi.org/10.1063/1.5007042) -Reformulates the DCON eigenvalue problem as a Riccati matrix ODE, enabling parallel integration across singular surfaces and faster computation. Implemented in `src/ForceFreeStates/Riccati.jl` and enabled via `use_riccati = true` in `[ForceFreeStates]`. +Reformulates the DCON eigenvalue problem as a Riccati matrix ODE, enabling parallel integration across singular surfaces and faster computation. Implemented in `src/ForceFreeStates/Riccati.jl` and enabled via `integrator = "riccati"` in `[ForceFreeStates]`. --- 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/developer_notes.md b/docs/src/developer_notes.md index 885a3de3c..aad8640f7 100644 --- a/docs/src/developer_notes.md +++ b/docs/src/developer_notes.md @@ -69,7 +69,7 @@ Three things follow. First, convergence is per-surface: the outer surface to about 1% — while `dpm[2,2]` is marginal and `dpm[1,1]` (q=2) never settles, still moving ~7% between the two tightest grids. A plateau detector must therefore report per-surface rather than pass/fail for the whole diagonal. -(As the `ldp` scan below shows, q=2 is not inherently unconvergeable — it is the +(As the `rational_packed` (formerly `ldp`) scan below shows, q=2 is not inherently unconvergeable — it is the auto grid that prevents it from settling.) Second, the growth rate is linear in Δ′: `gamma/dpm[1,1]` is 24.1 to within 0.5% @@ -83,7 +83,7 @@ after pass 2, so tightening `psi_accuracy` moves it *further* from self-consistency rather than closer, and the warning's advice to "consider tightening psi_accuracy" is counterproductive in this regime. -The same deck on a deterministic `ldp` grid, which skips the measure-and-re-form +The same deck on a deterministic `rational_packed` grid, which skips the measure-and-re-form step entirely, converges: | mpsi | dpm[1,1] | dpm[2,2] | dpm[3,3] | gamma 2/1 (Hz) | @@ -102,14 +102,14 @@ value should do. The non-convergence under the auto grid is an artifact of the generator, not of the Δ′ extraction. Two consequences. A plateau criterion is implementable today against a fixed -`ldp` grid, without waiting on the auto-grid work. And the auto grid's answers +`rational_packed` grid, without waiting on the auto-grid work. And the auto grid's answers are biased in both directions relative to the converged value: at its default `psi_accuracy` it gave 6.39 (28% low), at its tightest 9.51 (7% high). Anything pinned on the auto grid should be read with that in mind. This is not implemented. Doing it properly needs: - - either a fixed `ldp` grid (which already converges, see above) or knot + - either a fixed `rational_packed` grid (which already converges, see above) or knot refinement iterated to a fixed point (repeat the measure-and-re-form step until `implied_knot_count` stops exceeding the grid in use), since without it the scan target keeps moving; diff --git a/docs/src/equilibrium.md b/docs/src/equilibrium.md index ecff97671..056e99192 100644 --- a/docs/src/equilibrium.md +++ b/docs/src/equilibrium.md @@ -96,18 +96,18 @@ built by a **two-pass measured-curvature refinement** driven by the single accur (no file re-read). Every region's knot count scales as τ^(-1/3), so tightening `psi_accuracy` refines -the core, pedestal, and edge proportionally. The legacy `grid_type = "ldp"` +the core, pedestal, and edge proportionally. The legacy `grid_type = "rational_packed"` (formerly `"ldp"`) (sin²-spaced), `"pow1"`, `"uniform"`, and explicit `mpsi > 0` (single-pass, fixed layout) are still supported. Library users calling `setup_equilibrium` directly with `mpsi = 0` receive the coarse pass-1 grid; use `refined_psi_grid` and the `override_psi_nodes` keyword to apply the refinement manually. -The packing on the DIII-D-like example (n=1) compared to fixed `ldp` grids — note the +The packing on the DIII-D-like example (n=1) compared to fixed `rational_packed` grids — note the coarse spacing across the smooth mid-radius, the spacing dips at each rational surface, and the core/pedestal/edge packing (`benchmarks/plot_grid_knot_placement.jl` regenerates this figure): -![Radial knot packing: auto two-pass vs ldp](assets/grid_knot_placement.png) +![Radial knot packing: auto two-pass vs rational_packed](assets/grid_knot_placement.png) Decomposing the density by source on the same example shows the pedestal band (ψ_N ≈ 0.85–0.98) is driven by *measured* curvature, not the edge floor: the pressure, 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..54234a1a6 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -55,12 +55,12 @@ numerical strategies. columns of ``U_2`` that correspond to resonant modes are zeroed via Gaussian reduction (GR), keeping the solution bounded. This is the reference path for correctness comparisons. -Enable with (default): +Enable with: ```toml [ForceFreeStates] -use_riccati = false -use_parallel = false +integrator = "serial" ``` +(The default is `integrator = "stride"`, the fundamental-matrix path below.) ### Riccati integration @@ -87,8 +87,7 @@ directly in column `ipert_res` — without Gaussian reduction — and renormaliz Enable with: ```toml [ForceFreeStates] -use_riccati = true -use_parallel = false +integrator = "riccati" ``` **Speedup** (benchmarked on reference examples): @@ -126,10 +125,10 @@ The implementation uses a `direction` field on `IntegrationChunk`: crossing chunk. `balance_integration_chunks` preserves this: the sub-chunk closest to the rational surface inherits `direction`, while the earlier sub-chunk always gets `direction=+1`. -Enable with: +Enable with (default): ```toml [ForceFreeStates] -use_parallel = true +integrator = "stride" ``` **Accuracy** (N=26, DIIID-like example): energy eigenvalue within 2% of standard path. @@ -142,26 +141,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 +230,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 @@ -240,8 +239,7 @@ All `ForceFreeStates` options are set in the `[ForceFreeStates]` section of `gpe ```toml [ForceFreeStates] # Integration driver -use_riccati = false # true: Riccati path (faster, same accuracy) -use_parallel = false # true: parallel FM path (multi-thread, large N) +integrator = "stride" # "stride" (default): parallel FM path (multi-thread, Δ' matrix); "riccati": Riccati path; "serial": reference shooting path # Mode space nn_low = 1 # lowest toroidal mode number @@ -307,7 +305,7 @@ metric = FFS.make_metric(equil, intr.mpert) ffit = FFS.make_matrix(equil, intr, metric) # Choose integration driver. The top-level `eulerlagrange_integration` dispatches -# to the parallel or Riccati path based on ctrl.use_parallel / ctrl.use_riccati, +# on ctrl.integrator ("stride", "riccati", or "serial"), # and always returns a 4-tuple (odet, propagators, chunks, S_at_surface_left). odet, _, _, _ = FFS.eulerlagrange_integration(ctrl, equil, ffit, intr) diff --git a/docs/src/utilities.md b/docs/src/utilities.md index 44b80a931..f4ece086b 100644 --- a/docs/src/utilities.md +++ b/docs/src/utilities.md @@ -29,6 +29,15 @@ neoclassical models) used to set the Lundquist number in the tearing stack. Modules = [GeneralizedPerturbedEquilibrium.Utilities.NeoclassicalResistivity] ``` +## HDF5 Annotations + +Self-describing metadata for `gpec.h5` (long_name/units/dims attributes and HDF5 +Dimension Scales); see the metadata contract in `docs/development/hdf5-conventions.md`. + +```@autodocs +Modules = [GeneralizedPerturbedEquilibrium.Utilities.HDF5Annotations] +``` + ## IMAS Output ```@docs 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..300f3bcd1 100644 --- a/examples/DIIID-like_SLAYER_example/gpec.toml +++ b/examples/DIIID-like_SLAYER_example/gpec.toml @@ -14,7 +14,7 @@ psihigh = 0.9995 # Upper limit of normalized flux coordinate mpsi = 0 # Number of radial grid points (0 = auto-compute from psi_accuracy) psi_accuracy = 0.001 # Target absolute error in q for auto-mpsi mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-10 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -33,7 +33,7 @@ force_termination = true # Run FFS + SLAYER, skip PerturbedEquilibrium local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [psiedge, psilim]; integration domain set by qhigh / psihigh / dmlim +dW_edge_scan_start = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [dW_edge_scan_start, psilim]; integration domain set by qhigh / psihigh / dmlim qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)... qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)... sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -52,8 +52,8 @@ 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) -parallel_threads = 1 # serial/bit-deterministic BVP — keeps the regression Δ' (and hence γ) reproducible +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_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 dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim) / n @@ -69,7 +69,7 @@ enabled = true # Run the SLAYER tearing-mode analysis inner_model = "slayer_fitzpatrick" # Inner-layer Δ(Q) model: "slayer_fitzpatrick", "ggj_shooting", or "ggj_galerkin" scan_mode = "amr" # Q-plane scan strategy: "amr" (adaptive refinement) or "brute_force" coupling_mode = "uncoupled" # "uncoupled" (per-surface) or "coupled" (multi-surface determinant) -dc_type = "none" # Critical-Δ offset selector: "none", "lar", "rfitzp", or "toroidal" +delta_crit_type = "none" # Critical-Δ offset selector: "none", "lar", "fitzpatrick", or "toroidal" mu_i = 2.0 # Ion mass in proton-mass units (2.0 = deuterium) zeff = 1.0 # Effective charge chi_perp = 1.0 # fallback only; the kinetic file supplies χ⊥(ψ) diff --git a/examples/DIIID-like_gal_resistive_example/gpec.toml b/examples/DIIID-like_gal_resistive_example/gpec.toml index e296d0585..f017965b9 100644 --- a/examples/DIIID-like_gal_resistive_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_example/gpec.toml @@ -8,13 +8,13 @@ eq_filename = "TkMkr_D3Dlike_Hmode.geqdsk" # Path to equilibrium file eq_type = "efit" # Type of the input 2D equilibrium file jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type (ldp = linear-derivative packing toward rationals) +grid_type = "rational_packed" # Radial grid packing type (rational_packed = sin²-packed radial grid) psilow = 1e-4 # Lower limit of normalized flux coordinate psihigh = 0.993 # Upper limit of normalized flux coordinate (0.993 stays clear of the separatrix; truncating at ≳0.998 is numerically unreasonable here) mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) psi_accuracy = 0.001 # Target relative accuracy of splined profile derivatives for the auto grid mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-10 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -32,7 +32,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes on wall local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [psiedge, psilim]; integration domain set by qhigh / psihigh / dmlim +dW_edge_scan_start = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [dW_edge_scan_start, psilim]; integration domain set by qhigh / psihigh / dmlim qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)... qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)... sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -51,8 +51,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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) dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim) / n (only used when set_psilim_via_dmlim = true) diff --git a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml index 2d2cbe86b..e37976074 100644 --- a/examples/DIIID-like_gal_resistive_pe_example/gpec.toml +++ b/examples/DIIID-like_gal_resistive_pe_example/gpec.toml @@ -7,13 +7,13 @@ eq_filename = "TkMkr_D3Dlike_Hmode.geqdsk" # Path to equilibrium file eq_type = "efit" # Type of the input 2D equilibrium file jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type (ldp = linear-derivative packing toward rationals) +grid_type = "rational_packed" # Radial grid packing type (rational_packed = sin²-packed radial grid) psilow = 1e-4 # Lower limit of normalized flux coordinate psihigh = 0.993 # Upper limit of normalized flux coordinate (0.993 stays clear of the separatrix; truncating at ≳0.998 is numerically unreasonable here) mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) psi_accuracy = 0.001 # Target relative accuracy of splined profile derivatives for the auto grid mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-10 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -31,7 +31,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes on wall local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [psiedge, psilim]; integration domain set by qhigh / psihigh / dmlim +dW_edge_scan_start = 0.99 # Edge dW scan band: diagnostic dW(ψ) computed for ψ ∈ [dW_edge_scan_start, psilim]; integration domain set by qhigh / psihigh / dmlim qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)... qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)... sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -50,8 +50,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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) dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim) / n (only used when set_psilim_via_dmlim = true) @@ -100,4 +100,4 @@ compute_response = true # Compute plasma response to forcing compute_singular_coupling = true # Compute singular layer coupling metrics verbose = true # Enable verbose logging write_outputs_to_HDF5 = true # Write perturbed equilibrium outputs to HDF5 -reg_spot = 0.05 # Regularization width for singular surfaces (0 = disabled) +regularization_width = 0.05 # Regularization width for singular surfaces (0 = disabled) diff --git a/examples/DIIID-like_ideal_example/gpec.toml b/examples/DIIID-like_ideal_example/gpec.toml index 451a7a81f..93a4da7ec 100644 --- a/examples/DIIID-like_ideal_example/gpec.toml +++ b/examples/DIIID-like_ideal_example/gpec.toml @@ -13,7 +13,7 @@ psihigh = 0.995 # Upper limit of normalized poloidal flux (captur mpsi = 0 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) psi_accuracy = 0.001 # Target relative accuracy of splined profile derivatives for the auto grid mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-10 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -31,7 +31,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes on wall local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -50,8 +50,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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) dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) @@ -73,7 +73,7 @@ compute_response = true # Compute plasma response to forcing compute_singular_coupling = true # Compute singular layer coupling metrics verbose = true # Enable verbose logging write_outputs_to_HDF5 = true # Write perturbed equilibrium outputs to HDF5 -reg_spot = 0.05 # Regularization width for singular surfaces (0 = disabled) +regularization_width = 0.05 # Regularization width for singular surfaces (0 = disabled) [KineticForces] kinetic_file = "TkMkr_D3Dlike_Hmode_kinetic.h5" # GPEC HDF5 kinetic schema (psi,n_i,n_e,T_i,T_e,omega_E,+chi_e,chi_phi). Legacy .gpeckf/.kin still readable. diff --git a/examples/LAR_beta_scan/gpec.toml b/examples/LAR_beta_scan/gpec.toml index 6e5c664f6..d75d46e1f 100644 --- a/examples/LAR_beta_scan/gpec.toml +++ b/examples/LAR_beta_scan/gpec.toml @@ -10,7 +10,7 @@ [Equilibrium] eq_type = "tj_analytic" # TJ-analytic model (inverse pipeline; Fitzpatrick https://github.com/rfitzp/TJ) jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 0.01 # Lower limit of normalized poloidal flux psihigh = 0.995 # Upper limit of normalized poloidal flux mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) @@ -52,8 +52,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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 dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) 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..e96f3de2b 100644 --- a/examples/LAR_epsilon_scan/gpec.toml +++ b/examples/LAR_epsilon_scan/gpec.toml @@ -10,7 +10,7 @@ [Equilibrium] eq_type = "tj_analytic" # TJ-analytic model (inverse pipeline; overridden to "tj_analytic_direct" by run_scan.jl) jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 0.01 # Lower limit of normalized poloidal flux psihigh = 0.995 # Upper limit of normalized poloidal flux mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) @@ -53,8 +53,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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 dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) 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..db60498f8 100644 --- a/examples/LAR_ideal_match_test/gpec.toml +++ b/examples/LAR_ideal_match_test/gpec.toml @@ -9,7 +9,7 @@ [Equilibrium] eq_type = "tj_analytic" # Type of the input 2D equilibrium (analytic large-aspect-ratio model) jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type (ldp = linear-derivative packing toward rationals) +grid_type = "rational_packed" # Radial grid packing type (rational_packed = sin²-packed radial grid) psilow = 0.01 # Lower limit of normalized flux coordinate psihigh = 0.995 # Upper limit of normalized flux coordinate mpsi = 128 # Number of radial grid points (0 = auto-compute from psi_accuracy) @@ -50,8 +50,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 is about +20% speedup) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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) dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim) / n (only used when set_psilim_via_dmlim = true) diff --git a/examples/LAR_resistive_match_test/gpec.toml b/examples/LAR_resistive_match_test/gpec.toml index 7fb9f565e..c02191605 100644 --- a/examples/LAR_resistive_match_test/gpec.toml +++ b/examples/LAR_resistive_match_test/gpec.toml @@ -10,7 +10,7 @@ [Equilibrium] eq_type = "tj_analytic" # Type of the input 2D equilibrium (analytic large-aspect-ratio model) jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type (ldp = linear-derivative packing toward rationals) +grid_type = "rational_packed" # Radial grid packing type (rational_packed = sin²-packed radial grid) psilow = 0.01 # Lower limit of normalized flux coordinate psihigh = 0.995 # Upper limit of normalized flux coordinate mpsi = 128 # Number of radial grid points (0 = auto-compute from psi_accuracy) @@ -51,8 +51,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 is about +20% speedup) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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) dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim) / n (only used when set_psilim_via_dmlim = true) diff --git a/examples/Solovev_ideal_example/gpec.toml b/examples/Solovev_ideal_example/gpec.toml index 2ed0654c6..edcfff2c6 100644 --- a/examples/Solovev_ideal_example/gpec.toml +++ b/examples/Solovev_ideal_example/gpec.toml @@ -6,12 +6,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -42,7 +42,7 @@ compute_response = true # Compute plasma response to forcing compute_singular_coupling = true # Compute singular layer coupling metrics verbose = true # Enable verbose logging write_outputs_to_HDF5 = true # Write perturbed equilibrium outputs to HDF5 -reg_spot = 0.05 # Regularization width for singular surfaces (0 = disabled) +regularization_width = 0.05 # Regularization width for singular surfaces (0 = disabled) # Note: SLAYER tearing analysis is not run on the Solovev analytic equilibrium # — its Δ' / inner-layer dispersion does not yield meaningful tearing roots. @@ -53,7 +53,7 @@ reg_spot = 0.05 # Regularization width for singular surf local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -72,8 +72,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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 dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) diff --git a/examples/Solovev_ideal_example_3D/gpec.toml b/examples/Solovev_ideal_example_3D/gpec.toml index 991cfc4dc..1fcf67efb 100644 --- a/examples/Solovev_ideal_example_3D/gpec.toml +++ b/examples/Solovev_ideal_example_3D/gpec.toml @@ -5,12 +5,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -18,7 +18,7 @@ force_termination = false # Terminate after equilibrium setup (ski local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 1.0 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 1.0 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -38,8 +38,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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 dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) diff --git a/examples/Solovev_ideal_example_multi_n/gpec.toml b/examples/Solovev_ideal_example_multi_n/gpec.toml index 3fb3f7b06..1a52f1a93 100644 --- a/examples/Solovev_ideal_example_multi_n/gpec.toml +++ b/examples/Solovev_ideal_example_multi_n/gpec.toml @@ -6,12 +6,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -30,7 +30,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -48,8 +48,8 @@ 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 (multi-n Δ' matrix has open issues — sing_lim! warns and skips — but ξ and energies are valid) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (multi-n Δ' matrix has open issues — sing_lim! warns and skips — but ξ and energies are valid) +integrator_threads = 2 # Stride 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 multi-n — dmlim truncation is ambiguous when n varies (sing_lim! skips anyway) dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) diff --git a/examples/Solovev_ideal_example_multi_n/single_n_1/gpec.toml b/examples/Solovev_ideal_example_multi_n/single_n_1/gpec.toml index 8b2b75007..cc34dfeeb 100644 --- a/examples/Solovev_ideal_example_multi_n/single_n_1/gpec.toml +++ b/examples/Solovev_ideal_example_multi_n/single_n_1/gpec.toml @@ -5,12 +5,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -29,7 +29,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) diff --git a/examples/Solovev_ideal_example_multi_n/single_n_2/gpec.toml b/examples/Solovev_ideal_example_multi_n/single_n_2/gpec.toml index c7a51c723..e20a0145d 100644 --- a/examples/Solovev_ideal_example_multi_n/single_n_2/gpec.toml +++ b/examples/Solovev_ideal_example_multi_n/single_n_2/gpec.toml @@ -5,12 +5,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 128 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -29,7 +29,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) diff --git a/examples/Solovev_kinetic_NTV_example/gpec.toml b/examples/Solovev_kinetic_NTV_example/gpec.toml index 7812d4f9b..22d95f19e 100644 --- a/examples/Solovev_kinetic_NTV_example/gpec.toml +++ b/examples/Solovev_kinetic_NTV_example/gpec.toml @@ -8,12 +8,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, other) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 128 # Number of radial grid points (0 = auto-compute from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -44,13 +44,13 @@ compute_response = true # Compute plasma response to forcing compute_singular_coupling = true # Compute singular layer coupling metrics verbose = true # Enable verbose logging write_outputs_to_HDF5 = true # Write perturbed equilibrium outputs to HDF5 -reg_spot = 0.05 # Regularization width for singular surfaces (0 = disabled) +regularization_width = 0.05 # Regularization width for singular surfaces (0 = disabled) [ForceFreeStates] local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -69,8 +69,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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 dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) diff --git a/examples/Solovev_kinetic_calculated_example/gpec.toml b/examples/Solovev_kinetic_calculated_example/gpec.toml index 149f40af7..0b2e1dab0 100644 --- a/examples/Solovev_kinetic_calculated_example/gpec.toml +++ b/examples/Solovev_kinetic_calculated_example/gpec.toml @@ -6,12 +6,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized flux coordinate psihigh = 0.9995 # Upper limit of normalized flux coordinate mpsi = 16 # Number of radial grid points mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -29,7 +29,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW scan band: dW(ψ) computed for ψ ∈ [psiedge, psilim], integration truncated at peak +dW_edge_scan_start = 0.99 # Edge dW scan band: dW(ψ) computed for ψ ∈ [dW_edge_scan_start, psilim], integration truncated at peak qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)... qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)... sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) diff --git a/examples/a10_kinetic_example/gpec.toml b/examples/a10_kinetic_example/gpec.toml index d3f61b391..a2855eaaf 100644 --- a/examples/a10_kinetic_example/gpec.toml +++ b/examples/a10_kinetic_example/gpec.toml @@ -10,12 +10,12 @@ eq_filename = "fix_a100_k10_q2_bn010_prof1" # Path to equilibrium file eq_type = "efit" # Type of the input 2D equilibrium file jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-3 # Lower limit of normalized poloidal flux psihigh = 0.99 # Upper limit of normalized poloidal flux mpsi = 16 # Number of radial grid points (low value for fast iteration) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver [Wall] @@ -26,7 +26,7 @@ local_stability_flag = true # Perform local stability analysis (Mercier and b vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes set_psilim_via_dmlim = false # FALSE for limited/analytical equilibria — rationals sparse, dmlim would chop too much edge -psiedge = 1.0 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 1.0 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -55,6 +55,6 @@ zimp = 6 # Impurity charge mimp = 12 # Impurity mass electron = false # Include electron contribution (false = ion-only) nutype = "harmonic" # Collision operator (zero, small, krook, harmonic) -f0type = "maxwellian" # Distribution function (maxwellian, jkp, cgl) +f0type = "maxwellian" # Distribution function (maxwellian, park, cgl) atol_xlmda = 1e-9 # Absolute tolerance for inner pitch + energy integrations rtol_xlmda = 1e-5 # Relative tolerance for inner pitch + energy integrations diff --git a/regression-harness/cases/diiid_n1.toml b/regression-harness/cases/diiid_n1.toml index 0e0336a99..5827b6942 100644 --- a/regression-harness/cases/diiid_n1.toml +++ b/regression-harness/cases/diiid_n1.toml @@ -10,7 +10,7 @@ example_dir = "examples/DIIID-like_ideal_example" # Energies — leading generalized (W,N) pencil eigenvalues at the final truncation (psilim): power-normalized (⟨|ξ|²⟩ = 1 metric) and invariant to the working-coordinate Jacobian. [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "total energy Re(et[1])" @@ -18,7 +18,7 @@ noise_threshold = 1e-10 order = 10 [quantities.et_imag] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "imag_first" label = "total energy Im(et[1])" @@ -26,7 +26,7 @@ noise_threshold = 1e-10 order = 11 [quantities.ep_real] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "real_first" label = "plasma energy Re(ep[1])" @@ -34,7 +34,7 @@ noise_threshold = 1e-10 order = 12 [quantities.ev_real] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "real_first" label = "vacuum energy Re(ev[1])" @@ -42,7 +42,7 @@ noise_threshold = 1e-10 order = 13 [quantities.vacuum_eigenvalue] -h5path = "FreeBoundaryStability/vacuum_eigenvalue" +h5path = "ForceFreeStates/FreeBoundaryStability/vacuum_eigenvalue" type = "real_scalar" extract = "value" label = "vacuum matrix min eigenvalue" @@ -51,7 +51,7 @@ order = 14 # Energies — full eigenvalue arrays [quantities.ep_all] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "all_complex" label = "plasma energy (all)" @@ -59,7 +59,7 @@ noise_threshold = 1e-10 order = 20 [quantities.ev_all] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "all_complex" label = "vacuum energy (all)" @@ -67,7 +67,7 @@ noise_threshold = 1e-10 order = 21 [quantities.et_all] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "all_complex" label = "total energy (all)" @@ -76,7 +76,7 @@ order = 22 # Integration [quantities.nstep] -h5path = "integration/nstep" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep" type = "int_scalar" extract = "value" label = "ODE steps (saved)" @@ -84,7 +84,7 @@ noise_threshold = 0 order = 30 [quantities.nstep_total] -h5path = "integration/nstep_total" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep_total" type = "int_scalar" extract = "value" label = "ODE steps (total)" @@ -93,7 +93,7 @@ order = 31 # Equilibrium [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" @@ -101,7 +101,7 @@ noise_threshold = 1e-12 order = 40 [quantities.q95] -h5path = "equil/q95" +h5path = "Equilibrium/q95" type = "real_scalar" extract = "value" label = "q95" @@ -109,7 +109,7 @@ noise_threshold = 1e-12 order = 41 [quantities.betat] -h5path = "equil/betat" +h5path = "Equilibrium/betat" type = "real_scalar" extract = "value" label = "beta_t" @@ -117,7 +117,7 @@ noise_threshold = 1e-12 order = 42 [quantities.betan] -h5path = "equil/betan" +h5path = "Equilibrium/betan" type = "real_scalar" extract = "value" label = "beta_n" @@ -125,7 +125,7 @@ noise_threshold = 1e-12 order = 43 [quantities.li1] -h5path = "equil/li1" +h5path = "Equilibrium/li1" type = "real_scalar" extract = "value" label = "internal inductance li1" @@ -133,7 +133,7 @@ noise_threshold = 1e-12 order = 44 [quantities.li2] -h5path = "equil/li2" +h5path = "Equilibrium/li2" type = "real_scalar" extract = "value" label = "internal inductance li2" @@ -141,7 +141,7 @@ noise_threshold = 1e-12 order = 45 [quantities.li3] -h5path = "equil/li3" +h5path = "Equilibrium/li3" type = "real_scalar" extract = "value" label = "internal inductance li3" @@ -149,7 +149,7 @@ noise_threshold = 1e-12 order = 46 [quantities.betap1] -h5path = "equil/betap1" +h5path = "Equilibrium/betap1" type = "real_scalar" extract = "value" label = "poloidal beta betap1" @@ -157,7 +157,7 @@ noise_threshold = 1e-12 order = 47 [quantities.betap2] -h5path = "equil/betap2" +h5path = "Equilibrium/betap2" type = "real_scalar" extract = "value" label = "poloidal beta betap2" @@ -165,7 +165,7 @@ noise_threshold = 1e-12 order = 48 [quantities.betap3] -h5path = "equil/betap3" +h5path = "Equilibrium/betap3" type = "real_scalar" extract = "value" label = "poloidal beta betap3" @@ -173,7 +173,7 @@ noise_threshold = 1e-12 order = 49 [quantities.betaj] -h5path = "equil/betaj" +h5path = "Equilibrium/betaj" type = "real_scalar" extract = "value" label = "current beta betaj" @@ -181,7 +181,7 @@ noise_threshold = 1e-12 order = 57 [quantities.volume] -h5path = "equil/volume" +h5path = "Equilibrium/volume" type = "real_scalar" extract = "value" label = "plasma volume" @@ -189,7 +189,7 @@ noise_threshold = 1e-12 order = 58 [quantities.crnt] -h5path = "equil/crnt" +h5path = "Equilibrium/crnt" type = "real_scalar" extract = "value" label = "plasma current" @@ -197,7 +197,7 @@ noise_threshold = 1e-12 order = 59 [quantities.bt0] -h5path = "equil/bt0" +h5path = "Equilibrium/bt0" type = "real_scalar" extract = "value" label = "toroidal field bt0" @@ -205,7 +205,7 @@ noise_threshold = 1e-12 order = 62 [quantities.bwall] -h5path = "equil/bwall" +h5path = "Equilibrium/bwall" type = "real_scalar" extract = "value" label = "wall field bwall" @@ -213,7 +213,7 @@ noise_threshold = 1e-12 order = 63 [quantities.aratio] -h5path = "equil/aratio" +h5path = "Equilibrium/aratio" type = "real_scalar" extract = "value" label = "aspect ratio" @@ -221,7 +221,7 @@ noise_threshold = 1e-12 order = 64 [quantities.kappa] -h5path = "equil/kappa" +h5path = "Equilibrium/kappa" type = "real_scalar" extract = "value" label = "elongation kappa" @@ -230,7 +230,7 @@ order = 65 # Singular surfaces [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" @@ -238,7 +238,7 @@ noise_threshold = 0 order = 50 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" @@ -246,7 +246,7 @@ noise_threshold = 1e-8 order = 51 [quantities.sing_q] -h5path = "singular/q" +h5path = "SingularSurfaces/q" type = "real_vector" extract = "all_real" label = "singular q values" @@ -255,7 +255,7 @@ order = 52 # Mode numbers [quantities.mpert] -h5path = "info/mpert" +h5path = "Info/mpert" type = "int_scalar" extract = "value" label = "mpert" @@ -263,7 +263,7 @@ noise_threshold = 0 order = 60 [quantities.npert] -h5path = "info/npert" +h5path = "Info/npert" type = "int_scalar" extract = "value" label = "npert" @@ -276,7 +276,7 @@ order = 61 # Per-surface Δ' is now de-emphasized — see PR 178 notes — and SingularCoupling # instead reads this BVP matrix diagonal. [quantities.delta_prime] -h5path = "singular/delta_prime_matrix" +h5path = "SingularSurfaces/delta_prime_matrix" type = "complex_matrix" extract = "diagonal_complex" label = "delta prime (BVP diagonal)" @@ -284,7 +284,7 @@ noise_threshold = 1e-8 order = 80 [quantities.island_half_width] -h5path = "perturbed_equilibrium/singular_coupling/island_half_width" +h5path = "PerturbedEquilibrium/SingularCoupling/island_half_width" type = "real_vector" extract = "all_real" label = "island half-widths" @@ -292,7 +292,7 @@ noise_threshold = 1e-8 order = 81 [quantities.chirikov_parameter] -h5path = "perturbed_equilibrium/singular_coupling/chirikov_parameter" +h5path = "PerturbedEquilibrium/SingularCoupling/chirikov_parameter" type = "real_vector" extract = "all_real" label = "Chirikov parameter" @@ -300,7 +300,7 @@ noise_threshold = 1e-8 order = 82 [quantities.resonant_area_weighted_field] -h5path = "perturbed_equilibrium/singular_coupling/resonant_area_weighted_field" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_area_weighted_field" type = "complex_vector" extract = "norm" label = "||resonant area-weighted field||" @@ -308,7 +308,7 @@ noise_threshold = 1e-8 order = 83 [quantities.resonant_area_weighted_field_all] -h5path = "perturbed_equilibrium/singular_coupling/resonant_area_weighted_field" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_area_weighted_field" type = "complex_vector" extract = "all_complex" label = "resonant area-weighted field b^r" @@ -316,7 +316,7 @@ noise_threshold = 1e-8 # Perturbed equilibrium: energies [quantities.pe_plasma_energy] -h5path = "perturbed_equilibrium/energies/plasma_energy" +h5path = "PerturbedEquilibrium/Energies/plasma_energy" type = "real_scalar" extract = "value" label = "PE plasma energy" @@ -324,7 +324,7 @@ noise_threshold = 1e-10 order = 91 [quantities.pe_vacuum_energy] -h5path = "perturbed_equilibrium/energies/vacuum_energy" +h5path = "PerturbedEquilibrium/Energies/vacuum_energy" type = "real_scalar" extract = "value" label = "PE vacuum energy" @@ -332,7 +332,7 @@ noise_threshold = 1e-10 order = 92 [quantities.pe_surface_energy] -h5path = "perturbed_equilibrium/energies/surface_energy" +h5path = "PerturbedEquilibrium/Energies/surface_energy" type = "real_scalar" extract = "value" label = "PE surface energy" @@ -340,7 +340,7 @@ noise_threshold = 1e-10 order = 93 [quantities.pe_toroidal_torque] -h5path = "perturbed_equilibrium/energies/toroidal_torque" +h5path = "PerturbedEquilibrium/Energies/toroidal_torque" type = "real_scalar" extract = "value" label = "PE toroidal torque" @@ -353,7 +353,7 @@ order = 94 # section. Threshold is a noise floor for the adaptive-quadrature result; tune if # run-to-run jitter appears. [quantities.ntv_torque_fgar] -h5path = "kinetic_forces/fgar/total_torque" +h5path = "KineticForces/fgar/total_torque" type = "real_scalar" extract = "value" label = "NTV torque FGAR [N·m]" @@ -364,7 +364,7 @@ order = 95 # Tracked at a tighter threshold — it isolates the energy component the # bounce-averaging chain moves most. [quantities.ntv_energy_fgar] -h5path = "kinetic_forces/fgar/total_energy" +h5path = "KineticForces/fgar/total_energy" type = "real_scalar" extract = "value" label = "NTV kinetic energy dW FGAR [J]" @@ -373,7 +373,7 @@ order = 96 # Profiles (checksums) [quantities.q_profile] -h5path = "splines/profiles/q" +h5path = "Equilibrium/Profiles/q" type = "real_vector" extract = "checksum" label = "q profile (checksum)" @@ -381,7 +381,7 @@ noise_threshold = 0 order = 70 [quantities.pressure_profile] -h5path = "splines/profiles/mu0p" +h5path = "Equilibrium/Profiles/mu0p" type = "real_vector" extract = "checksum" label = "pressure profile (checksum)" @@ -391,7 +391,7 @@ order = 71 # Local stability profiles (local_stability_flag = true). Checksums hash the full # profile so they are robust to NaN edge-surface entries and catch any change. [quantities.locstab_di] -h5path = "locstab/di" +h5path = "LocalStability/di" type = "real_vector" extract = "checksum" label = "Mercier D_I profile (checksum)" @@ -399,7 +399,7 @@ noise_threshold = 0 order = 72 [quantities.locstab_dr] -h5path = "locstab/dr" +h5path = "LocalStability/dr" type = "real_vector" extract = "checksum" label = "resistive interchange D_R profile (checksum)" @@ -407,7 +407,7 @@ noise_threshold = 0 order = 73 [quantities.locstab_ballooning_dp] -h5path = "locstab/ballooning_Delta_prime" +h5path = "LocalStability/ballooning_Delta_prime" type = "real_vector" extract = "checksum" label = "ballooning Delta' profile (checksum)" diff --git a/regression-harness/cases/diiid_slayer_n1.toml b/regression-harness/cases/diiid_slayer_n1.toml index 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..8158c41e0 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,7 +33,7 @@ order = 20 # Chirikov overlap parameter per surface [quantities.pe_chirikov] -h5path = "perturbed_equilibrium/singular_coupling/chirikov_parameter" +h5path = "PerturbedEquilibrium/SingularCoupling/chirikov_parameter" type = "real_vector" extract = "all_real" label = "Chirikov parameter" @@ -42,7 +42,7 @@ order = 21 # Resonant flux Φ_res per surface (applied) — norm + per-surface complex values [quantities.pe_resonant_flux_norm] -h5path = "perturbed_equilibrium/singular_coupling/resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_flux" type = "complex_vector" extract = "norm" label = "||resonant flux||" @@ -50,7 +50,7 @@ noise_threshold = 1e-6 order = 30 [quantities.pe_resonant_flux_all] -h5path = "perturbed_equilibrium/singular_coupling/resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/resonant_flux" type = "complex_vector" extract = "all_complex" label = "resonant flux Phi_res" @@ -59,7 +59,7 @@ order = 31 # Penetrated (reconnected) resonant field per surface — the resistive shielding signature [quantities.pe_penetrated_field] -h5path = "perturbed_equilibrium/singular_coupling/penetrated_field" +h5path = "PerturbedEquilibrium/SingularCoupling/penetrated_field" type = "complex_vector" extract = "all_complex" label = "penetrated field" @@ -68,7 +68,7 @@ order = 32 # Per-surface Δ' from the PE singular-coupling analysis [quantities.pe_delta_prime] -h5path = "perturbed_equilibrium/singular_coupling/delta_prime" +h5path = "PerturbedEquilibrium/SingularCoupling/delta_prime" type = "complex_vector" extract = "all_complex" label = "PE Δ' per surface" @@ -77,7 +77,7 @@ order = 33 # Resonant-flux coupling matrix (n_rational × numpert_total) — Frobenius norm, catches broader drift [quantities.pe_C_resonant_flux_norm] -h5path = "perturbed_equilibrium/singular_coupling/C_resonant_flux" +h5path = "PerturbedEquilibrium/SingularCoupling/C_resonant_flux" type = "complex_matrix" extract = "norm" label = "||C resonant flux||" diff --git a/regression-harness/cases/solovev_kinetic_calculated.toml b/regression-harness/cases/solovev_kinetic_calculated.toml index e5ebb29ef..3f10c8599 100644 --- a/regression-harness/cases/solovev_kinetic_calculated.toml +++ b/regression-harness/cases/solovev_kinetic_calculated.toml @@ -9,28 +9,28 @@ example_dir = "examples/Solovev_kinetic_calculated_example" # Energies — leading eigenvalues [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "total energy Re(et[1])" noise_threshold = 1e-10 [quantities.et_imag] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "imag_first" label = "total energy Im(et[1])" noise_threshold = 1e-10 [quantities.ep_real] -h5path = "FreeBoundaryStability/eigenmode_plasma_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" type = "complex_vector" extract = "real_first" label = "plasma energy Re(ep[1])" noise_threshold = 1e-10 [quantities.ev_real] -h5path = "FreeBoundaryStability/eigenmode_vacuum_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" type = "complex_vector" extract = "real_first" label = "vacuum energy Re(ev[1])" @@ -38,7 +38,7 @@ noise_threshold = 1e-10 # Energies — full eigenvalue arrays [quantities.et_all] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "all_complex" label = "total energy (all)" @@ -46,14 +46,14 @@ noise_threshold = 1e-10 # Integration [quantities.nstep] -h5path = "integration/nstep" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep" type = "int_scalar" extract = "value" label = "ODE steps (saved)" noise_threshold = 0 [quantities.nstep_total] -h5path = "integration/nstep_total" +h5path = "ForceFreeStates/Solutions/ForwardIntegration/nstep_total" type = "int_scalar" extract = "value" label = "ODE steps (total)" @@ -61,14 +61,14 @@ noise_threshold = 0 # Equilibrium [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" noise_threshold = 1e-12 [quantities.q95] -h5path = "equil/q95" +h5path = "Equilibrium/q95" type = "real_scalar" extract = "value" label = "q95" @@ -76,44 +76,36 @@ noise_threshold = 1e-12 # Singular surfaces [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" noise_threshold = 0 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" noise_threshold = 1e-8 [quantities.sing_q] -h5path = "singular/q" +h5path = "SingularSurfaces/q" type = "real_vector" extract = "all_real" label = "singular q values" noise_threshold = 1e-8 -# Kinetic-specific -[quantities.kinetic_factor] -h5path = "kinetic/kinetic_factor" -type = "real_scalar" -extract = "value" -label = "kinetic_factor" -noise_threshold = 0 - # Mode numbers [quantities.mpert] -h5path = "info/mpert" +h5path = "Info/mpert" type = "int_scalar" extract = "value" label = "mpert" noise_threshold = 0 [quantities.npert] -h5path = "info/npert" +h5path = "Info/npert" type = "int_scalar" extract = "value" label = "npert" diff --git a/regression-harness/cases/solovev_kinetic_ntv.toml b/regression-harness/cases/solovev_kinetic_ntv.toml index 18f5a8144..cfdac6492 100644 --- a/regression-harness/cases/solovev_kinetic_ntv.toml +++ b/regression-harness/cases/solovev_kinetic_ntv.toml @@ -12,7 +12,7 @@ example_dir = "examples/Solovev_kinetic_NTV_example" # converges to rtol_psi=1e-2, but the result is deterministic for fixed code + inputs, so # the noise floor is set near FP noise and any real movement is surfaced. [quantities.ntv_torque] -h5path = "kinetic_forces/fgar/total_torque" +h5path = "KineticForces/fgar/total_torque" type = "real_vector" extract = "all_real" label = "NTV torque fgar [Re, Im]" @@ -20,7 +20,7 @@ noise_threshold = 1e-8 order = 10 [quantities.ntv_psi_nsteps] -h5path = "kinetic_forces/fgar/psi_nsteps" +h5path = "KineticForces/fgar/psi_nsteps" type = "int_scalar" extract = "value" label = "NTV ψ quadrature evaluations" @@ -29,7 +29,7 @@ order = 11 # Stability anchors — confirm the upstream FFS/PE stages feeding the NTV diagnostic. [quantities.et_real] -h5path = "FreeBoundaryStability/eigenmode_energies" +h5path = "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" type = "complex_vector" extract = "real_first" label = "root-area-weighted total energy Re(et[1])" @@ -37,7 +37,7 @@ noise_threshold = 1e-10 order = 20 [quantities.msing] -h5path = "singular/msing" +h5path = "SingularSurfaces/msing" type = "int_scalar" extract = "value" label = "# singular surfaces" @@ -45,7 +45,7 @@ noise_threshold = 0 order = 21 [quantities.sing_psi] -h5path = "singular/psi" +h5path = "SingularSurfaces/psi" type = "real_vector" extract = "all_real" label = "singular psi locations" @@ -53,7 +53,7 @@ noise_threshold = 1e-8 order = 22 [quantities.q0] -h5path = "equil/q0" +h5path = "Equilibrium/q0" type = "real_scalar" extract = "value" label = "q0" diff --git a/regression-harness/cases/solovev_kinetic_nuzero.toml b/regression-harness/cases/solovev_kinetic_nuzero.toml index 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..c028dba81 100644 --- a/regression-harness/src/extractor.jl +++ b/regression-harness/src/extractor.jl @@ -2,6 +2,70 @@ HDF5 quantity extraction engine. """ +# Legacy-path fallback: outputs written before the module-mirroring CamelCase schema +# use the old group names on the right. Case TOMLs always carry the new paths; when a +# path is missing (the output came from a pre-rename ref), the translated legacy path +# is retried so cross-commit comparisons and --ref-range scans work across the +# boundary. First matching prefix wins — keep more-specific entries first. +const LEGACY_PREFIX_MAP = [ + "Input/RawInputs/Equilibrium" => "input/raw_inputs/equilibrium", + "Input/RawInputs/ForcingTerms" => "input/raw_inputs/forcing_terms", + "Input/RawInputs/Coils" => "input/raw_inputs/coils", + "Input/" => "input/", + "Info/" => "info/", + "Equilibrium/Profiles/" => "splines/profiles/", + "Equilibrium/Geometry/" => "splines/rzphi/", + "Equilibrium/" => "equil/", + "ForceFreeStates/Solutions/ForwardIntegration/" => "integration/", + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/" => "galerkin/solution/", + "ForceFreeStates/Solutions/GalerkinIntegration/Match/InnerParams/" => "galerkin/match/inner_params/", + "ForceFreeStates/Solutions/GalerkinIntegration/Match/Inner/" => "galerkin/match/inner/", + "ForceFreeStates/Solutions/GalerkinIntegration/Match/" => "galerkin/match/", + "ForceFreeStates/Solutions/GalerkinIntegration/" => "galerkin/", + "ForceFreeStates/EulerLagrangeMatrices/Ideal/" => "matrices/ideal/", + "ForceFreeStates/EulerLagrangeMatrices/Kinetic/" => "matrices/kinetic/", + "ForceFreeStates/EulerLagrangeMatrices/" => "matrices/", + "ForceFreeStates/FreeBoundaryStability/" => "FreeBoundaryStability/", + "ForceFreeStates/EdgeScan/" => "EdgeScan/", + "LocalStability/" => "locstab/", + "SingularSurfaces/GalerkinDeltaPrime/" => "galerkin/", + "SingularSurfaces/Kinetic/" => "singular/kinetic/", + "SingularSurfaces/" => "singular/", + "PerturbedEquilibrium/ForcingModes/" => "perturbed_equilibrium/forcing_modes/", + "PerturbedEquilibrium/ResponseMatrices/" => "perturbed_equilibrium/response_matrices/", + "PerturbedEquilibrium/Response/" => "perturbed_equilibrium/response/", + "PerturbedEquilibrium/SingularCoupling/" => "perturbed_equilibrium/singular_coupling/", + "PerturbedEquilibrium/Energies/" => "perturbed_equilibrium/energies/", + "PerturbedEquilibrium/" => "perturbed_equilibrium/", + "KineticForces/" => "kinetic_forces/", + "Tearing/PerSurface/DpMatrix/" => "slayer/per_surface/dp_matrix/", + "Tearing/PerSurface/" => "slayer/per_surface/", + "Tearing/Roots/" => "slayer/roots/", + "Tearing/LayerWidths/" => "slayer/layer_widths/", + "Tearing/Diagnostics/ValidRoots/" => "slayer/diagnostics/valid_roots/", + "Tearing/Diagnostics/Poles/" => "slayer/diagnostics/poles/", + "Tearing/Diagnostics/FilteredRoots/" => "slayer/diagnostics/filtered_roots/", + "Tearing/Scan/Surface_" => "slayer/scan/surface_", + "Tearing/" => "slayer/", +] + +""" +Translate a new-schema h5path to its pre-rename legacy equivalent, or return +`nothing` when no mapping applies. +""" +function legacy_h5path(path::String) + for (new, old) in LEGACY_PREFIX_MAP + if startswith(path, new) + legacy = replace(path, new => old; count=1) + # Structural renames inside KineticForces (not plain prefix swaps). + legacy = replace(legacy, "/EnergyIntegrals/" => "/records/") + legacy = replace(legacy, r"^kinetic_forces/(\w+)/KineticMatrices/" => s"kinetic_forces/matrices_\1/") + return legacy + end + end + return nothing +end + """ Extract all quantities from a gpec.h5 file according to case spec. Returns a Vector{ExtractedQuantity}. @@ -19,8 +83,13 @@ function extract_quantities(h5path::String, qty_specs::Vector{QuantitySpec}, run continue end - # Check if the H5 path exists - if !haskey(fid, spec.h5path) + # Resolve the H5 path, falling back to the pre-rename legacy schema. + path = spec.h5path + if !haskey(fid, path) + legacy = legacy_h5path(path) + path = (legacy !== nothing && haskey(fid, legacy)) ? legacy : nothing + end + if path === nothing push!(results, ExtractedQuantity( spec.name, spec.label, nothing, nothing, nothing, @@ -28,7 +97,7 @@ function extract_quantities(h5path::String, qty_specs::Vector{QuantitySpec}, run continue end - raw = read(fid[spec.h5path]) + raw = read(fid[path]) eq = apply_extraction(spec, raw) push!(results, eq) end @@ -91,7 +160,7 @@ function apply_extraction(spec::QuantitySpec, raw)::ExtractedQuantity elseif spec.extract == "diagonal_complex" # Extract the diagonal of a square matrix as a complex array. - # Use for tracking per-surface BVP Δ' from singular/delta_prime_matrix. + # Use for tracking per-surface BVP Δ' from SingularSurfaces/delta_prime_matrix. ndims(raw) == 2 && size(raw, 1) == size(raw, 2) || error("diagonal_complex requires a square 2-D matrix; got size $(size(raw))") diag_vec = [raw[i, i] for i in 1:size(raw, 1)] diff --git a/regression-harness/src/runner.jl b/regression-harness/src/runner.jl index 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..8448acbce 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") @@ -162,7 +162,7 @@ end Plot the edge stability scan energy components (et, ep, ev, evonly) vs ψ_N. The edge scan evaluates `δW_total = δW_plasma + δW_vacuum` at each stored integration step -in the region [psiedge, psilim], with the plasma boundary swept from psiedge to psilim. +in the region [dW_edge_scan_start, psilim], with the plasma boundary swept from dW_edge_scan_start to psilim. A positive et indicates stability; the truncation point is chosen at the peak et. Four subplots are shown: @@ -177,7 +177,7 @@ A horizontal dashed line at zero marks the stability boundary. A vertical dashed ### Arguments - - `h5path`: Path to a GPEC HDF5 output file produced with `psiedge < psilim` + - `h5path`: Path to a GPEC HDF5 output file produced with `dW_edge_scan_start < psilim` ### Keyword arguments @@ -191,20 +191,20 @@ 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 - @warn "No edge_scan group in $h5path. Run with psiedge < psilim to generate it." + @warn "No edge_scan group in $h5path. Run with dW_edge_scan_start < psilim to generate it." return nothing end @@ -264,9 +264,9 @@ A `Plots.jl` plot object. """ function plot_eigenvalues(h5path; matrix_type=:total, save_path=nothing) dataset = Dict( - :total => "FreeBoundaryStability/eigenmode_energies", - :plasma => "FreeBoundaryStability/eigenmode_plasma_energies", - :vacuum => "FreeBoundaryStability/eigenmode_vacuum_energies" + :total => "ForceFreeStates/FreeBoundaryStability/eigenmode_energies", + :plasma => "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies", + :vacuum => "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" ) haskey(dataset, matrix_type) || error("matrix_type must be :total, :plasma, or :vacuum") @@ -321,9 +321,9 @@ A `Plots.jl` plot object. """ function plot_delta_prime(h5path; save_path=nothing) msing, psi_sing, q_sing, ca_l, ca_r, psio, mn_index = h5open(h5path, "r") do fid - read(fid["singular/msing"]), read(fid["singular/psi"]), read(fid["singular/q"]), - read(fid["singular/ca_left"]), read(fid["singular/ca_right"]), - read(fid["equil/psio"]), read(fid["info/mn_index"]) + read(fid["SingularSurfaces/msing"]), read(fid["SingularSurfaces/psi"]), read(fid["SingularSurfaces/q"]), + read(fid["SingularSurfaces/ca_left"]), read(fid["SingularSurfaces/ca_right"]), + read(fid["Equilibrium/psio"]), read(fid["Info/mn_index"]) end msing == 0 && return plot(; title="No singular surfaces found", legend=false) @@ -374,8 +374,8 @@ end Plot the BALOO-style infinite-n ballooning stability diagram: the experimental pressure gradient α (solid) and the first stability boundary α_crit (dashed) versus normalized poloidal flux ψ_N. Surfaces where the experimental α lies above the boundary -are ballooning-unstable. Reads `locstab/psi`, `locstab/alpha`, and -`locstab/alpha_critical` (populated when ForceFreeStates runs with +are ballooning-unstable. Reads `LocalStability/psi`, `LocalStability/alpha`, and +`LocalStability/alpha_critical` (populated when ForceFreeStates runs with `local_stability_flag = true`). ### Arguments @@ -394,8 +394,8 @@ A `Plots.jl` plot object. """ function plot_ballooning_alpha_boundary(h5path; save_path=nothing, psi_min=0.0) psi, alpha, alpha_crit = h5open(h5path, "r") do fid - haskey(fid, "locstab/alpha") || return (Float64[], Float64[], Float64[]) - read(fid["locstab/psi"]), read(fid["locstab/alpha"]), read(fid["locstab/alpha_critical"]) + haskey(fid, "LocalStability/alpha") || return (Float64[], Float64[], Float64[]) + read(fid["LocalStability/psi"]), read(fid["LocalStability/alpha"]), read(fid["LocalStability/alpha_critical"]) end isempty(alpha) && return plot(; title="No local stability data (set local_stability_flag)", legend=false) @@ -424,7 +424,7 @@ end plot_cond_fbar(h5path; save_path=nothing, zoom=false) Plot `cond(F̄)` vs ψ from the kinetic-singular-surface scan stored in -`singular/kinetic/` (populated when ForceFreeStates runs with +`SingularSurfaces/Kinetic/` (populated when ForceFreeStates runs with `kinetic_factor > 0`, `singfac_min > 0`). `F̄` is the kinetic Euler-Lagrange matrix formed by Schur-reducing the six @@ -457,16 +457,16 @@ A `Plots.jl` plot object, or `nothing` if no kinetic scan is stored in the file. """ function plot_cond_fbar(h5path; save_path=nothing, zoom=false) scan_psi, scan_cond, thr, k_psi, i_psi, i_q, kmsing = h5open(h5path, "r") do fid - if !(haskey(fid, "singular") && haskey(fid["singular"], "kinetic")) + if !(haskey(fid, "SingularSurfaces") && haskey(fid["SingularSurfaces"], "Kinetic")) return Float64[], Float64[], 0.0, Float64[], Float64[], Float64[], 0 end - kg = fid["singular/kinetic"] + kg = fid["SingularSurfaces/Kinetic"] (read(kg["scan_psi"]), read(kg["scan_cond"]), read(kg["scan_threshold"]), read(kg["psi"]), - read(fid["singular/psi"]), - read(fid["singular/q"]), + read(fid["SingularSurfaces/psi"]), + read(fid["SingularSurfaces/q"]), read(kg["kmsing"])) end @@ -553,8 +553,8 @@ A `Plots.jl` plot object. """ function plot_ffs_summary(h5path; save_path=nothing) has_vac = h5open(h5path, "r") do fid - haskey(fid, "FreeBoundaryStability/W_freeboundary_eigenmodes") && - !isempty(read(fid["FreeBoundaryStability/W_freeboundary_eigenmodes"])) + haskey(fid, "ForceFreeStates/FreeBoundaryStability/W_freeboundary_eigenmodes") && + !isempty(read(fid["ForceFreeStates/FreeBoundaryStability/W_freeboundary_eigenmodes"])) end p_crit = plot_fixed_boundary_stability_criterion(h5path) diff --git a/src/Analysis/PerturbedEquilibrium.jl b/src/Analysis/PerturbedEquilibrium.jl index f1219a2e9..da005a5c2 100644 --- a/src/Analysis/PerturbedEquilibrium.jl +++ b/src/Analysis/PerturbedEquilibrium.jl @@ -2,7 +2,7 @@ PerturbedEquilibrium Post-processing and visualization functions for GPEC perturbed equilibrium results stored -in the `perturbed_equilibrium/` group of a GPEC HDF5 output file. +in the `PerturbedEquilibrium/` group of a GPEC HDF5 output file. """ module PerturbedEquilibrium @@ -10,6 +10,10 @@ using HDF5 using LaTeXStrings using Plots +# HDF5 group prefixes shared by every reader in this module. +const PE_SINGULAR_COUPLING = "PerturbedEquilibrium/SingularCoupling/" +const PE_RESPONSE = "PerturbedEquilibrium/Response/" + # Check that a PE dataset exists and is non-empty. function _has_pe_data(h5path, key) h5open(h5path, "r") do fid @@ -25,7 +29,7 @@ per toroidal mode n. Integer-valued q rational surfaces are annotated. The reson `b^r = Φ^r/A^r` is the resonant flux normalized by the scalar surface area, in tesla [Pharr 2026]. Requires the perturbed equilibrium module to have been run and -`singular_coupling/resonant_area_weighted_field` to be present in the HDF5 file. +`SingularCoupling/resonant_area_weighted_field` to be present in the HDF5 file. ### Arguments @@ -40,7 +44,7 @@ Requires the perturbed equilibrium module to have been run and A `Plots.jl` plot object. """ function plot_resonant_area_weighted_field_amplitude(h5path; save_path=nothing) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "resonant_area_weighted_field") || return plot(; title="No resonant area-weighted field data — run with perturbed equilibrium enabled", legend=false) @@ -76,7 +80,7 @@ end Scatter plot of island half-width `w/2` per rational surface vs ψ_N. Integer-valued q rational surfaces are annotated. -Requires `singular_coupling/island_half_width` in the HDF5 file. +Requires `SingularCoupling/island_half_width` in the HDF5 file. ### Arguments @@ -91,7 +95,7 @@ Requires `singular_coupling/island_half_width` in the HDF5 file. A `Plots.jl` plot object. """ function plot_island_widths(h5path; save_path=nothing) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "island_half_width") || return plot(; title="No island width data — run with perturbed equilibrium enabled", legend=false) @@ -130,7 +134,7 @@ Scatter plot of the Chirikov overlap parameter per rational surface vs ψ_N, wit horizontal reference line at K = 1 (island overlap threshold). Points are colored red when K > 1. Integer-valued q rational surfaces are annotated. -Requires `singular_coupling/chirikov_parameter` in the HDF5 file. +Requires `SingularCoupling/chirikov_parameter` in the HDF5 file. ### Arguments @@ -145,7 +149,7 @@ Requires `singular_coupling/chirikov_parameter` in the HDF5 file. A `Plots.jl` plot object. """ function plot_chirikov_parameter(h5path; save_path=nothing) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "chirikov_parameter") || return plot(; title="No Chirikov data — run with perturbed equilibrium enabled", legend=false) @@ -184,15 +188,15 @@ end plot_driven_delta_prime(h5path; save_path=nothing) Scatter plot of `Re(Δ')` per rational surface vs ψ_N, computed by the perturbed -equilibrium module (from `perturbed_equilibrium/singular_coupling/delta_prime`). +equilibrium module (from `PerturbedEquilibrium/SingularCoupling/delta_prime`). One marker series per toroidal mode n. Integer-valued q rational surfaces are annotated. This is the forcing-driven Δ' (response to the applied perturbation amplitudes in `intr.forcing_modes`); for the equilibrium-intrinsic Δ' from the STRIDE BVP, -read `singular/delta_prime_matrix` from the HDF5 directly. +read `SingularSurfaces/delta_prime_matrix` from the HDF5 directly. -Requires `perturbed_equilibrium/singular_coupling/delta_prime` in the HDF5 file. +Requires `PerturbedEquilibrium/SingularCoupling/delta_prime` in the HDF5 file. ### Arguments @@ -207,7 +211,7 @@ Requires `perturbed_equilibrium/singular_coupling/delta_prime` in the HDF5 file. A `Plots.jl` plot object. """ function plot_driven_delta_prime(h5path; save_path=nothing) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "delta_prime") || return plot(; title="No PE Δ' data — run with perturbed equilibrium enabled", legend=false) @@ -293,7 +297,7 @@ end # Internal helper — resonant current scatter plot function _plot_resonant_current(h5path) - base = "perturbed_equilibrium/singular_coupling/" + base = PE_SINGULAR_COUPLING _has_pe_data(h5path, base * "resonant_current") || return plot(; title="No resonant current data", legend=false) @@ -360,7 +364,7 @@ function plot_mode_spectrogram(h5path; component=:xi_psi, save_path=nothing) haskey(comp_map, component) || error("component must be one of :xi_psi, :b_psi, :b_theta, :b_zeta") - base = "perturbed_equilibrium/response/" + base = PE_RESPONSE dataset_path = base * comp_map[component] _has_pe_data(h5path, dataset_path) || @@ -368,10 +372,10 @@ function plot_mode_spectrogram(h5path; component=:xi_psi, save_path=nothing) data, psi_response, mlow, mhigh, nhigh, q95, rational_psi = h5open(h5path, "r") do fid read(fid[dataset_path]), - read(fid["integration/psi"]), - read(fid["info/mlow"]), read(fid["info/mhigh"]), read(fid["info/nhigh"]), - read(fid["equil/q95"]), - read(fid["perturbed_equilibrium/singular_coupling/rational_psi"]) + read(fid["ForceFreeStates/Solutions/ForwardIntegration/psi"]), + read(fid["Info/mlow"]), read(fid["Info/mhigh"]), read(fid["Info/nhigh"]), + read(fid["Equilibrium/q95"]), + read(fid[PE_SINGULAR_COUPLING * "rational_psi"]) end mpert = mhigh - mlow + 1 @@ -452,13 +456,13 @@ end # Internal helper — |b_psi(m)| spectrum at the outermost psi surface function _plot_bpsi_edge_spectrum(h5path) - base = "perturbed_equilibrium/response/" + base = PE_RESPONSE _has_pe_data(h5path, base * "b_psi_area_weighted") || return plot(; title="No b_psi data — run with perturbed equilibrium enabled", legend=false) data, mlow, mhigh = h5open(h5path, "r") do fid read(fid[base * "b_psi_area_weighted"]), - read(fid["info/mlow"]), read(fid["info/mhigh"]) + read(fid["Info/mlow"]), read(fid["Info/mhigh"]) end mpert = mhigh - mlow + 1 diff --git a/src/Analysis/PerturbedEquilibriumModes.jl b/src/Analysis/PerturbedEquilibriumModes.jl index 198ad8693..b5e5a8e45 100644 --- a/src/Analysis/PerturbedEquilibriumModes.jl +++ b/src/Analysis/PerturbedEquilibriumModes.jl @@ -21,7 +21,7 @@ the HDF5 file. # Arguments - `h5_file::String`: Path to gpec.h5 output file -- `variable::String`: HDF5 dataset path, e.g. `"perturbed_equilibrium/response/xi_R"` +- `variable::String`: HDF5 dataset path, e.g. `"PerturbedEquilibrium/Response/xi_R"` # Keyword arguments - `mtheta::Int`: theta grid resolution (default: `max(2*(|mlow|+mpert), 512)`) @@ -45,10 +45,10 @@ function modes_to_theta(h5_file::String, variable::String; modes = read(f, variable) # (npsi, numpert_total) npsi, numpert_total = size(modes) - mlow = read(f, "info/mlow") - nlow = read(f, "info/nlow") - mpert = read(f, "info/mpert") - npert = read(f, "info/npert") + mlow = read(f, "Info/mlow") + nlow = read(f, "Info/nlow") + mpert = read(f, "Info/mpert") + npert = read(f, "Info/npert") @assert numpert_total == mpert * npert "Expected numpert_total=$(mpert*npert), got $numpert_total" n_vals = [nlow + k - 1 for k in 1:npert] @@ -66,19 +66,19 @@ function modes_to_theta(h5_file::String, variable::String; if !keep_sfl_phi # Reconstruct ν spline from stored grid + nodal values (FastInterpolations v0.4 API) - rzphi_xs = read(f, "splines/rzphi/xs") - rzphi_ys = read(f, "splines/rzphi/ys") - nu_vals = read(f, "splines/rzphi/nu") + rzphi_xs = read(f, "Equilibrium/Geometry/xs") + rzphi_ys = read(f, "Equilibrium/Geometry/ys") + nu_vals = read(f, "Equilibrium/Geometry/nu") nu_spline = cubic_interp( (rzphi_xs, rzphi_ys), nu_vals; bc=(CubicFit(), PeriodicBC()), extrap=(ExtendExtrap(), WrapExtrap()) ) - psi_grid = read(f, "integration/psi") + psi_grid = read(f, "ForceFreeStates/Solutions/ForwardIntegration/psi") - bt_sign = haskey(f, "equil/bt_sign") ? read(f, "equil/bt_sign") : 1 - crnt = haskey(f, "equil/crnt") ? read(f, "equil/crnt") : 1.0 + bt_sign = haskey(f, "Equilibrium/bt_sign") ? read(f, "Equilibrium/bt_sign") : 1 + crnt = haskey(f, "Equilibrium/crnt") ? read(f, "Equilibrium/crnt") : 1.0 helicity = bt_sign * Int(sign(crnt)) hint = (Ref(1), Ref(1)) diff --git a/src/Equilibrium/DirectEquilibrium.jl b/src/Equilibrium/DirectEquilibrium.jl index ed6582131..66976648b 100644 --- a/src/Equilibrium/DirectEquilibrium.jl +++ b/src/Equilibrium/DirectEquilibrium.jl @@ -434,7 +434,7 @@ function _build_psi_grid(equil_params, psilow, psihigh) N_core = round(Int, mpsi * log_core / log_total) N_mid = mpsi - N_edge - N_core make_optimal_psi_grid(psilow, psihigh, N_core, N_mid, N_edge) - elseif equil_params.grid_type == "ldp" + elseif equil_params.grid_type == "rational_packed" [psilow + (psihigh - psilow) * sin((ipsi / mpsi) * (π / 2))^2 for ipsi in 0:mpsi] elseif equil_params.grid_type == "pow1" # Fortran powspace(psilow, psihigh, 1, mpsi+1, "upper") — edge-packed grid (equil/grid.f90:92-195) @@ -532,7 +532,7 @@ robustness. rewind!(pool, Float64) end - # Temporary splines for q0 extrapolation and optional newq0 revision + # Temporary splines for q0 extrapolation and optional q0_override revision profiles = ProfileSplines( psi_nodes, sq_nodes[:, 1], # F * 2π @@ -543,17 +543,17 @@ robustness. # q(0) by linear extrapolation from innermost surface q0 = profiles.q_spline.y[1] - profiles.q_deriv(psi_nodes[1]; hint=Ref(1)) * psi_nodes[1] if q0 <= 0.0 - @warn "q0 extrapolation to axis gives q0 = $(@sprintf("%.3f", q0)) ≤ 0 — likely a spline artifact from psilow being too large; check psilow or use newq0 to override." + @warn "q0 extrapolation to axis gives q0 = $(@sprintf("%.3f", q0)) ≤ 0 — likely a spline artifact from psilow being too large; check psilow or use q0_override to override." end - if equil_params.newq0 == -1 - equil_params.newq0 = -q0 + if equil_params.q0_override == -1 + equil_params.q0_override = -q0 end - if equil_params.newq0 != 0.0 - @info "Revising q-profile for newq0 = $(@sprintf("%.3f", equil_params.newq0))" + if equil_params.q0_override != 0.0 + @info "Revising q-profile for q0_override = $(@sprintf("%.3f", equil_params.q0_override))" f0 = profiles.F_spline.y[1] - profiles.F_deriv(psi_nodes[1]; hint=Ref(1)) * psi_nodes[1] - f0fac = f0^2 * ((equil_params.newq0 / q0)^2 - 1.0) + f0fac = f0^2 * ((equil_params.q0_override / q0)^2 - 1.0) for i in 1:(mpsi+1) - ffac = sqrt(1.0 + f0fac / profiles.F_spline.y[i]^2) * sign(equil_params.newq0) + ffac = sqrt(1.0 + f0fac / profiles.F_spline.y[i]^2) * sign(equil_params.q0_override) sq_nodes[i, 1] *= ffac sq_nodes[i, 4] *= ffac rzphi_nodes[i, :, 3] .*= ffac diff --git a/src/Equilibrium/EquilibriumTypes.jl b/src/Equilibrium/EquilibriumTypes.jl index 4bb29f8f2..e46a30372 100644 --- a/src/Equilibrium/EquilibriumTypes.jl +++ b/src/Equilibrium/EquilibriumTypes.jl @@ -24,8 +24,8 @@ Bundles all necessary settings originally specified in the equil fortran namelis - `r0exp::Float64` - Major radius normalization for CHEASE/EQDSK [m] - `b0exp::Float64` - On-axis toroidal field normalization for CHEASE/EQDSK [T] - `grid_type::String` - Grid type for flux surface discretization ("auto" — two-pass measured-curvature - refinement when mpsi=0, three-region log layout when mpsi>0; "ldp", "pow1", "uniform"; - "log_asymptotic" is a legacy alias for "auto") + refinement when mpsi=0, three-region log layout when mpsi>0; "rational_packed", "pow1", "uniform"; + "log_asymptotic" is a legacy alias for "auto"; "ldp" is a deprecated alias for "rational_packed") - `psilow::Float64` - Lower limit of normalized flux coordinate - `psihigh::Float64` - Upper limit of normalized flux coordinate - `mpsi::Int` - Number of radial grid intervals; 0 with grid_type="auto" selects the @@ -35,10 +35,10 @@ Bundles all necessary settings originally specified in the equil fortran namelis - `psi_accuracy::Float64` - Target relative accuracy τ of splined profile derivatives for the two-pass auto grid (knot count scales as τ^(-1/3)) - `mtheta::Int` - Number of poloidal grid points - - `newq0::Int` - Override for on-axis safety factor (0 = use input value) + - `q0_override::Float64` - Override for the on-axis safety factor q0 (0 = use input value; -1 = flip the sign of the extrapolated q0) - `etol::Float64` - Error tolerance for equilibrium solver - `force_termination::Bool` - Terminate after equilibrium setup (skip stability calculations) - - `use_galgrid::Bool` - Use the same grid as galerkin method + - `use_galerkin_grid::Bool` - Use the same grid as galerkin method """ @kwdef mutable struct EquilibriumConfig eq_type::String = "efit" @@ -64,11 +64,11 @@ Bundles all necessary settings originally specified in the equil fortran namelis psi_accuracy::Float64 = 0.001 mtheta::Int = 512 - newq0::Int = 0 + q0_override::Float64 = 0.0 etol::Float64 = 1e-10 force_termination::Bool = false - use_galgrid::Bool = true + use_galerkin_grid::Bool = true # IMAS-specific: expected COCOS convention of the input dd.equilibrium (11=IMAS standard, 2=GPEC internal) imas_cocos::Int = 11 @@ -81,8 +81,8 @@ Bundles all necessary settings originally specified in the equil fortran namelis # so their incoming values are ignored (hence `_`). function EquilibriumConfig(eq_type, eq_filename, r0exp, b0exp, jac_type, _, _, _, _, jac_custom_power_bp, jac_custom_power_b, jac_custom_power_r, jac_custom_power_rc, - grid_type, psilow, psihigh, mpsi, psi_accuracy, mtheta, newq0, etol, - force_termination, use_galgrid, imas_cocos) + grid_type, psilow, psihigh, mpsi, psi_accuracy, mtheta, q0_override, etol, + force_termination, use_galerkin_grid, imas_cocos) if jac_type == "hamada" @info "Forcing hamada coordinate jacobian exponents: power_*" power_b = 0 @@ -147,8 +147,8 @@ Bundles all necessary settings originally specified in the equil fortran namelis psihigh = min(psihigh, 1.0) return new(eq_type, eq_filename, r0exp, b0exp, jac_type, power_bp, power_b, power_r, power_rc, jac_custom_power_bp, jac_custom_power_b, jac_custom_power_r, jac_custom_power_rc, - grid_type, psilow, psihigh, mpsi, psi_accuracy, mtheta, newq0, etol, - force_termination, use_galgrid, imas_cocos) + grid_type, psilow, psihigh, mpsi, psi_accuracy, mtheta, q0_override, etol, + force_termination, use_galerkin_grid, imas_cocos) end end @@ -176,6 +176,12 @@ function EquilibriumConfig(equil_dict::Dict{String,Any}, base_path::String="./") end end + # Deprecated grid_type value: initials "ldp" renamed to the descriptive spelling. + if get(config_data, "grid_type", "") == "ldp" + @warn "grid_type = \"ldp\" in [Equilibrium] is deprecated; use grid_type = \"rational_packed\". The old spelling will be removed after v2.0.0." + config_data["grid_type"] = "rational_packed" + end + # Construct validated struct config = EquilibriumConfig(; symbolize_keys(config_data)...) # Only resolve `eq_filename` against `base_path` if the user actually diff --git a/src/Equilibrium/InverseEquilibrium.jl b/src/Equilibrium/InverseEquilibrium.jl index 725391b15..a7d69c8d4 100644 --- a/src/Equilibrium/InverseEquilibrium.jl +++ b/src/Equilibrium/InverseEquilibrium.jl @@ -60,7 +60,7 @@ function equilibrium_solver(input::InverseRunInput; override_psi_nodes::Union{No mtheta = config.mtheta psilow = config.psilow psihigh = config.psihigh - newq0 = config.newq0 + q0_override = config.q0_override # c----------------------------------------------------------------------- # c allocate and define local arrays. @@ -155,7 +155,7 @@ function equilibrium_solver(input::InverseRunInput; override_psi_nodes::Union{No N_core = round(Int, mpsi * log_core / log_total) N_mid = mpsi - N_edge - N_core sq_xs = make_optimal_psi_grid(psilow, psihigh, N_core, N_mid, N_edge) - elseif grid_type == "ldp" + elseif grid_type == "rational_packed" if mpsi == 0 mpsi = 128 end @@ -295,16 +295,16 @@ function equilibrium_solver(input::InverseRunInput; override_psi_nodes::Union{No f1_sq_lo = sq_deriv(sq_xs[1]) f1_sq_hi = sq_deriv(sq_xs[end]) q0 = f_sq[1, 4] - f1_sq_lo[4] * sq_xs[1] - if newq0 == -1 - newq0 = -q0 + if q0_override == -1 + q0_override = -q0 end - if newq0 != 0 + if q0_override != 0 f0 = f_sq[1, 2] - f1_sq_lo[2] * sq_xs[1] - f0fac = f0^2 * ((newq0 / q0)^2 - 1) - q0 = newq0 + f0fac = f0^2 * ((q0_override / q0)^2 - 1) + q0 = q0_override for ipsi in 0:mpsi - ffac = sqrt(1 + f0fac / f_sq[ipsi+1, 1]^2) * sign(newq0) + ffac = sqrt(1 + f0fac / f_sq[ipsi+1, 1]^2) * sign(q0_override) sq_fs[ipsi+1, 1] *= ffac sq_fs[ipsi+1, 4] *= ffac rzphi_fs[ipsi+1, :, 3] *= ffac 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/EulerLagrange.jl b/src/ForceFreeStates/EulerLagrange.jl index f563dca45..42305b71a 100644 --- a/src/ForceFreeStates/EulerLagrange.jl +++ b/src/ForceFreeStates/EulerLagrange.jl @@ -84,10 +84,10 @@ function balance_integration_chunks(chunks::Vector{IntegrationChunk}, ctrl::Forc # assemble_fm_matrix(condition=true) can't keep accumulated products well-conditioned # because single long-span propagators may already have cond ~ 10²⁴. min_bvp_intervals = 8 * (intr.msing + 1) + intr.msing - # Use the effective parallel width (capped by ctrl.parallel_threads) rather than - # Threads.nthreads() — otherwise a user on `julia -t 16` who sets parallel_threads=2 + # Use the effective parallel width (capped by ctrl.integrator_threads) rather than + # Threads.nthreads() — otherwise a user on `julia -t 16` who sets integrator_threads=2 # for determinism still pays for 4× the requested sub-chunk count. - effective_threads = min(Threads.nthreads(), max(ctrl.parallel_threads, 1)) + effective_threads = min(Threads.nthreads(), max(ctrl.integrator_threads, 1)) target_n = max(min_chunks, 4 * effective_threads, min_bvp_intervals) result = collect(chunks) @@ -140,21 +140,21 @@ end eulerlagrange_integration(ctrl, equil, ffit, intr) -> (odet, propagators, chunks, S_left) Integrate the Euler-Lagrange equations from the axis to `intr.psilim`, crossing each singular -surface on the way (Fortran `ode_run`). Dispatches on `ctrl` to the parallel propagator BVP -(`use_parallel`), the dual Riccati formulation (`use_riccati`), or -[`serial_eulerlagrange_integration`](@ref). +surface on the way (Fortran `ode_run`). Dispatches on `ctrl.integrator`: the STRIDE propagator +BVP (`"stride"`), the dual Riccati formulation (`"riccati"`), or +[`serial_eulerlagrange_integration`](@ref) (`"serial"`). -Only the parallel branch populates `propagators` / `chunks` / `S_left`, which +Only the stride branch populates `propagators` / `chunks` / `S_left`, which `compute_delta_prime_matrix!` consumes for the Δ' BVP; the other two return `nothing` for all three. """ function eulerlagrange_integration(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) - # Dispatch to parallel or Riccati solver if requested. - # Parallel path returns (odet, propagators, chunks, S_at_surface_left) for deferred Δ' BVP. - if ctrl.use_parallel + # Dispatch on the integrator algorithm. + # Stride path returns (odet, propagators, chunks, S_at_surface_left) for deferred Δ' BVP. + if ctrl.integrator == "stride" return parallel_eulerlagrange_integration(ctrl, equil, ffit, intr) - elseif ctrl.use_riccati + elseif ctrl.integrator == "riccati" return (riccati_eulerlagrange_integration(ctrl, equil, ffit, intr), nothing, nothing, nothing) end return serial_eulerlagrange_integration(ctrl, equil, ffit, intr) @@ -166,7 +166,7 @@ end Serial shooting branch of [`eulerlagrange_integration`](@ref): integrates chunk by chunk, applying Gaussian reduction whenever a solution norm ratio exceeds `ctrl.ucrit` and undoing it via `transform_u!` at the end, so `odet.u_store` comes back dense in the axis basis. Call -directly to force this branch regardless of `ctrl.use_parallel` / `ctrl.use_riccati`; `verbose` +directly to force this branch regardless of `ctrl.integrator`; `verbose` overrides `ctrl.verbose` for progress logging. """ function serial_eulerlagrange_integration(ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal; @@ -214,7 +214,7 @@ function serial_eulerlagrange_integration(ctrl::ForceFreeStatesControl, equil::E odet.step -= 1 trim_storage!(odet) - # Edge-dW scan over [psiedge, psilim] — populates odet.edge_scan for HDF5 output. + # Edge-dW scan over [dW_edge_scan_start, psilim] — populates odet.edge_scan for HDF5 output. # The scan mutates odet.psifac and odet.u internally; save/restore them around the call. # findmax_dW_edge! also (re)allocates odet.edge_scan; that field is the diagnostic # product and is intentionally NOT restored. @@ -224,7 +224,7 @@ function serial_eulerlagrange_integration(ctrl::ForceFreeStatesControl, equil::E # location. Legacy path (true) reproduces the ode_record_edge heuristic from Fortran # STRIDE — psilim/qlim/u are pulled back to the dW peak. Preserved for experimental # work; see docstring in ForceFreeStatesStructs.jl for the reliability caveats. - if ctrl.psiedge < intr.psilim + if ctrl.dW_edge_scan_start < intr.psilim saved_psifac, saved_u = odet.psifac, copy(odet.u) peak_step = findmax_dW_edge!(odet, ctrl, equil, ffit, intr) if ctrl.truncate_at_dW_peak @@ -752,7 +752,7 @@ function integrate_el_region!( near_start = abs(odet.q - q_start) < near_q_frac * q_range || steps_in_segment[] == 1 near_end = abs(odet.q - q_end) < near_q_frac * q_range # Always save in the edge scan region so findmax_dW_edge! has dense q coverage. - in_edge_scan = ctrl.psiedge < intr.psilim && integrator.t >= ctrl.psiedge + in_edge_scan = ctrl.dW_edge_scan_start < intr.psilim && integrator.t >= ctrl.dW_edge_scan_start if near_start || near_end || (odet.total_steps % ctrl.save_interval == 0) || in_edge_scan # q at the accepted point, not the last internal Runge-Kutta stage @@ -881,7 +881,7 @@ end """ findmax_dW_edge!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) -Records the total dW in the integration region between `ctrl.psiedge` and +Records the total dW in the integration region between `ctrl.dW_edge_scan_start` and `ctrl.psilim`. This performs the same function as `ode_record_edge` in the Fortran, but everything is now done post-integration which cleans up the logic, i.e. no "_edge" arrays. @@ -897,8 +897,8 @@ for clarity. We create the wv matrix spline once prior to the loop. """ function findmax_dW_edge!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, ffit::FourFitVars, intr::ForceFreeStatesInternal) - # Find the first ODE step at or past psiedge; all subsequent steps are contiguous edge steps - edge_start = findfirst(i -> odet.psi_store[i] >= ctrl.psiedge, 1:odet.step) + # Find the first ODE step at or past dW_edge_scan_start; all subsequent steps are contiguous edge steps + edge_start = findfirst(i -> odet.psi_store[i] >= ctrl.dW_edge_scan_start, 1:odet.step) N_edge = odet.step - edge_start + 1 # Initialize EdgeScanState sized exactly to the number of edge steps @@ -908,7 +908,7 @@ function findmax_dW_edge!(odet::OdeState, ctrl::ForceFreeStatesControl, equil::E es.psi .= odet.psi_store[edge_start:odet.step] es.q .= odet.q_store[edge_start:odet.step] - # Create a rough spline for wv matrix between psiedge -> psilim so we can approximate dW + # Create a rough spline for wv matrix between dW_edge_scan_start -> psilim so we can approximate dW es.wvmat = free_compute_wv_spline(ctrl, equil, intr) # Loop with compact index j into EdgeScanState; ODE index is edge_start + j - 1. diff --git a/src/ForceFreeStates/ForceFreeStatesStructs.jl b/src/ForceFreeStates/ForceFreeStatesStructs.jl index d9c4770d2..93e79a56f 100644 --- a/src/ForceFreeStates/ForceFreeStatesStructs.jl +++ b/src/ForceFreeStates/ForceFreeStatesStructs.jl @@ -206,7 +206,7 @@ A mutable struct holding internal state variables for stability calculations. via `pest3_decompose(dp_raw)` — needed for the full det(D' − D(γ)) = 0 eigenvalue problem with Glasser stabilization. - Empty unless `ctrl.use_parallel` is true. No ½ prefactor is applied (matches + Empty unless `ctrl.integrator == "stride"`. No ½ prefactor is applied (matches Fortran rdcon; Pletzer–Dewar paper multiplies by ½). """ delta_prime_raw::Matrix{ComplexF64} = Matrix{ComplexF64}(undef, 0, 0) @@ -230,7 +230,6 @@ gpec.toml. - `nn_high::Int` - Upper bound for toroidal modes - `delta_mlow::Int` - Expands lower bound of Fourier harmonics by delta_mlow - `delta_mhigh::Int` - Expands upper bound of Fourier harmonics by delta_mhigh - - `nstep::Int` - Maximum number of integration steps (not yet implemented) - `ksing::Int` - Singular surface handling parameter - `eulerlagrange_tolerance::Float64` - Relative tolerance for ODE integration of Euler-Lagrange equations - `ucrit::Float64` - Critical value of unorm ratio to trigger solution normalization. In the standard path it triggers Gaussian reduction; in the Riccati path it triggers `renormalize_riccati_inplace!`. Default `1e4` empirically keeps max(|U₁|, |U₂|) in O(1)–O(10⁴) over the integration domain on DIII-D / Solovev sweeps; lower triggers excess renorms without accuracy gain, higher risks overflow before the next renorm. @@ -245,18 +244,16 @@ gpec.toml. - `kinetic_factor::Float64` - Dimensionless scaling factor for kinetic matrices. Zero (the default) disables the kinetic path; any positive value enables it and scales the kinetic matrices: when kinetic_source="fixed", scales X-shaped test matrices relative to ideal matrix norms; when kinetic_source="calculated", applied as uniform post-hoc multiplier to W and T components. - `qlow::Float64` - Integration terminated at q limit determined by minimum of qlow and q0 from equil - `reform_eq_with_psilim::Bool` - Reform equilibrium with computed psilim (not yet implemented) - - `psiedge::Float64` - If less than psilim, records a dW(ψ) diagnostic scan over [psiedge, psilim] on odet.edge_scan. The integration domain (psilim) is always controlled by qhigh / psihigh and is not modified by this scan (unless `truncate_at_dW_peak=true`, see caveats below). - - `truncate_at_dW_peak::Bool` - When `true` and `psiedge < psilim`, the edge-dW scan's peak location is adopted as the new physical plasma edge — `intr.psilim`/`intr.qlim`/`odet.u` are pulled back to the peak, AND the FM Δ' chunks/propagators are made self-consistent with the new boundary (the chunk that straddles the peak is rebuilt + re-integrated; any chunks past the peak are dropped). This reproduces the spirit of the original ode_record_edge heuristic from Fortran STRIDE while keeping Δ' and δW well-defined at the new boundary. The Δ' metric is still physically dependent on where the peak falls in the edge band, so use this flag deliberately when you mean to scan against the peak-defined edge (e.g. for studying edge-mode regimes); leave at `false` (default) for the full-domain Δ' at `qhigh` / `psihigh` / `dmlim`. + - `dW_edge_scan_start::Float64` - If less than psilim, records a dW(ψ) diagnostic scan over [dW_edge_scan_start, psilim] on odet.edge_scan. The integration domain (psilim) is always controlled by qhigh / psihigh and is not modified by this scan (unless `truncate_at_dW_peak=true`, see caveats below). + - `truncate_at_dW_peak::Bool` - When `true` and `dW_edge_scan_start < psilim`, the edge-dW scan's peak location is adopted as the new physical plasma edge — `intr.psilim`/`intr.qlim`/`odet.u` are pulled back to the peak, AND the FM Δ' chunks/propagators are made self-consistent with the new boundary (the chunk that straddles the peak is rebuilt + re-integrated; any chunks past the peak are dropped). This reproduces the spirit of the original ode_record_edge heuristic from Fortran STRIDE while keeping Δ' and δW well-defined at the new boundary. The Δ' metric is still physically dependent on where the peak falls in the edge band, so use this flag deliberately when you mean to scan against the peak-defined edge (e.g. for studying edge-mode regimes); leave at `false` (default) for the full-domain Δ' at `qhigh` / `psihigh` / `dmlim`. - `diagnose::Bool` - Enable diagnostic output (not yet implemented) - - `diagnose_ca::Bool` - Enable asymptotic coefficient diagnostics (not yet implemented) - `write_outputs_to_HDF5::Bool` - Write results to HDF5 format - `HDF5_filename::String` - Name of HDF5 output file - `save_interval::Int` - Save every Nth ODE step (1=all, 10=every 10th). Always saves near rational surfaces. (Same as `euler_step` in the Fortran) - `force_termination::Bool` - Terminate after force-free states (skip perturbed equilibrium calculations) - - `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). + - `integrator::String` - Which Euler-Lagrange integration algorithm to use. `"stride"` (default): fundamental-matrix (propagator) chunk integration following Fortran STRIDE, assembled via a BVP — the only path that produces the Δ' matrix (`SingularSurfaces/delta_prime_matrix`) consumed by SLAYER/GGJ downstream. Each chunk is integrated independently from identity IC using `Threads.@threads` (thread cap `integrator_threads`; `integrator_threads = 1` runs the chunks serially and bit-deterministically). Requires `singfac_min != 0`; crossings use the Riccati-style algorithm (no Gaussian reduction). `"riccati"`: 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. `"serial"`: the serial shooting method with Gaussian reduction (see `serial_eulerlagrange_integration`). + - `integrator_threads::Int` - Cap on the number of threads the stride-integrator 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 `integrator_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 stride path produces bit-identical Δ′ across thread counts; `integrator_threads = 2` is about 20% faster than serial and saturates the speedup. If a stride run diverges, drop to `integrator_threads = 1` rather than switching to `integrator = "serial"` — the latter is silently wrong. Capped at `Threads.nthreads()`. + - `populate_dense_xi::Bool` - When `integrator = "stride"`, 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 stride path stores only chunk-endpoint Riccati S matrices with diagnostic derivatives (see Riccati.jl docstring caveats), and the HDF5 `ForceFreeStates/Solutions/ForwardIntegration` `xi_psi`/`dxi_psi`/`xi_s` are unusable. Δ' (`SingularSurfaces/delta_prime_matrix`) is computed from the stride BVP and is bit-identical between `populate_dense_xi=true` and `false`. Energies (`ForceFreeStates/FreeBoundaryStability` `eigenmode_*_energies`) are computed by `free_run` from `odet`, so with `populate_dense_xi=true` they match what a pure serial run (`integrator = "serial"`) would produce; with `populate_dense_xi=false` they use the stride-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 `integrator = "stride"` (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 stride BVP wall-clock for typical N). - `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 @@ -270,13 +267,12 @@ gpec.toml. nn_high::Int = 0 delta_mlow::Int = 0 delta_mhigh::Int = 0 - nstep::Int = typemax(Int) ksing::Int = -1 eulerlagrange_tolerance::Float64 = 1e-8 ucrit::Float64 = 1e4 numsteps_init::Int = 4000 numunorms_init::Int = 100 - singfac_min::Float64 = 1e-4 # Matches Fortran STRIDE; required nonzero for use_parallel path. + singfac_min::Float64 = 1e-4 # Matches Fortran STRIDE; required nonzero for the stride integrator path. set_psilim_via_dmlim::Bool = true # Safe default for diverted equilibria (most production use); set false for limited/analytical (LAR, Solovev). Auto-skipped for multi-n. See docstring. dmlim::Float64 = 0.2 sing_order::Int = 6 @@ -285,18 +281,16 @@ gpec.toml. kinetic_factor::Float64 = 0.0 qlow::Float64 = 0.0 reform_eq_with_psilim::Bool = false - psiedge::Float64 = 0.99 + dW_edge_scan_start::Float64 = 0.99 truncate_at_dW_peak::Bool = false # Edge-dW peak becomes new physical edge; Δ' BVP made self-consistent. See docstring. - parallel_threads::Int = 2 + integrator_threads::Int = 2 diagnose::Bool = false - diagnose_ca::Bool = false write_outputs_to_HDF5::Bool = true HDF5_filename::String = "gpec.h5" 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. - 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). + integrator::String = "stride" # Default stride: unlocks SingularSurfaces/delta_prime_matrix (STRIDE BVP Δ' matrix) used by SLAYER/GGJ downstream. + populate_dense_xi::Bool = false # When integrator="stride", set to true ONLY if a PerturbedEquilibrium pipeline will consume dense ξ. Default false avoids the ~1× stride-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). # --- RDCON outer-region Galerkin Δ′ solver (gal_solve port) --- @@ -447,7 +441,7 @@ end """ EdgeScanState -Holds the state and results for the edge dW stability scan over ψ ∈ [psiedge, psilim]. +Holds the state and results for the edge dW stability scan over ψ ∈ [dW_edge_scan_start, psilim]. Initialized and populated by `findmax_dW_edge!`; results written to HDF5 under `EdgeScan/`. The energies are generalized (W, N) pencil values: power-normalized and invariant to the working (Jacobian) coordinate (see `power_norm_matrix!`). @@ -562,7 +556,7 @@ and a small set of temporary matrices and factors used to compute singular-layer - `index::Array{Int,2}` - Index matrix used for sorting solution norms with shape `(numpert_total, numunorms_init)`. - - `sing_flag::Vector{Bool}` - Boolean flags indicating which stored normalizations correspond to singular solutions # Edge dW scan state and results (disabled sentinel when psiedge >= psilim, i.e. no edge scan) + - `sing_flag::Vector{Bool}` - Boolean flags indicating which stored normalizations correspond to singular solutions # Edge dW scan state and results (disabled sentinel when dW_edge_scan_start >= psilim, i.e. no edge scan) (length `numunorms_init`). - `zeroed_idx::Vector{Vector{Int}}` - For each ideal rational surface jump, a vector of indices of solutions that were zeroed. # Data for integrator @@ -593,7 +587,7 @@ and a small set of temporary matrices and factors used to compute singular-layer ca_r::Array{ComplexF64,4} = Array{ComplexF64}(undef, numpert_total, numpert_total, 2, msing) ca_l::Array{ComplexF64,4} = Array{ComplexF64}(undef, numpert_total, numpert_total, 2, msing) - # Edge dW scan state and results (disabled sentinel when psiedge >= psilim, i.e. no edge scan) + # Edge dW scan state and results (disabled sentinel when dW_edge_scan_start >= psilim, i.e. no edge scan) edge_scan::EdgeScanState = EdgeScanState(numpert_total, 0) # Data for integrator diff --git a/src/ForceFreeStates/Free.jl b/src/ForceFreeStates/Free.jl index adf545b1c..e7ed75f68 100644 --- a/src/ForceFreeStates/Free.jl +++ b/src/ForceFreeStates/Free.jl @@ -170,7 +170,7 @@ q-window minimum. # Number of psi grid points for the spline: 4 per q-window minimum # TODO: 4 spline points is arbitrary - is there a better way? - qedge = profiles.q_spline(ctrl.psiedge) + qedge = profiles.q_spline(ctrl.dW_edge_scan_start) npsi = max(4, ceil(Int, (intr.qlim - qedge) * intr.nhigh * 4)) psi_array = zeros!(pool, Float64, npsi + 1) wv_array = zeros!(pool, ComplexF64, npsi + 1, intr.numpert_total, intr.numpert_total) @@ -179,7 +179,7 @@ q-window minimum. # Space points evenly in q over [qedge, qlim] (i=1 → qedge, i=npsi+1 → qlim) qi = qedge + (intr.qlim - qedge) * ((i - 1) / npsi) - psii = ctrl.psiedge + (intr.psilim - ctrl.psiedge) * ((i - 1) / npsi) + psii = ctrl.dW_edge_scan_start + (intr.psilim - ctrl.dW_edge_scan_start) * ((i - 1) / npsi) psi_array[i] = find_zero( (psi -> profiles.q_spline(psi) - qi, psi -> profiles.q_deriv(psi)), @@ -207,7 +207,7 @@ end Compute total complex energy eigenvalue (total1). This is a trimmed down version of `free_run` that only computes the total energy eigenvalue for the mode unstable mode, used in `findmax_dW_edge!` -which calls this function at each step in the psiedge -> psilim region of integration. This performs +which calls this function at each step in the dW_edge_scan_start -> psilim region of integration. This performs the same function as `free_test` in the Fortran code, except we have moved the creation of the wv matrix spline to `free_compute_wv_spline` and pass it in `odet.edge_scan.wvmat` (a complex-valued spline). """ diff --git a/src/ForceFreeStates/Galerkin/GalerkinSolve.jl b/src/ForceFreeStates/Galerkin/GalerkinSolve.jl index 8389ad9bc..8ef057197 100644 --- a/src/ForceFreeStates/Galerkin/GalerkinSolve.jl +++ b/src/ForceFreeStates/Galerkin/GalerkinSolve.jl @@ -229,57 +229,111 @@ 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 - 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 + gal = "ForceFreeStates/Solutions/GalerkinIntegration" + gdp = "SingularSurfaces/GalerkinDeltaPrime" + out_h5["$gal/msing"] = result.msing + if result.msing == 0 + annotate_galerkin!(out_h5) + return nothing + end + 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 + annotate_galerkin!(out_h5) + return nothing +end + +# Metadata tables for the Galerkin outputs (Match/** is debug-only and exempt from the +# metadata contract; see docs/development/hdf5-conventions.md). +const GALERKIN_H5_ANNOTATIONS = [ + "ForceFreeStates/Solutions/GalerkinIntegration/msing" => (; long_name="number of rational (singular) surfaces in the Galerkin solve"), + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/psi" => (; long_name="normalized poloidal flux ψ_N grid of the Galerkin solution"), + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/q" => (; long_name="safety factor on the Galerkin solution grid", dims=("psi",)), + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/issing" => (; long_name="flag: grid node lies on a rational surface", dims=("psi",)), + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi" => (; long_name="Galerkin solution functions ξ (arbitrary amplitude)", dims=("mode", "psi", "solution")), + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi_deriv" => + (; long_name="ψ_N derivative of the Galerkin solution functions (arbitrary amplitude)", dims=("mode", "psi", "solution")), + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/xi_cut" => + (; long_name="Galerkin solution functions with the leading-order resonant response excised", dims=("mode", "psi", "solution")), + "ForceFreeStates/Solutions/GalerkinIntegration/Solution/cut_range" => + (; long_name="ψ_N bounds of the excised resonant + extension cells per surface", dims=("surface", "bound")), + "SingularSurfaces/GalerkinDeltaPrime/delta" => + (; long_name="outer-region Δ' matrix (2msing×2msing, side-major [L_s1, R_s1, ...]; RDCON Galerkin)", dims=("surface_side", "surface_side")), + "SingularSurfaces/GalerkinDeltaPrime/pest3_A" => (; long_name="PEST-3 matching block A' (Galerkin outer region)", dims=("surface", "surface")), + "SingularSurfaces/GalerkinDeltaPrime/pest3_B" => (; long_name="PEST-3 matching block B' (Galerkin outer region)", dims=("surface", "surface")), + "SingularSurfaces/GalerkinDeltaPrime/pest3_Gamma" => (; long_name="PEST-3 matching block Γ' (Galerkin outer region)", dims=("surface", "surface")), + "SingularSurfaces/GalerkinDeltaPrime/pest3_Delta" => (; long_name="PEST-3 matching block Δ' (Galerkin outer region)", dims=("surface", "surface")), + "SingularSurfaces/GalerkinDeltaPrime/sing_psi" => (; long_name="normalized poloidal flux ψ_N of each rational surface"), + "SingularSurfaces/GalerkinDeltaPrime/sing_q" => (; long_name="safety factor q = m/n at each rational surface", dims=("surface",)), + "SingularSurfaces/GalerkinDeltaPrime/sing_m" => (; long_name="resonant poloidal mode number m at each rational surface", dims=("surface",)), + "SingularSurfaces/GalerkinDeltaPrime/sing_n" => (; long_name="resonant toroidal mode number n at each rational surface", dims=("surface",)), + "SingularSurfaces/GalerkinDeltaPrime/di" => (; long_name="Mercier D_I at each rational surface", dims=("surface",)), + "SingularSurfaces/GalerkinDeltaPrime/alpha" => (; long_name="Frobenius small-solution exponent α at each rational surface", dims=("surface",)), + "SingularSurfaces/GalerkinDeltaPrime/delta_coil" => (; long_name="edge coil-response matrix (edge mode × surface-side; RPEC columns)", dims=("mode", "surface_side")) +] + +# Attach long_name/units/dims + dimension scales to everything write_galerkin! wrote. +function annotate_galerkin!(out_h5) + ann = Utilities.HDF5Annotations + ann.annotate!(out_h5, GALERKIN_H5_ANNOTATIONS) + sol = "ForceFreeStates/Solutions/GalerkinIntegration/Solution" + gdp = "SingularSurfaces/GalerkinDeltaPrime" + ann.make_scale!(out_h5, "$sol/psi", "psi") + ann.attach_scale!(out_h5, "$sol/q", 1, "$sol/psi", "psi") + ann.attach_scale!(out_h5, "$sol/issing", 1, "$sol/psi", "psi") + ann.make_scale!(out_h5, "$gdp/sing_psi", "psi_rational") + for a in ("sing_q", "sing_m", "sing_n", "di", "alpha") + ann.attach_scale!(out_h5, "$gdp/$a", 1, "$gdp/sing_psi", "psi_rational") + end return nothing 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..68f658db1 100644 --- a/src/ForceFreeStates/Riccati.jl +++ b/src/ForceFreeStates/Riccati.jl @@ -1307,7 +1307,7 @@ otherwise mirrors, differing in three places: renormalizes to (S_new, I) in one step 3. `transform_u!` is skipped — S is already the true solution, so there is no reduction to undo -Enable via `use_riccati = true` in the `[ForceFreeStates]` section of gpec.toml. +Enable via `integrator = "riccati"` in the `[ForceFreeStates]` section of gpec.toml. """ function riccati_eulerlagrange_integration( ctrl::ForceFreeStatesControl, equil::Equilibrium.PlasmaEquilibrium, @@ -1353,13 +1353,13 @@ function riccati_eulerlagrange_integration( end end - # Edge-dW scan over [psiedge, psilim] — populates odet.edge_scan for HDF5 output. + # Edge-dW scan over [dW_edge_scan_start, psilim] — populates odet.edge_scan for HDF5 output. # See EulerLagrange.jl counterpart and ForceFreeStatesControl docstring for the # diagnostic vs legacy-truncation semantics and reliability caveats on # truncate_at_dW_peak=true. odet.step -= 1 trim_storage!(odet) - if ctrl.psiedge < intr.psilim + if ctrl.dW_edge_scan_start < intr.psilim saved_psifac, saved_u = odet.psifac, copy(odet.u) peak_step = findmax_dW_edge!(odet, ctrl, equil, ffit, intr) if ctrl.truncate_at_dW_peak @@ -1622,7 +1622,7 @@ concurrently using `Threads.@threads`, then re-integrates the outer plasma seria without renormalization); Riccati integration keeps matrices bounded and provides dense checkpoints for `findmax_dW_edge!`. -Enable via `use_parallel = true` in `[ForceFreeStates]` of gpec.toml. Requires `singfac_min != 0`. +Enable via `integrator = "stride"` in `[ForceFreeStates]` of gpec.toml. Requires `singfac_min != 0`. **Key differences from serial integration:** - No Gaussian reduction in the propagator BVP phase (crossings use the @@ -1650,7 +1650,7 @@ function parallel_eulerlagrange_integration( ) odet = _initialize_parallel_odet(ctrl, equil, ffit, intr) chunks, propagators, odet_proxies = _setup_parallel_chunks_and_proxies(odet, ctrl, intr) - bvp_threads = max(1, min(Threads.nthreads(), ctrl.parallel_threads)) + bvp_threads = max(1, min(Threads.nthreads(), ctrl.integrator_threads)) _log_parallel_start(ctrl, odet, equil, chunks, bvp_threads) _run_parallel_bvp_phase!(propagators, chunks, ctrl, equil, ffit, intr, odet_proxies, bvp_threads) @@ -1725,14 +1725,14 @@ function _log_parallel_start(ctrl::ForceFreeStatesControl, odet::OdeState, chunks::Vector{IntegrationChunk}, bvp_threads::Int) ctrl.verbose || return @info " ψ = $((@sprintf "%.3f" odet.psifac)), q = $((@sprintf "%.3f" equil.profiles.q_spline(odet.psifac)))" - @info " Parallel FM: $(length(chunks)) chunks, $bvp_threads BVP thread$(bvp_threads == 1 ? "" : "s") (julia_nthreads=$(Threads.nthreads()), ctrl.parallel_threads=$(ctrl.parallel_threads))" + @info " Parallel FM: $(length(chunks)) chunks, $bvp_threads BVP thread$(bvp_threads == 1 ? "" : "s") (julia_nthreads=$(Threads.nthreads()), ctrl.integrator_threads=$(ctrl.integrator_threads))" end # Integrate each chunk's FM propagator from identity IC. Serial when bvp_threads == 1 # (bit-deterministic; ~20% slower than 2-thread but immune to thread- # schedule sensitivity). Parallel uses :static scheduler so Threads.threadid() returns a # stable index into odet_proxies. If a parallel run ever diverges on a delicate equilibrium, -# drop to parallel_threads = 1 rather than use_parallel = false — the latter is silently wrong. +# drop to integrator_threads = 1 rather than integrator = "serial" — the latter is silently wrong. function _run_parallel_bvp_phase!(propagators::Vector{ChunkPropagator}, chunks::Vector{IntegrationChunk}, ctrl::ForceFreeStatesControl, @@ -1827,7 +1827,7 @@ function _reintegrate_outer_plasma!(odet::OdeState, last_crossing_step::Int, # Post: odet.u is in (S, I) form; odet.step points to next empty slot. end -# Edge-dW scan over [psiedge, psilim] — populates odet.edge_scan for HDF5. By default +# Edge-dW scan over [dW_edge_scan_start, psilim] — populates odet.edge_scan for HDF5. By default # (truncate_at_dW_peak=false) it's diagnostic-only: integration domain is unchanged. # When truncate_at_dW_peak=true, the dW peak becomes the new physical edge: intr.psilim, # odet, propagators, and chunks are made self-consistent (straddling chunk rebuilt with @@ -1843,7 +1843,7 @@ function _handle_edge_dW_scan!(odet::OdeState, chunks::Vector{IntegrationChunk}, N = intr.numpert_total odet.step -= 1 trim_storage!(odet) - ctrl.psiedge < intr.psilim || return chunks, propagators + ctrl.dW_edge_scan_start < intr.psilim || return chunks, propagators saved_psifac, saved_u = odet.psifac, copy(odet.u) peak_step = findmax_dW_edge!(odet, ctrl, equil, ffit, intr) @@ -1924,7 +1924,7 @@ which `compute_delta_prime_matrix!` uses). Called from `parallel_eulerlagrange_integration` when `ctrl.populate_dense_xi = true`. Approximate cost: one serial EL integration on top of the parallel BVP phase. Required to make -`use_parallel = true` produce DCON eigenfunctions usable by the +`integrator = "stride"` produce DCON eigenfunctions usable by the PerturbedEquilibrium downstream pipeline. """ function _populate_dense_xi_via_serial_el!( diff --git a/src/ForcingTerms/CoilFourier.jl b/src/ForcingTerms/CoilFourier.jl index 62996bf59..dbc1a160c 100644 --- a/src/ForcingTerms/CoilFourier.jl +++ b/src/ForcingTerms/CoilFourier.jl @@ -62,7 +62,7 @@ Defaults to the **outermost computed surface** `equil.rzphi_xs[end]` (i.e. `psih square root: `rfac = SQRT(crzphi_f(1))` (`coil/field.F:170`, where `crzphi_f(1)` is `rzphi_rsquared` and `crzphi_f(2)` is `rzphi_offset`); `coil/field.F:133` calls that mesh the "control surface mesh". `psilim = psihigh` (`dcon/sing.f:170`) and is only ever moved *inward* -by `sas_flag`/`qhigh`/`psiedge` truncation. Since `psihigh` is the last knot of the radial +by `sas_flag`/`qhigh`/`dW_edge_scan_start` truncation. Since `psihigh` is the last knot of the radial grid these splines are built on (`equil/inverse.f:142`), Fortran evaluates exactly ON the last knot and never extrapolates. The docs state it directly: the external field is specified "on the surface of the GPEC plasma boundary defined by the psihigh variable in equil.in" @@ -83,7 +83,7 @@ auto grid was refined. NOTE ON THE DEFAULT: the physically correct control surface is `psilim`, the *integration* limit, not `psihigh`, the *equilibrium spline* limit. They are equal unless -`dmlim`/`qhigh`/`psiedge` truncation fires, in which case `psilim < psihigh`. PPPL shipped a fix +`dmlim`/`qhigh`/`dW_edge_scan_start` truncation fires, in which case `psilim < psihigh`. PPPL shipped a fix for exactly this confusion (`docs/releases.rst:281`: "Fixes inappropriate uses of psihigh, which may not be the end of integration psilim if sas_flag, qhigh, or peak_flag are used"). diff --git a/src/ForcingTerms/CoilGeometry.jl b/src/ForcingTerms/CoilGeometry.jl index cd6bcb17d..cd5824069 100644 --- a/src/ForcingTerms/CoilGeometry.jl +++ b/src/ForcingTerms/CoilGeometry.jl @@ -69,7 +69,7 @@ Shifts/tilts apply to every source (the analytic geometry is built first, then t - `currents`: current [A] per conductor `[ncoil]`; shorter arrays pad with zeros - `shiftx`, `shifty`, `shiftz`: per-conductor translation [m] `[ncoil]` - `tiltx`, `tilty`, `tiltz`: per-conductor tilt in degrees (or meters if `tilt_in_meters`) - - `xnom`, `ynom`, `znom`: explicit rotation center [m]; defaults to arc-length-weighted center of mass + - `rotation_center_x`, `rotation_center_y`, `rotation_center_z`: explicit rotation center [m]; defaults to arc-length-weighted center of mass - `n_tilt`: toroidal mode number for tilt/shift modulation; -1 means inherit run's n - `tilt_in_meters`: interpret tilt as displacement [m] instead of angle [degrees] @@ -106,9 +106,9 @@ Base.@kwdef struct CoilSetConfig tiltx::Vector{Float64} = Float64[] tilty::Vector{Float64} = Float64[] tiltz::Vector{Float64} = Float64[] - xnom::Vector{Float64} = Float64[] - ynom::Vector{Float64} = Float64[] - znom::Vector{Float64} = Float64[] + rotation_center_x::Vector{Float64} = Float64[] + rotation_center_y::Vector{Float64} = Float64[] + rotation_center_z::Vector{Float64} = Float64[] n_tilt::Int = -1 tilt_in_meters::Bool = false @@ -180,6 +180,13 @@ function CoilConfig(ft_ctrl::ForcingTermsControl) end function _parse_coil_set_config(d::Dict{String,Any}) + # Deprecated key spellings, accepted with a warning until removal after v2.0.0. + for (old, new) in ("xnom" => "rotation_center_x", "ynom" => "rotation_center_y", "znom" => "rotation_center_z") + haskey(d, old) || continue + @warn "`$old` in [[ForcingTerms.coil_set]] was renamed to `$new`; the old key is deprecated and will be removed after v2.0.0." + haskey(d, new) || (d[new] = d[old]) + delete!(d, old) + end fvec(key) = Float64.(get(d, key, Float64[])) # rz_corners arrives as a Vector of [R, Z] pairs (TOML array of arrays) corners = [Float64.(c) for c in get(d, "rz_corners", Vector{Float64}[])] @@ -195,9 +202,9 @@ function _parse_coil_set_config(d::Dict{String,Any}) tiltx=fvec("tiltx"), tilty=fvec("tilty"), tiltz=fvec("tiltz"), - xnom=fvec("xnom"), - ynom=fvec("ynom"), - znom=fvec("znom"), + rotation_center_x=fvec("rotation_center_x"), + rotation_center_y=fvec("rotation_center_y"), + rotation_center_z=fvec("rotation_center_z"), n_tilt=get(d, "n_tilt", -1), tilt_in_meters=get(d, "tilt_in_meters", false), radius=Float64(get(d, "radius", 0.0)), @@ -608,7 +615,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) @@ -777,7 +784,7 @@ Apply per-conductor shifts and tilts to a coil set, returning a modified copy. Replicates the Fortran `coil_read` shift/tilt logic (coil.F lines 240–340): - - Tilts are rotations around the arc-length-weighted center of mass (unless `xnom/ynom/znom` specified) + - Tilts are rotations around the arc-length-weighted center of mass (unless `rotation_center_x/rotation_center_y/rotation_center_z` specified) - `n_tilt` controls the toroidal periodicity of tilt/shift modulation - n_tilt = 0: rigid shift only (no tilts applied) - n_tilt ≥ 1: n-fold modulated perturbations @@ -797,9 +804,9 @@ function apply_transforms(cs::CoilSet, cfg::CoilSetConfig; n_tilt::Int=1) tilty_cfg = _pad(cfg.tilty, ncoil) tiltz_cfg = _pad(cfg.tiltz, ncoil) - xnom_cfg = _pad(isempty(cfg.xnom) ? fill(_NOM_UNSET_SENTINEL, ncoil) : cfg.xnom, ncoil) - ynom_cfg = _pad(isempty(cfg.ynom) ? fill(_NOM_UNSET_SENTINEL, ncoil) : cfg.ynom, ncoil) - znom_cfg = _pad(isempty(cfg.znom) ? fill(_NOM_UNSET_SENTINEL, ncoil) : cfg.znom, ncoil) + rotation_center_x_cfg = _pad(isempty(cfg.rotation_center_x) ? fill(_NOM_UNSET_SENTINEL, ncoil) : cfg.rotation_center_x, ncoil) + rotation_center_y_cfg = _pad(isempty(cfg.rotation_center_y) ? fill(_NOM_UNSET_SENTINEL, ncoil) : cfg.rotation_center_y, ncoil) + rotation_center_z_cfg = _pad(isempty(cfg.rotation_center_z) ? fill(_NOM_UNSET_SENTINEL, ncoil) : cfg.rotation_center_z, ncoil) # Check if n_tilt = 0 suppresses tilts (Fortran: "no n=0 component") apply_tilt = n_tilt != 0 @@ -827,9 +834,9 @@ function apply_transforms(cs::CoilSet, cfg::CoilSetConfig; n_tilt::Int=1) view(cs.x, j, k, :), view(cs.y, j, k, :), view(cs.z, j, k, :) ) # Use user-specified center if provided (|nom| < _NOM_THRESHOLD, matching Fortran) - x0 = abs(xnom_cfg[j]) < _NOM_THRESHOLD ? xnom_cfg[j] : cx - y0 = abs(ynom_cfg[j]) < _NOM_THRESHOLD ? ynom_cfg[j] : cy - z0 = abs(znom_cfg[j]) < _NOM_THRESHOLD ? znom_cfg[j] : cz + x0 = abs(rotation_center_x_cfg[j]) < _NOM_THRESHOLD ? rotation_center_x_cfg[j] : cx + y0 = abs(rotation_center_y_cfg[j]) < _NOM_THRESHOLD ? rotation_center_y_cfg[j] : cy + z0 = abs(rotation_center_z_cfg[j]) < _NOM_THRESHOLD ? rotation_center_z_cfg[j] : cz (x0, y0, z0) end 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..18306b73c 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -59,6 +59,16 @@ include("Analysis/Analysis.jl") import .Analysis as Analysis export Analysis +# HDF5 paths read back by the file-based rerun (Rerun.jl); shared consts keep the +# writer and rerun reader from drifting apart. Schema conventions: +# docs/development/hdf5-conventions.md. +const H5_INPUT_TOML = "Input/gpec_toml_raw" +const H5_RAW_EQUILIBRIUM = "Input/RawInputs/Equilibrium" +const H5_RAW_FORCING = "Input/RawInputs/ForcingTerms" +const H5_RAW_COILS = "Input/RawInputs/Coils" +const H5_GIT_VERSION = "Info/git_version" + +include("HDF5Schema.jl") include("Rerun.jl") # Import ForceFreeStates types and functions needed for main @@ -70,9 +80,14 @@ using .ForceFreeStates: find_kinetic_singular_surfaces! using .ForceFreeStates: eulerlagrange_integration, free_run, normalize_eigenfunctions! using .ForceFreeStates: galerkin_solve, write_galerkin!, GalerkinResult, gal_matched_odestate -const _DEPRECATED_FFS_KEYS = ("mer_flag", "force_wv_symmetry", "ode_flag", "cyl_flag", "mat_flag") +const _DEPRECATED_FFS_KEYS = ("mer_flag", "force_wv_symmetry", "ode_flag", "cyl_flag", "mat_flag", "nstep", "diagnose_ca") const _DEPRECATED_EQUIL_KEYS = ("power_bp", "power_b", "power_r", "power_rc") +# Old→new TOML key spellings, accepted with a warning until removal after v2.0.0. +const _RENAMED_EQUIL_KEYS = ("newq0" => "q0_override", "use_galgrid" => "use_galerkin_grid") +const _RENAMED_FFS_KEYS = ("parallel_threads" => "integrator_threads", "psiedge" => "dW_edge_scan_start") +const _RENAMED_PE_KEYS = ("reg_spot" => "regularization_width",) + # Drop deprecated keys from a parsed gpec.toml section so legacy files keep parsing # instead of throwing an unknown-keyword error; warn so the removal is not silent. function _drop_deprecated_keys!(table, deprecated_keys, section::String) @@ -85,6 +100,44 @@ function _drop_deprecated_keys!(table, deprecated_keys, section::String) return table end +# Remap old→new key spellings in a parsed gpec.toml section so legacy decks keep working; +# warn on each hit, and let an explicitly set new key win over its old alias. +function _rename_keys!(table, renames, section::String) + for (old, new) in renames + haskey(table, old) || continue + @warn "`$old` in [$section] was renamed to `$new`; the old key is deprecated and will be removed after v2.0.0." + haskey(table, new) || (table[new] = table[old]) + delete!(table, old) + end + return table +end + +# Remap a deprecated value of an enum-like key (old → new spelling), warning on each hit. +function _rename_value!(table, key::String, old, new, section::String) + get(table, key, nothing) == old || return table + @warn "`$key = \"$old\"` in [$section] is deprecated; use `$key = \"$new\"`. The old value will be removed after v2.0.0." + table[key] = new + return table +end + +# The use_parallel/use_riccati boolean pair was replaced by the `integrator` enum; map the +# old flags onto the equivalent algorithm, mirroring the old dispatch order (parallel wins +# over riccati, both false means serial) including the old use_parallel=true default. +function _remap_integrator_keys!(table) + (haskey(table, "use_parallel") || haskey(table, "use_riccati")) || return table + implied = get(table, "use_parallel", true) ? "stride" : + (get(table, "use_riccati", false) ? "riccati" : "serial") + delete!(table, "use_parallel") + delete!(table, "use_riccati") + if haskey(table, "integrator") + @warn "`use_parallel`/`use_riccati` in [ForceFreeStates] are deprecated and ignored because `integrator` is also set." + else + @warn "`use_parallel`/`use_riccati` in [ForceFreeStates] were replaced by the `integrator` enum; assuming `integrator = \"$implied\"`. The old keys will be removed after v2.0.0." + table["integrator"] = implied + end + return table +end + function main(args::Vector{String}=String[]; dd::Union{IMASdd.dd,Nothing}=nothing) # Every input source builds a ready `(inputs, eq_config, additional_input)` and hands it to # `main_from_inputs`: a gpec.toml working directory, an IMAS `dd`, or a gpec.h5 snapshot. @@ -123,6 +176,7 @@ function build_inputs_from_toml(path::String; dd::Union{IMASdd.dd,Nothing}=nothi inputs = TOML.parsefile(joinpath(path, "gpec.toml")) haskey(inputs, "Equilibrium") || error("No [Equilibrium] section in gpec.toml") + _rename_keys!(inputs["Equilibrium"], _RENAMED_EQUIL_KEYS, "Equilibrium") _drop_deprecated_keys!(inputs["Equilibrium"], _DEPRECATED_EQUIL_KEYS, "Equilibrium") eq_config = Equilibrium.EquilibriumConfig(inputs["Equilibrium"], path) @@ -154,12 +208,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}, @@ -181,8 +235,16 @@ function main_from_inputs( # Build data structures from inputs intr = ForceFreeStatesInternal(; dir_path=path) ffs_table = inputs["ForceFreeStates"] + _rename_keys!(ffs_table, _RENAMED_FFS_KEYS, "ForceFreeStates") + _remap_integrator_keys!(ffs_table) _drop_deprecated_keys!(ffs_table, _DEPRECATED_FFS_KEYS, "ForceFreeStates") ctrl = ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in ffs_table)...) + ctrl.integrator in ("stride", "riccati", "serial") || + error("[ForceFreeStates] integrator = \"$(ctrl.integrator)\" is not one of \"stride\", \"riccati\", \"serial\"") + # SLAYER consumes the STRIDE Δ' matrix; fail at config time rather than after a long run. + if haskey(inputs, "SLAYER") && get(inputs["SLAYER"], "enabled", false) === true && ctrl.integrator != "stride" + error("[SLAYER] requires the STRIDE Δ' matrix: set integrator = \"stride\" in [ForceFreeStates] (got \"$(ctrl.integrator)\")") + end # Determine toroidal mode numbers (n >= 1 required; 0 means "not specified") intr.nlow, intr.nhigh = ctrl.nn_low, ctrl.nn_high @@ -215,6 +277,8 @@ function main_from_inputs( # does not need kinetic_profiles, but the post-PE block always does, so we load # whenever a [KineticForces] section is present or the stability path requests the # calculated source. psio is invariant across grid re-formation. + haskey(inputs, "KineticForces") && + _rename_value!(inputs["KineticForces"], "f0type", "jkp", "park", "KineticForces") kf_ctrl = haskey(inputs, "KineticForces") ? KineticForces.KineticForcesControl(; @@ -288,7 +352,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 +589,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) @@ -572,6 +636,7 @@ function main_from_inputs( ft_ctrl = ForcingTerms.ForcingTermsControl() # Use defaults end + _rename_keys!(inputs["PerturbedEquilibrium"], _RENAMED_PE_KEYS, "PerturbedEquilibrium") pe_ctrl = PerturbedEquilibrium.PerturbedEquilibriumControl(; (Symbol(k) => v for (k, v) in inputs["PerturbedEquilibrium"])... ) @@ -718,8 +783,11 @@ function write_outputs_to_HDF5( h5open(joinpath(intr.dir_path, ctrl.HDF5_filename), "w") do out_h5 + # File-level metadata contract (schema_version, Conventions, title, date). + Utilities.HDF5Annotations.write_root_attrs!(out_h5; title="GPEC output: $(basename(abspath(intr.dir_path)))") + # Store git version for reproducibility - out_h5["info/git_version"] = git_version + out_h5[H5_GIT_VERSION] = git_version # Outer-region Galerkin Δ′ matrix (RDCON), if computed if gal_data !== nothing @@ -730,111 +798,111 @@ function write_outputs_to_HDF5( # ForceFreeStates/Equilibrium/Wall/PE control struct), plus the equilibrium ingest # arrays so a file-based rerun never needs the original g-file / CHEASE / IMAS source. if inputs !== nothing - out_h5["input/gpec_toml_raw"] = sprint(TOML.print, inputs) + out_h5[H5_INPUT_TOML] = sprint(TOML.print, inputs) end if equil.ingest !== nothing # analytic equilibria are regenerated from their TOML section - eq_group = "input/raw_inputs/equilibrium" - out_h5["$eq_group/ingest_kind"] = equil.ingest isa Equilibrium.DirectIngest ? "direct" : "inverse" + out_h5["$H5_RAW_EQUILIBRIUM/ingest_kind"] = equil.ingest isa Equilibrium.DirectIngest ? "direct" : "inverse" for f in fieldnames(typeof(equil.ingest)) - out_h5["$eq_group/$f"] = getfield(equil.ingest, f) + out_h5["$H5_RAW_EQUILIBRIUM/$f"] = getfield(equil.ingest, f) end end if forcing_modes !== nothing - forcing_group = create_group(out_h5, "input/raw_inputs/forcing_terms") + forcing_group = create_group(out_h5, H5_RAW_FORCING) ForcingTerms.save_forcing_to_h5(forcing_modes, forcing_group) end # Write derived run parameters - out_h5["info/mpert"] = intr.mpert - out_h5["info/mlow"] = intr.mlow - out_h5["info/mhigh"] = intr.mhigh - out_h5["info/npert"] = intr.npert - out_h5["info/nlow"] = intr.nlow - out_h5["info/nhigh"] = intr.nhigh + out_h5["Info/mpert"] = intr.mpert + out_h5["Info/mlow"] = intr.mlow + out_h5["Info/mhigh"] = intr.mhigh + out_h5["Info/npert"] = intr.npert + out_h5["Info/nlow"] = intr.nlow + out_h5["Info/nhigh"] = intr.nhigh m = [(i - 1) % intr.mpert + intr.mlow for i in 1:(intr.numpert_total)] n = [(i - 1) ÷ intr.mpert + intr.nlow for i in 1:(intr.numpert_total)] - out_h5["info/mn_index"] = hcat(m, n) # (N, 2) matrix - out_h5["info/psilim"] = intr.psilim - out_h5["info/qlim"] = intr.qlim - out_h5["info/q1lim"] = intr.q1lim + out_h5["Info/mn_index"] = hcat(m, n) # (N, 2) matrix + out_h5["Info/psilim"] = intr.psilim + out_h5["Info/qlim"] = intr.qlim + out_h5["Info/q1lim"] = intr.q1lim # Write derived equilibrium parameters for (key, val) in zip(fieldnames(Equilibrium.EquilibriumParameters), getfield.(Ref(equil.params), fieldnames(Equilibrium.EquilibriumParameters))) if val !== nothing # TODO: looks like ro, zo, psio, and b_norm are not set, so skipping those for now but should fix eventually - out_h5["equil/$key"] = val + out_h5["Equilibrium/$key"] = val end end - out_h5["equil/psio"] = equil.psio - out_h5["equil/ro"] = equil.ro - out_h5["equil/zo"] = equil.zo + out_h5["Equilibrium/psio"] = equil.psio + out_h5["Equilibrium/ro"] = equil.ro + out_h5["Equilibrium/zo"] = equil.zo - # Write spline arrays (using profiles with named splines) + # Write equilibrium profile and geometry arrays (from the named splines) profiles = equil.profiles - out_h5["splines/profiles/xs"] = profiles.xs - out_h5["splines/profiles/2piF"] = profiles.F_spline.y - out_h5["splines/profiles/mu0p"] = profiles.P_spline.y - out_h5["splines/profiles/dVdpsi"] = profiles.dVdpsi_spline.y - out_h5["splines/profiles/q"] = profiles.q_spline.y - out_h5["splines/rzphi/xs"] = equil.rzphi_xs - out_h5["splines/rzphi/ys"] = equil.rzphi_ys + out_h5["Equilibrium/Profiles/xs"] = profiles.xs + out_h5["Equilibrium/Profiles/2piF"] = profiles.F_spline.y + out_h5["Equilibrium/Profiles/mu0p"] = profiles.P_spline.y + out_h5["Equilibrium/Profiles/dVdpsi"] = profiles.dVdpsi_spline.y + out_h5["Equilibrium/Profiles/q"] = profiles.q_spline.y + out_h5["Equilibrium/Geometry/xs"] = equil.rzphi_xs + out_h5["Equilibrium/Geometry/ys"] = equil.rzphi_ys # Extract grid point values from interpolants for HDF5 output - out_h5["splines/rzphi/rcoords"] = equil.rzphi_rsquared.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/offset"] = equil.rzphi_offset.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/nu"] = equil.rzphi_nu.nodal_derivs.partials[1, :, :] - out_h5["splines/rzphi/jac"] = equil.rzphi_jac.nodal_derivs.partials[1, :, :] + out_h5["Equilibrium/Geometry/rcoords"] = equil.rzphi_rsquared.nodal_derivs.partials[1, :, :] + out_h5["Equilibrium/Geometry/offset"] = equil.rzphi_offset.nodal_derivs.partials[1, :, :] + out_h5["Equilibrium/Geometry/nu"] = equil.rzphi_nu.nodal_derivs.partials[1, :, :] + out_h5["Equilibrium/Geometry/jac"] = equil.rzphi_jac.nodal_derivs.partials[1, :, :] # Write local stability data; always write all entries, using empty arrays when not computed. - # locstab/di = Mercier D_I (det(d0bar)); locstab/dr = resistive interchange D_R; - # locstab/ballooning_Delta_prime = high-n ballooning Δ' (distinct from the Riccati - # tearing Δ' under perturbed_equilibrium/singular_coupling/delta_prime). + # LocalStability/di = Mercier D_I (det(d0bar)); LocalStability/dr = resistive interchange D_R; + # LocalStability/ballooning_Delta_prime = high-n ballooning Δ' (distinct from the Riccati + # tearing Δ' under PerturbedEquilibrium/SingularCoupling/delta_prime). if 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 - - # Write edge stability scan data (only present when psiedge < psilim). + 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 dW_edge_scan_start < 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 +915,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 +925,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 +944,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 +962,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 +981,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 +1000,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,56 +1016,59 @@ 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 + + # Self-describing metadata pass (long_name/units/dims + dimension scales). + apply_main_h5_metadata!(out_h5, intr) end end """ _write_coil_snapshot!(h5_path::String, coil_sets::Vector{CoilSet}) -Append the coil geometry used by a run into `input/raw_inputs/coils/` of an existing +Append the coil geometry used by a run into `Input/RawInputs/Coils/` of an existing gpec.h5 file (opened in append mode), so the run can be replayed from the output alone. One subgroup per coil set; see `ForcingTerms.save_coils_to_h5`. """ function _write_coil_snapshot!(h5_path::String, coil_sets::Vector{ForcingTerms.CoilSet}) isfile(h5_path) || return nothing h5open(h5_path, "r+") do out_h5 - haskey(out_h5, "input/raw_inputs/coils") && return nothing - ForcingTerms.save_coils_to_h5(coil_sets, create_group(out_h5, "input/raw_inputs/coils")) + haskey(out_h5, H5_RAW_COILS) && return nothing + ForcingTerms.save_coils_to_h5(coil_sets, create_group(out_h5, H5_RAW_COILS)) end return nothing end diff --git a/src/HDF5Schema.jl b/src/HDF5Schema.jl new file mode 100644 index 000000000..ace08b783 --- /dev/null +++ b/src/HDF5Schema.jl @@ -0,0 +1,239 @@ +# Metadata tables for the datasets written by write_outputs_to_HDF5 (the main gpec.h5 +# writer), applied post-write by Utilities.HDF5Annotations.annotate!. Sub-writers +# (Galerkin, PerturbedEquilibrium, KineticForces, Tearing) keep their tables next to +# their own writers. Paths absent from a given run are skipped automatically. +# +# Conventions (docs/development/hdf5-conventions.md): units are SI strings, "1" for +# dimensionless; ψ always means the normalized poloidal flux ψ_N ∈ [0, 1]; stability +# energies are power-normalized (per unit surface-averaged |ξ|², not joules); `dims` +# lists axis names in Julia (column-major) order, axis 1 first. + +const MAIN_H5_ANNOTATIONS = [ + # --- Info/ --- + "Info/git_version" => (; long_name="GPEC git version that produced this file"), + "Info/mpert" => (; long_name="number of poloidal harmonics per toroidal mode"), + "Info/mlow" => (; long_name="lowest poloidal mode number m"), + "Info/mhigh" => (; long_name="highest poloidal mode number m"), + "Info/npert" => (; long_name="number of toroidal mode numbers"), + "Info/nlow" => (; long_name="lowest toroidal mode number n"), + "Info/nhigh" => (; long_name="highest toroidal mode number n"), + "Info/mn_index" => (; long_name="(m, n) mode numbers for each perturbation index", dims=("mode_index", "m_or_n")), + "Info/psilim" => (; long_name="normalized poloidal flux at the integration boundary"), + "Info/qlim" => (; long_name="safety factor q at the integration boundary"), + "Info/q1lim" => (; long_name="dq/dψ_N at the integration boundary"), + # --- Equilibrium/ scalars (written per-field when set; superset listed) --- + "Equilibrium/ro" => (; long_name="R-coordinate of the magnetic axis", units="m"), + "Equilibrium/zo" => (; long_name="Z-coordinate of the magnetic axis", units="m"), + "Equilibrium/psio" => (; long_name="total poloidal flux difference |ψ_axis - ψ_boundary|", units="Wb/rad"), + "Equilibrium/rsep" => (; long_name="R-coordinates of the plasma boundary", units="m"), + "Equilibrium/zsep" => (; long_name="Z-coordinates of the plasma boundary", units="m"), + "Equilibrium/rext" => (; long_name="R-coordinates of the plasma edge", units="m"), + "Equilibrium/zext" => (; long_name="Z-coordinates of the plasma edge", units="m"), + "Equilibrium/psi0" => (; long_name="normalized poloidal flux at reference location"), + "Equilibrium/b0" => (; long_name="total magnetic field strength at the axis", units="T"), + "Equilibrium/q0" => (; long_name="safety factor at the magnetic axis"), + "Equilibrium/qmin" => (; long_name="minimum safety factor in the plasma"), + "Equilibrium/qmax" => (; long_name="maximum safety factor in the plasma"), + "Equilibrium/qa" => (; long_name="safety factor at the plasma edge"), + "Equilibrium/q95" => (; long_name="safety factor at the 95% flux surface"), + "Equilibrium/qextrema_psi" => (; long_name="normalized poloidal flux at q-profile extrema"), + "Equilibrium/qextrema_q" => (; long_name="safety factor at q-profile extrema"), + "Equilibrium/mextrema" => (; long_name="number of extrema in the q-profile"), + "Equilibrium/rmean" => (; long_name="mean major radius of the plasma", units="m"), + "Equilibrium/amean" => (; long_name="mean minor radius of the plasma", units="m"), + "Equilibrium/aratio" => (; long_name="aspect ratio R0/a"), + "Equilibrium/kappa" => (; long_name="plasma elongation"), + "Equilibrium/delta1" => (; long_name="upper triangularity"), + "Equilibrium/delta2" => (; long_name="lower triangularity"), + "Equilibrium/bt0" => (; long_name="toroidal field at the axis", units="T"), + "Equilibrium/crnt" => (; long_name="plasma current", units="A"), + "Equilibrium/bwall" => (; long_name="toroidal field at the wall", units="T"), + "Equilibrium/betat" => (; long_name="toroidal beta"), + "Equilibrium/betan" => (; long_name="normalized beta β_N"), + "Equilibrium/betap1" => (; long_name="poloidal beta (definition 1)"), + "Equilibrium/betap2" => (; long_name="poloidal beta (definition 2)"), + "Equilibrium/betap3" => (; long_name="poloidal beta (definition 3)"), + "Equilibrium/betaj" => (; long_name="current-weighted beta"), + "Equilibrium/li1" => (; long_name="internal inductance (definition 1)"), + "Equilibrium/li2" => (; long_name="internal inductance (definition 2)"), + "Equilibrium/li3" => (; long_name="internal inductance (definition 3)"), + "Equilibrium/volume" => (; long_name="plasma volume", units="m^3"), + "Equilibrium/bt_sign" => (; long_name="sign of the toroidal field"), + "Equilibrium/psi_norm" => (; long_name="normalized poloidal flux at the axis"), + "Equilibrium/b_norm" => (; long_name="normalized total field strength at the axis"), + "Equilibrium/psi_axis" => (; long_name="poloidal flux at the magnetic axis", units="Wb/rad"), + "Equilibrium/psi_boundary" => (; long_name="poloidal flux at the plasma boundary", units="Wb/rad"), + "Equilibrium/psi_axis_norm" => (; long_name="normalized poloidal flux at the axis"), + "Equilibrium/psi_boundary_norm" => (; long_name="normalized poloidal flux at the boundary"), + "Equilibrium/psi_axis_offset" => (; long_name="offset applied to the axis poloidal flux", units="Wb/rad"), + "Equilibrium/psi_boundary_offset" => (; long_name="offset applied to the boundary poloidal flux", units="Wb/rad"), + "Equilibrium/psi_axis_sign" => (; long_name="sign of the axis poloidal flux"), + "Equilibrium/psi_boundary_sign" => (; long_name="sign of the boundary poloidal flux"), + "Equilibrium/psi_boundary_zero" => (; long_name="flag: boundary poloidal flux is zero"), + "Equilibrium/verbose" => (; long_name="flag: equilibrium setup ran with verbose output (diagnostic echo)"), + "Equilibrium/diagnose_src" => (; long_name="flag: equilibrium source-data diagnostics were enabled (diagnostic echo)"), + "Equilibrium/diagnose_maxima" => (; long_name="flag: equilibrium extrema diagnostics were enabled (diagnostic echo)"), + # --- Equilibrium/Profiles/ (1-D profiles on the ψ_N grid xs) --- + "Equilibrium/Profiles/xs" => (; long_name="normalized poloidal flux ψ_N profile grid"), + "Equilibrium/Profiles/2piF" => (; long_name="2π F with F = R B_φ the toroidal field function", units="T*m", dims=("psi",)), + "Equilibrium/Profiles/mu0p" => (; long_name="μ0 × plasma pressure", units="T^2", dims=("psi",)), + "Equilibrium/Profiles/dVdpsi" => (; long_name="flux-surface volume derivative dV/dψ_N", units="m^3", dims=("psi",)), + "Equilibrium/Profiles/q" => (; long_name="safety factor profile", dims=("psi",)), + # --- Equilibrium/Geometry/ (2-D flux-coordinate maps on (xs, ys) = (ψ_N, θ/2π)) --- + "Equilibrium/Geometry/xs" => (; long_name="normalized poloidal flux ψ_N geometry grid"), + "Equilibrium/Geometry/ys" => (; long_name="normalized poloidal angle θ/2π geometry grid"), + "Equilibrium/Geometry/rcoords" => (; long_name="squared minor-radius coordinate r² of the working coordinate map", units="m^2", dims=("psi", "theta")), + "Equilibrium/Geometry/offset" => (; long_name="poloidal-angle offset of the working coordinate map (fraction of 2π)", dims=("psi", "theta")), + "Equilibrium/Geometry/nu" => (; long_name="toroidal-angle offset ν = φ − 2πζ of the working coordinate map", units="rad", dims=("psi", "theta")), + "Equilibrium/Geometry/jac" => (; long_name="Jacobian of the (ψ_N, θ, ζ) working coordinates", units="m^3", dims=("psi", "theta")), + # --- LocalStability/ --- + "LocalStability/di" => (; long_name="Mercier ideal interchange criterion D_I", dims=("psi",)), + "LocalStability/dr" => (; long_name="Glasser-Greene-Johnson resistive interchange criterion D_R", dims=("psi",)), + "LocalStability/ballooning_Delta_prime" => (; long_name="high-n ballooning Δ' (distinct from the tearing Δ')", dims=("psi",)), + "LocalStability/psi" => (; long_name="normalized poloidal flux ψ_N of the ballooning α boundary scan"), + "LocalStability/alpha" => (; long_name="experimental normalized pressure gradient α", dims=("psi",)), + "LocalStability/alpha_critical" => (; long_name="critical normalized pressure gradient α for first ballooning stability", dims=("psi",)), + # --- ForceFreeStates/Solutions/ForwardIntegration/ --- + "ForceFreeStates/Solutions/ForwardIntegration/nstep" => (; long_name="number of saved solution snapshots"), + "ForceFreeStates/Solutions/ForwardIntegration/nstep_total" => (; long_name="total ODE solver steps taken"), + "ForceFreeStates/Solutions/ForwardIntegration/psi" => (; long_name="normalized poloidal flux ψ_N at saved solution snapshots"), + "ForceFreeStates/Solutions/ForwardIntegration/q" => (; long_name="safety factor at saved solution snapshots", dims=("psi",)), + "ForceFreeStates/Solutions/ForwardIntegration/xi_psi" => (; long_name="fundamental-matrix solutions ξ^ψ (arbitrary amplitude)", dims=("mode", "solution", "psi")), + "ForceFreeStates/Solutions/ForwardIntegration/u2" => + (; long_name="conjugate momenta of the fundamental-matrix solutions (arbitrary amplitude)", dims=("mode", "solution", "psi")), + "ForceFreeStates/Solutions/ForwardIntegration/dxi_psi" => + (; long_name="ψ_N derivative of the fundamental-matrix solutions ξ^ψ (arbitrary amplitude)", dims=("mode", "solution", "psi")), + "ForceFreeStates/Solutions/ForwardIntegration/xi_s" => (; long_name="Clebsch surface-displacement solutions Ξ_s (arbitrary amplitude)", dims=("mode", "solution", "psi")), + "ForceFreeStates/Solutions/ForwardIntegration/crit" => (; long_name="DCON zero-crossing criterion at saved snapshots", dims=("psi",)), + # --- ForceFreeStates/EdgeScan/ (power-normalized (W, N) pencil energies) --- + "ForceFreeStates/EdgeScan/psi" => (; long_name="normalized poloidal flux ψ_N of the edge truncation scan"), + "ForceFreeStates/EdgeScan/q" => (; long_name="safety factor at scan points", dims=("psi",)), + "ForceFreeStates/EdgeScan/total_energy" => (; long_name="power-normalized total energy of the least-stable free-boundary mode (per unit ⟨|ξ|²⟩)", dims=("psi",)), + "ForceFreeStates/EdgeScan/plasma_energy" => (; long_name="power-normalized plasma energy of the least-stable mode (per unit ⟨|ξ|²⟩)", dims=("psi",)), + "ForceFreeStates/EdgeScan/vacuum_energy" => (; long_name="power-normalized vacuum energy of the least-stable mode (per unit ⟨|ξ|²⟩)", dims=("psi",)), + "ForceFreeStates/EdgeScan/vacuum_eigenvalue" => (; long_name="least vacuum eigenvalue of the (W, N) pencil at scan points", dims=("psi",)), + # --- SingularSurfaces/ --- + "SingularSurfaces/msing" => (; long_name="number of rational (singular) surfaces in the domain"), + "SingularSurfaces/psi" => (; long_name="normalized poloidal flux ψ_N of each rational surface"), + "SingularSurfaces/q" => (; long_name="safety factor q = m/n at each rational surface", dims=("surface",)), + "SingularSurfaces/q1" => (; long_name="dq/dψ_N at each rational surface", dims=("surface",)), + "SingularSurfaces/m" => (; long_name="resonant poloidal mode numbers per surface (0-padded)", dims=("surface", "mode")), + "SingularSurfaces/n" => (; long_name="resonant toroidal mode numbers per surface (0-padded)", dims=("surface", "mode")), + "SingularSurfaces/di0" => (; long_name="Mercier D_I evaluated at each rational surface", dims=("surface",)), + "SingularSurfaces/ca_left" => + (; long_name="asymptotic large/small-solution coefficient matrices just left of each surface", dims=("mode", "solution", "large_small", "surface")), + "SingularSurfaces/ca_right" => + (; long_name="asymptotic large/small-solution coefficient matrices just right of each surface", dims=("mode", "solution", "large_small", "surface")), + "SingularSurfaces/E" => (; long_name="Glasser-Greene-Johnson coefficient E per surface", dims=("surface",)), + "SingularSurfaces/F" => (; long_name="Glasser-Greene-Johnson coefficient F per surface", dims=("surface",)), + "SingularSurfaces/G" => (; long_name="Glasser-Greene-Johnson coefficient G per surface", dims=("surface",)), + "SingularSurfaces/H" => (; long_name="Glasser-Greene-Johnson coefficient H per surface", dims=("surface",)), + "SingularSurfaces/K" => (; long_name="Glasser-Greene-Johnson coefficient K per surface", dims=("surface",)), + "SingularSurfaces/M" => (; long_name="Glasser-Greene-Johnson coefficient M per surface", dims=("surface",)), + "SingularSurfaces/avg_bsq_over_dpsisq" => (; long_name="flux-surface average ⟨B²/|∇ψ_N|²⟩ per surface", units="T^2*m^2", dims=("surface",)), + "SingularSurfaces/avg_bsq" => (; long_name="flux-surface average ⟨B²⟩ per surface", units="T^2", dims=("surface",)), + "SingularSurfaces/p_local" => (; long_name="μ0 × local pressure at each surface", units="T^2", dims=("surface",)), + "SingularSurfaces/p1_local" => (; long_name="μ0 × dp/dψ_N at each surface", units="T^2", dims=("surface",)), + "SingularSurfaces/v1_local" => (; long_name="dV/dψ_N at each surface", units="m^3", dims=("surface",)), + "SingularSurfaces/delta_prime_matrix" => (; long_name="inter-surface Δ' matrix (PEST3 convention, STRIDE BVP with vacuum coupling)", dims=("surface", "surface")), + "SingularSurfaces/delta_prime_raw" => (; long_name="raw 2msing×2msing outer-region D' matrix, side-major ordering [L_s1, R_s1, ...]", dims=("surface_side", "surface_side")), + "SingularSurfaces/delta_coil" => (; long_name="edge coil-response matrix (edge mode × surface-side)", dims=("mode", "surface_side")), + # --- SingularSurfaces/Kinetic/ --- + "SingularSurfaces/Kinetic/kmsing" => (; long_name="number of kinetic singular surfaces (det(F̄) near-zeros)"), + "SingularSurfaces/Kinetic/psi" => (; long_name="normalized poloidal flux ψ_N of kinetic singular surfaces"), + "SingularSurfaces/Kinetic/q" => (; long_name="safety factor at kinetic singular surfaces"), + "SingularSurfaces/Kinetic/q1" => (; long_name="dq/dψ_N at kinetic singular surfaces"), + "SingularSurfaces/Kinetic/scan_psi" => (; long_name="ψ_N grid of the cond(F̄) scan"), + "SingularSurfaces/Kinetic/scan_cond" => (; long_name="condition number of F̄ along the scan"), + "SingularSurfaces/Kinetic/scan_threshold" => (; long_name="cond(F̄) threshold used to flag kinetic singular surfaces"), + # --- ForceFreeStates/FreeBoundaryStability/ (power-normalized (W, N) pencil) --- + "ForceFreeStates/FreeBoundaryStability/W_freeboundary" => (; long_name="power-normalized free-boundary energy matrix W (per unit ⟨|ξ|²⟩)", dims=("mode", "mode")), + "ForceFreeStates/FreeBoundaryStability/W_plasma" => (; long_name="power-normalized plasma energy matrix (per unit ⟨|ξ|²⟩)", dims=("mode", "mode")), + "ForceFreeStates/FreeBoundaryStability/W_vacuum" => (; long_name="power-normalized vacuum energy matrix (per unit ⟨|ξ|²⟩)", dims=("mode", "mode")), + "ForceFreeStates/FreeBoundaryStability/W_freeboundary_eigenmodes" => + (; long_name="generalized eigenvectors of the (W, N) pencil, columns sorted most-unstable first, unit power norm", dims=("mode", "eigenmode")), + "ForceFreeStates/FreeBoundaryStability/eigenmode_energies" => + (; long_name="generalized eigenvalues of the (W, N) pencil: total energy per unit ⟨|ξ|²⟩, coordinate-invariant", dims=("eigenmode",)), + "ForceFreeStates/FreeBoundaryStability/eigenmode_plasma_energies" => (; long_name="plasma contribution to the power-normalized eigenmode energies", dims=("eigenmode",)), + "ForceFreeStates/FreeBoundaryStability/eigenmode_vacuum_energies" => (; long_name="vacuum contribution to the power-normalized eigenmode energies", dims=("eigenmode",)), + "ForceFreeStates/FreeBoundaryStability/vacuum_eigenvalue" => (; long_name="least eigenvalue of the vacuum energy matrix"), + # --- SurfaceGeometries/ --- + "SurfaceGeometries/Plasma/x" => (; long_name="Cartesian x of plasma-surface point cloud", units="m"), + "SurfaceGeometries/Plasma/y" => (; long_name="Cartesian y of plasma-surface point cloud", units="m"), + "SurfaceGeometries/Plasma/z" => (; long_name="Cartesian z of plasma-surface point cloud", units="m"), + "SurfaceGeometries/Wall/x" => (; long_name="Cartesian x of wall point cloud", units="m"), + "SurfaceGeometries/Wall/y" => (; long_name="Cartesian y of wall point cloud", units="m"), + "SurfaceGeometries/Wall/z" => (; long_name="Cartesian z of wall point cloud", units="m"), +] + +# Euler-Lagrange operator matrices: same wording per letter, Ideal/ and Kinetic/ variants. +const _ELM_IDEAL_LETTERS = [ + ("A", "Euler-Lagrange primitive coefficient matrix A"), + ("B", "Euler-Lagrange primitive coefficient matrix B"), + ("C", "Euler-Lagrange primitive coefficient matrix C"), + ("D", "Euler-Lagrange primitive coefficient matrix D"), + ("E", "Euler-Lagrange primitive coefficient matrix E"), + ("H", "Euler-Lagrange primitive coefficient matrix H"), + ("F", "Euler-Lagrange derived coefficient matrix F"), + ("K", "Euler-Lagrange derived coefficient matrix K"), + ("G", "Euler-Lagrange derived coefficient matrix G"), +] +const _ELM_KINETIC_LETTERS = vcat(_ELM_IDEAL_LETTERS, [("f0", "raw kinetic component matrix f0")]) +const ELM_H5_ANNOTATIONS = vcat( + ["ForceFreeStates/EulerLagrangeMatrices/psi" => (; long_name="normalized poloidal flux ψ_N grid of the operator matrices")], + ["ForceFreeStates/EulerLagrangeMatrices/Ideal/$l" => (; long_name="ideal " * d, dims=("psi", "mode", "mode")) for (l, d) in _ELM_IDEAL_LETTERS], + ["ForceFreeStates/EulerLagrangeMatrices/Kinetic/$l" => (; long_name="kinetic-modified " * d, dims=("psi", "mode", "mode")) for (l, d) in _ELM_KINETIC_LETTERS] +) + +""" + apply_main_h5_metadata!(out_h5, intr) + +Apply the self-describing metadata contract to the datasets written by +`write_outputs_to_HDF5`: `long_name`/`units`/`dims` attributes plus HDF5 Dimension +Scales for the shared coordinate datasets (ψ_N grids, rational-surface ψ). +""" +function apply_main_h5_metadata!(out_h5, intr) + ann = Utilities.HDF5Annotations + ann.annotate!(out_h5, MAIN_H5_ANNOTATIONS) + ann.annotate!(out_h5, ELM_H5_ANNOTATIONS) + + # Coordinate datasets → dimension scales, attached to the profiles sharing the axis. + fwd = "ForceFreeStates/Solutions/ForwardIntegration" + ann.make_scale!(out_h5, "$fwd/psi", "psi") + ann.attach_scale!(out_h5, "$fwd/q", 1, "$fwd/psi", "psi") + ann.attach_scale!(out_h5, "$fwd/crit", 1, "$fwd/psi", "psi") + for a in ("xi_psi", "u2", "dxi_psi") + ann.attach_scale!(out_h5, "$fwd/$a", 3, "$fwd/psi", "psi") + end + + ann.make_scale!(out_h5, "Equilibrium/Profiles/xs", "psi") + for a in ("2piF", "mu0p", "dVdpsi", "q") + ann.attach_scale!(out_h5, "Equilibrium/Profiles/$a", 1, "Equilibrium/Profiles/xs", "psi") + end + + ann.make_scale!(out_h5, "Equilibrium/Geometry/xs", "psi") + ann.make_scale!(out_h5, "Equilibrium/Geometry/ys", "theta") + for a in ("rcoords", "offset", "nu", "jac") + ann.attach_scale!(out_h5, "Equilibrium/Geometry/$a", 1, "Equilibrium/Geometry/xs", "psi") + ann.attach_scale!(out_h5, "Equilibrium/Geometry/$a", 2, "Equilibrium/Geometry/ys", "theta") + end + + ann.make_scale!(out_h5, "SingularSurfaces/psi", "psi_rational") + for a in ("q", "q1", "di0", "E", "F", "G", "H", "K", "M", + "avg_bsq_over_dpsisq", "avg_bsq", "p_local", "p1_local", "v1_local") + ann.attach_scale!(out_h5, "SingularSurfaces/$a", 1, "SingularSurfaces/psi", "psi_rational") + end + + ann.make_scale!(out_h5, "ForceFreeStates/EdgeScan/psi", "psi") + for a in ("q", "total_energy", "plasma_energy", "vacuum_energy", "vacuum_eigenvalue") + ann.attach_scale!(out_h5, "ForceFreeStates/EdgeScan/$a", 1, "ForceFreeStates/EdgeScan/psi", "psi") + end + + elm = "ForceFreeStates/EulerLagrangeMatrices" + ann.make_scale!(out_h5, "$elm/psi", "psi") + for grp in ("Ideal", "Kinetic"), (l, _) in _ELM_KINETIC_LETTERS + ann.attach_scale!(out_h5, "$elm/$grp/$l", 1, "$elm/psi", "psi") + end + + return out_h5 +end diff --git a/src/InnerLayer/SLAYER/LayerInputs.jl b/src/InnerLayer/SLAYER/LayerInputs.jl index 96177903d..ee988238f 100644 --- a/src/InnerLayer/SLAYER/LayerInputs.jl +++ b/src/InnerLayer/SLAYER/LayerInputs.jl @@ -92,11 +92,11 @@ profiles, without an intermediate file round-trip. callable of `psi` (default `1.0`). - `chi_tor` -- toroidal heat diffusivity [m²/s]. Scalar or a callable of `psi` (default `1.0`). - - `dr_val` -- resistive interchange index `D_R = E + F + H²` + - `delta_crit_D_R` -- resistive interchange index `D_R = E + F + H²` (Glasser-Greene-Johnson 1975) feeding the critical-Δ formulas - (`:lar`, `:rfitzp`, `:toroidal`). When `nothing` (default), Julia + (`:lar`, `:fitzpatrick`, `:toroidal`). When `nothing` (default), Julia derives it per-surface from the equilibrium as - `dr_val_k = D_R(ψ_k) = E_k + F_k + H_k²`, + `delta_crit_D_R_k = D_R(ψ_k) = E_k + F_k + H_k²`, consistent with Connor-Hastie-Helander 2015 (PPCF 57 065001) Eq. 59 which uses `(−D_R)` in the χ_‖-matching critical-Δ. Pass a scalar / vector / callable to override. @@ -106,14 +106,14 @@ profiles, without an intermediate file round-trip. NOT the Mercier index `D_I = E + F + H − 1/4`. The two differ by `(H − 1/2)²`, which is non-trivial on shaped equilibria (~factor 3 on DIII-D); this code uses the physically correct `D_R`. - - `dgeo_val` -- Connor 2015 (PPCF 57 065001) Eq. 59 geometric factor - used by `dc_type=:toroidal`. When `nothing` (default), an error is - raised if `dc_type=:toroidal` is also requested — the auto-derived + - `delta_crit_geo_factor` -- Connor 2015 (PPCF 57 065001) Eq. 59 geometric factor + used by `delta_crit_type=:toroidal`. When `nothing` (default), an error is + raised if `delta_crit_type=:toroidal` is also requested — the auto-derived formula additionally needs ⟨|∇ψ|²⟩ FSA which `ResistGeometry` doesn't currently expose. Pass a scalar / vector / callable to use - a prescribed value. (For `dc_type=:rfitzp` and `:lar`, dgeo_val is + a prescribed value. (For `delta_crit_type=:fitzpatrick` and `:lar`, delta_crit_geo_factor is not consulted.) - - `dc_type` -- `:none` (default), `:lar`, `:rfitzp`, or `:toroidal`. + - `delta_crit_type` -- `:none` (default), `:lar`, `:fitzpatrick`, or `:toroidal`. - `theta` -- poloidal angle at which to measure minor radius (default `0.0`, outboard midplane). - `resistivity_model` -- `SauterNeoModel()` (default), `RedlNeoModel()`, @@ -135,9 +135,9 @@ function build_slayer_inputs(equil, sings, profiles::KineticProfiles; z_i::Real=1.0, chi_perp=1.0, chi_tor=1.0, - dr_val=nothing, - dgeo_val=nothing, - dc_type::Symbol=:none, + delta_crit_D_R=nothing, + delta_crit_geo_factor=nothing, + delta_crit_type::Symbol=:none, theta::Real=0.0, compute_omega_star::Bool=true, resistivity_model::NeoResistivityModel=SauterNeoModel(), @@ -247,17 +247,17 @@ function build_slayer_inputs(equil, sings, profiles::KineticProfiles; q, zeff; lnLamb=lnL) end - # dr_val: per-surface resistive interchange index D_R = E + F + H² + # delta_crit_D_R: per-surface resistive interchange index D_R = E + F + H² # (Glasser-Greene-Johnson 1975). Used by `_solve_dc_tmp` to compute # the χ_‖-matching critical-Δ via Connor-Hastie-Helander 2015 Eq. 59, # which has `(−D_R)` as a multiplier. NOT the Mercier index # D_I = E + F + H − 1/4 (see this function's docstring); we use the # physically correct D_R here. - dr_val_k = if dr_val === nothing + delta_crit_D_R_k = if delta_crit_D_R === nothing rg === nothing && throw( ArgumentError( - "build_slayer_inputs: dr_val=nothing " * + "build_slayer_inputs: delta_crit_D_R=nothing " * "requires `sing.restype` populated by " * "ForceFreeStates.resist_eval_all!. " * "Surface k=$k has restype=nothing." @@ -265,19 +265,19 @@ function build_slayer_inputs(equil, sings, profiles::KineticProfiles; ) rg.E + rg.F + rg.H^2 else - _eval(dr_val, psi) + _eval(delta_crit_D_R, psi) end - # dgeo_val: only used by dc_type=:toroidal (the Connor-Hastie- + # delta_crit_geo_factor: only used by delta_crit_type=:toroidal (the Connor-Hastie- # Helander 2015 formula). Auto-derivation requires ⟨|∇ψ|²⟩ FSA # which the current `ResistGeometry` doesn't expose; for now we - # require an explicit value if the toroidal dc_type is selected. - dgeo_val_k = if dgeo_val === nothing - dc_type === :toroidal && + # require an explicit value if the toroidal delta_crit_type is selected. + delta_crit_geo_factor_k = if delta_crit_geo_factor === nothing + delta_crit_type === :toroidal && throw( ArgumentError( - "build_slayer_inputs: dc_type=:toroidal " * - "needs `dgeo_val` (Connor 2015 PPCF 57 " * + "build_slayer_inputs: delta_crit_type=:toroidal " * + "needs `delta_crit_geo_factor` (Connor 2015 PPCF 57 " * "065001 Eq. 59 geometric factor). " * "Auto-derivation from equilibrium not " * "yet implemented; pass a scalar / vector " * @@ -286,7 +286,7 @@ function build_slayer_inputs(equil, sings, profiles::KineticProfiles; ) 0.0 else - _eval(dgeo_val, psi) + _eval(delta_crit_geo_factor, psi) end out[k] = slayer_parameters(; @@ -297,9 +297,9 @@ function build_slayer_inputs(equil, sings, profiles::KineticProfiles; chi_perp=_eval(chi_perp, psi), chi_tor=_eval(chi_tor, psi), m=m_res, n=n_res, - dr_val=dr_val_k, - dgeo_val=dgeo_val_k, - dc_type=dc_type, ising=k, + delta_crit_D_R=delta_crit_D_R_k, + delta_crit_geo_factor=delta_crit_geo_factor_k, + delta_crit_type=delta_crit_type, ising=k, resistivity_model=resistivity_model, f_trap=f_trap_kw, nu_e_star=nu_e_star_kw, diff --git a/src/InnerLayer/SLAYER/LayerParameters.jl b/src/InnerLayer/SLAYER/LayerParameters.jl index 57ee30763..d24de21e1 100644 --- a/src/InnerLayer/SLAYER/LayerParameters.jl +++ b/src/InnerLayer/SLAYER/LayerParameters.jl @@ -40,12 +40,12 @@ de-normalization. The parametrization uses `P_perp`, `P_tor`, and | `R0` | Major radius [m] | | `bt` | Toroidal field [T] | | `sval_r` | r-based magnetic shear r_s · (dq/dr) / q (Fitzpatrick convention) | -| `dr_val` | Radial width parameter at surface (input to dc_tmp) | -| `dgeo_val` | Geometric Δ (Shafranov shift factor) | +| `delta_crit_D_R` | Radial width parameter at surface (input to dc_tmp) | +| `delta_crit_geo_factor` | Geometric Δ (Shafranov shift factor) | | `eta` | Parallel resistivity entering τ_R = μ₀r_s²/η [Ω·m] | | `d_beta` | Beta-weighted ion length scale c_β · d_i [m] | | `dc_tmp` | Critical-Δ offset from chi_parallel matching | -| `dc_type` | Selector for `dc_tmp` formula | +| `delta_crit_type` | Selector for `dc_tmp` formula | The complex normalized growth rate `Q = ω + iγ` is **not** stored here; it is passed as a separate argument to `solve_inner`. @@ -77,19 +77,19 @@ Base.@kwdef struct SLAYERParameters <: InnerLayerParameters R0::Float64 bt::Float64 sval_r::Float64 - dr_val::Float64 = 0.0 - dgeo_val::Float64 = 0.0 + delta_crit_D_R::Float64 = 0.0 + delta_crit_geo_factor::Float64 = 0.0 eta::Float64 d_beta::Float64 # Critical-Δ offset dc_tmp::Float64 = 0.0 - dc_type::Symbol = :none + delta_crit_type::Symbol = :none end -# Allowed dc_type values for the critical-Δ offset. `:none` is the default +# Allowed delta_crit_type values for the critical-Δ offset. `:none` is the default # `dc_tmp = 0` branch. -const ALLOWED_DC_TYPES = (:none, :lar, :rfitzp, :toroidal) +const ALLOWED_DELTA_CRIT_TYPES = (:none, :lar, :fitzpatrick, :toroidal) """ r_based_shear(rs, q, dq_dpsi, da_dpsi) -> Float64 @@ -118,14 +118,14 @@ end # Internal: solve the Wd self-consistency loop for the chi_parallel-based # critical Δ (Connor-Hastie-Helander 2015). Returns dc_tmp as a Float64. -function _solve_dc_tmp(; dc_type::Symbol, dr_val::Real, dgeo_val::Real, +function _solve_dc_tmp(; delta_crit_type::Symbol, delta_crit_D_R::Real, delta_crit_geo_factor::Real, chi_perp::Real, t_e::Real, zeff::Real, tau_ee::Real, rs::Real, R0::Real, sval_r::Real, n_tor::Integer, max_iter::Integer=100, tol::Real=1e-10) - dc_type in ALLOWED_DC_TYPES || - throw(ArgumentError("SLAYERParameters: unknown dc_type=$dc_type. " * - "Allowed: $(ALLOWED_DC_TYPES)")) - (dc_type === :none || dr_val == 0.0) && return 0.0 + delta_crit_type in ALLOWED_DELTA_CRIT_TYPES || + throw(ArgumentError("SLAYERParameters: unknown delta_crit_type=$delta_crit_type. " * + "Allowed: $(ALLOWED_DELTA_CRIT_TYPES)")) + (delta_crit_type === :none || delta_crit_D_R == 0.0) && return 0.0 vte = sqrt(2.0 * t_e * E_CHG / M_E) chi_par_smfp = (1.581 * tau_ee * vte^2) / (1.0 + 0.2535 * zeff) @@ -150,15 +150,15 @@ function _solve_dc_tmp(; dc_type::Symbol, dr_val::Real, dgeo_val::Real, chi_par_lmfp = (2.0 * R0 * vte) / (sqrt(π) * n_tor * sval_r * Wd) chi_par = (chi_par_smfp * chi_par_lmfp) / (chi_par_smfp + chi_par_lmfp) - if dc_type === :lar - return 0.5 * (-dr_val) * π^1.5 * + if delta_crit_type === :lar + return 0.5 * (-delta_crit_D_R) * π^1.5 * (chi_par / chi_perp)^0.25 * sqrt((n_tor * sval_r) / (R0 * rs)) - elseif dc_type === :rfitzp - return -(sqrt(2.0) * π^1.5 * dr_val) / Wd - elseif dc_type === :toroidal - return 0.5 * (-dr_val) * π^1.5 * - (chi_par / chi_perp)^0.25 * dgeo_val + elseif delta_crit_type === :fitzpatrick + return -(sqrt(2.0) * π^1.5 * delta_crit_D_R) / Wd + elseif delta_crit_type === :toroidal + return 0.5 * (-delta_crit_D_R) * π^1.5 * + (chi_par / chi_perp)^0.25 * delta_crit_geo_factor end return 0.0 end @@ -168,8 +168,8 @@ end qval, sval_r, bt, rs, R0, mu_i, zeff, chi_perp, chi_tor, m, n, - dr_val=0.0, dgeo_val=0.0, - dc_type=:none, ising=0, + delta_crit_D_R=0.0, delta_crit_geo_factor=0.0, + delta_crit_type=:none, ising=0, resistivity_model=SauterNeoModel(), f_trap=nothing, nu_e_star=nothing, R_major_eff=nothing, @@ -199,8 +199,8 @@ parametrization (P_perp/P_tor/D_norm; the older magnetic/electron Prandtl - `zeff` -- effective charge - `chi_perp`, `chi_tor` -- perpendicular / toroidal heat diffusivity [m²/s] - `m`, `n` -- poloidal / toroidal mode numbers at the surface - - `dr_val`, `dgeo_val` -- inputs for the critical-Δ formula - - `dc_type` -- one of `:none`, `:lar`, `:rfitzp`, `:toroidal` + - `delta_crit_D_R`, `delta_crit_geo_factor` -- inputs for the critical-Δ formula + - `delta_crit_type` -- one of `:none`, `:lar`, `:fitzpatrick`, `:toroidal` - `ising` -- singular-surface index for traceability # Resistivity kwargs @@ -246,8 +246,8 @@ function slayer_parameters(; rs::Real, R0::Real, mu_i::Real, zeff::Real, chi_perp::Real, chi_tor::Real, m::Integer, n::Integer, - dr_val::Real=0.0, dgeo_val::Real=0.0, - dc_type::Symbol=:none, ising::Integer=0, + delta_crit_D_R::Real=0.0, delta_crit_geo_factor::Real=0.0, + delta_crit_type::Symbol=:none, ising::Integer=0, resistivity_model::NeoResistivityModel=SauterNeoModel(), f_trap::Union{Real,Nothing}=nothing, nu_e_star::Union{Real,Nothing}=nothing, @@ -344,7 +344,7 @@ function slayer_parameters(; delta_n = lu^(1.0 / 3.0) / rs # Critical-Δ offset from chi_parallel matching - dc_tmp = _solve_dc_tmp(; dc_type=dc_type, dr_val=dr_val, dgeo_val=dgeo_val, + dc_tmp = _solve_dc_tmp(; delta_crit_type=delta_crit_type, delta_crit_D_R=delta_crit_D_R, delta_crit_geo_factor=delta_crit_geo_factor, chi_perp=chi_perp, t_e=t_e, zeff=zeff, tau_ee=tau_ee, rs=rs, R0=R0, sval_r=sval_r, n_tor=n) @@ -356,8 +356,8 @@ function slayer_parameters(; Q_e=Q_e, Q_i=Q_i, iota_e=iota_e, tauk=tauk, tau_r=tau_r, delta_n=delta_n, rs=rs, R0=R0, bt=bt, sval_r=sval_r, - dr_val=dr_val, dgeo_val=dgeo_val, + delta_crit_D_R=delta_crit_D_R, delta_crit_geo_factor=delta_crit_geo_factor, eta=eta, d_beta=d_beta, - dc_tmp=dc_tmp, dc_type=dc_type + dc_tmp=dc_tmp, delta_crit_type=delta_crit_type ) end diff --git a/src/KineticForces/Compute.jl b/src/KineticForces/Compute.jl index 4175a62b4..dbfe528dd 100644 --- a/src/KineticForces/Compute.jl +++ b/src/KineticForces/Compute.jl @@ -23,7 +23,7 @@ Build the ψ-quadrature node list `[x0, interior points strictly inside (x0, xou resonances); this function owns the ordering: sort, drop near-duplicates (closer than `PANEL_MERGE_ATOL`, e.g. a kinetic resonance coinciding with a rational), and drop points within `PANEL_MERGE_ATOL` of a bound to avoid degenerate panels. Paneling the integral at -these surfaces puts the resonant torque-density peaks (reg_spot/collisionally broadened, but +these surfaces puts the resonant torque-density peaks (regularization_width/collisionally broadened, but narrow in ψ) on Gauss-Kronrod interval endpoints, which the rule handles natively instead of hunting them by adaptive bisection. """ diff --git a/src/KineticForces/EnergyIntegration.jl b/src/KineticForces/EnergyIntegration.jl index 36cc1ecef..191eb4bfb 100644 --- a/src/KineticForces/EnergyIntegration.jl +++ b/src/KineticForces/EnergyIntegration.jl @@ -73,12 +73,12 @@ For CGL there is no resonance denominator: N_cgl = x^2.5 / (i·n). x25 = x * x * sqrt(x) # x^2.5 fx = if p.f0type == "maxwellian" ComplexF64((p.we + p.wn + p.wt * (x - 1.5)) * x25) - elseif p.f0type == "jkp" + elseif p.f0type == "park" ComplexF64((p.we + p.wn + p.wt * 2) * x25) elseif p.f0type == "cgl" complex(0.0, -x25 / p.n) # x^2.5 / (i*n) else - error("f0type must be maxwellian, jkp, or cgl") + error("f0type must be maxwellian, park, or cgl") end if p.qt fx *= (x - 2.5) @@ -101,12 +101,12 @@ never carries a CGL numerator (CGL has no pole). a = p.we + p.wn + p.wt * (x - 1.5) nn = ComplexF64(a * x25) dn = ComplexF64(p.wt * x25 + a * 2.5 * x15) # d/dx[(…)·x^2.5] - elseif p.f0type == "jkp" + elseif p.f0type == "park" a = p.we + p.wn + p.wt * 2 nn = ComplexF64(a * x25) dn = ComplexF64(a * 2.5 * x15) else - error("_energy_numerator_deriv supports maxwellian and jkp") + error("_energy_numerator_deriv supports maxwellian and park") end return p.qt ? dn * (x - 2.5) + nn : dn # d/dx[N·(x-2.5)] = N′·(x-2.5) + N end @@ -302,7 +302,7 @@ all collisionalities: the collisionless case (ν ≡ 0) is the exact ν→0 limi its real-axis pole resolved analytically (see `_integrate_energy_resonant`). Collision operator types (`nutype`): `"zero"`, `"small"`, `"krook"`, `"harmonic"`. -Distribution function types (`f0type`): `"maxwellian"`, `"jkp"`, `"cgl"`. +Distribution function types (`f0type`): `"maxwellian"`, `"park"`, `"cgl"`. `ximag` is accepted for backward compatibility but no longer used — resonance poles are now handled analytically rather than by contour deformation. diff --git a/src/KineticForces/KineticForcesStructs.jl b/src/KineticForces/KineticForcesStructs.jl index 7e533d8e9..c5a85e33a 100644 --- a/src/KineticForces/KineticForcesStructs.jl +++ b/src/KineticForces/KineticForcesStructs.jl @@ -126,7 +126,7 @@ ctrl = KineticForcesControl(; (Symbol(k) => v for (k, v) in inputs["KineticForce # Energy integration parameters nutype::String = "harmonic" # Collision operator: "zero", "small", "krook", "harmonic" - f0type::String = "maxwellian" # Distribution function: "maxwellian", "jkp", "cgl" + f0type::String = "maxwellian" # Distribution function: "maxwellian", "park", "cgl" # Diagnostic parameters psilims::Vector{Float64} = [0.0, 1.0] # Integration limits in psi @@ -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..40c8c89e7 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,13 +50,67 @@ 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 end + + # Metadata pass: method tokens are data-driven, so annotate each method group. + for method_name in keys(g) + annotate_kinetic_forces!(g[method_name]) + end +end + +# Metadata table per KineticForces// group (paths relative to the method group). +# The NTV torque and kinetic energy follow Logan et al. (2013); the six drift-kinetic +# coefficient matrices are Logan 2015 Eqs 7.30-7.35. +const KF_METHOD_H5_ANNOTATIONS = [ + "nn" => (; long_name="toroidal mode number n of this torque calculation"), + "total_torque" => (; long_name="total NTV toroidal torque T_φ", units="N*m"), + "total_energy" => (; long_name="total perturbed kinetic energy 2n·δW_k", units="J"), + "psi_nsteps" => (; long_name="number of ψ_N quadrature evaluations"), + "panel_psi" => (; long_name="ψ_N panel boundaries of the radial quadrature"), + "resonance_psi" => (; long_name="ψ_N of located kinetic-resonance surfaces"), + "psi" => (; long_name="normalized poloidal flux ψ_N at quadrature evaluation points"), + "dTdpsi_real" => (; long_name="Re dT_φ/dψ_N torque density at quadrature points", units="N*m", dims=("psi",)), + "dTdpsi_imag" => (; long_name="Im dT_φ/dψ_N (2n·dδW_k/dψ_N energy density) at quadrature points", units="J", dims=("psi",)), + "T_real" => (; long_name="cumulative toroidal torque T_φ(ψ_N) (trapezoidal)", units="N*m", dims=("psi",)), + "T_imag" => (; long_name="cumulative 2n·δW_k(ψ_N) (trapezoidal)", units="J", dims=("psi",)), + "EnergyIntegrals/psi" => (; long_name="ψ_N of each energy-integration record"), + "EnergyIntegrals/lambda" => (; long_name="pitch λ = μB0/E of each record"), + "EnergyIntegrals/ell" => (; long_name="bounce harmonic ℓ of each record"), + "EnergyIntegrals/leff" => (; long_name="effective bounce harmonic ℓ_eff of each record"), + "EnergyIntegrals/torque_real" => (; long_name="Re of the record's torque contribution", units="N*m"), + "EnergyIntegrals/torque_imag" => (; long_name="Im of the record's torque contribution", units="N*m"), + "EnergyIntegrals/kinetic_energy_real" => (; long_name="Re of the record's kinetic energy contribution", units="J"), + "EnergyIntegrals/kinetic_energy_imag" => (; long_name="Im of the record's kinetic energy contribution", units="J"), + "EnergyIntegrals/trajectory_offsets" => (; long_name="ragged-array offsets: record k spans offsets[k]+1:offsets[k+1] of the *_all arrays"), + "EnergyIntegrals/x_all" => (; long_name="normalized energy x = E/T abscissae of all integration trajectories (concatenated)"), + "EnergyIntegrals/integrand_real_all" => (; long_name="Re of the energy-space torque integrand along all trajectories (concatenated)"), + "EnergyIntegrals/integrand_imag_all" => (; long_name="Im of the energy-space torque integrand along all trajectories (concatenated)"), + "EnergyIntegrals/integral_real_all" => (; long_name="Re of the cumulative energy-space integral along all trajectories (concatenated)"), + "EnergyIntegrals/integral_imag_all" => (; long_name="Im of the cumulative energy-space integral along all trajectories (concatenated)"), + "KineticMatrices/matrix_1" => (; long_name="drift-kinetic coefficient matrix 1 of 6 (Logan 2015 Eqs 7.30-7.35)", dims=("mode", "mode")), + "KineticMatrices/matrix_2" => (; long_name="drift-kinetic coefficient matrix 2 of 6 (Logan 2015 Eqs 7.30-7.35)", dims=("mode", "mode")), + "KineticMatrices/matrix_3" => (; long_name="drift-kinetic coefficient matrix 3 of 6 (Logan 2015 Eqs 7.30-7.35)", dims=("mode", "mode")), + "KineticMatrices/matrix_4" => (; long_name="drift-kinetic coefficient matrix 4 of 6 (Logan 2015 Eqs 7.30-7.35)", dims=("mode", "mode")), + "KineticMatrices/matrix_5" => (; long_name="drift-kinetic coefficient matrix 5 of 6 (Logan 2015 Eqs 7.30-7.35)", dims=("mode", "mode")), + "KineticMatrices/matrix_6" => (; long_name="drift-kinetic coefficient matrix 6 of 6 (Logan 2015 Eqs 7.30-7.35)", dims=("mode", "mode")) +] + +# Attach long_name/units/dims + the ψ_N quadrature scale to one method group. +function annotate_kinetic_forces!(method_g) + ann = Utilities.HDF5Annotations + ann.annotate!(method_g, KF_METHOD_H5_ANNOTATIONS) + ann.make_scale!(method_g, "psi", "psi") + for a in ("dTdpsi_real", "dTdpsi_imag", "T_real", "T_imag") + ann.attach_scale!(method_g, a, 1, "psi", "psi") + end + return nothing end """ @@ -70,7 +124,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/FieldReconstruction.jl b/src/PerturbedEquilibrium/FieldReconstruction.jl index e491477c4..16d9d57a1 100644 --- a/src/PerturbedEquilibrium/FieldReconstruction.jl +++ b/src/PerturbedEquilibrium/FieldReconstruction.jl @@ -18,7 +18,7 @@ where χ₁ = 2π·Ψ₀ [Park Phys. Plasmas 14, 052110 (2007) eq. 8-10]. Clebsch displacement components for PENTRC (matches Fortran gpout_xclebsch): ξ^ψ = xsp_mn (unregularized) - ∂ξ^ψ/∂ψ = xmp1_mn (regularized: xsp1 * singfac²/(singfac² + reg_spot²)) + ∂ξ^ψ/∂ψ = xmp1_mn (regularized: xsp1 * singfac²/(singfac² + regularization_width²)) ξ^α = xms_mn (regularized: -A⁻¹(B·xmp1 + C·xsp), divided by χ₁ in output) Contravariant displacement from Jacobian convolution (matches Fortran gpeq_contra): @@ -56,14 +56,14 @@ Tuple of (xi_modes, b_modes) NamedTuples: - `xi_modes.clebsch_psi`: ξ^ψ for PENTRC (= xi_psi, unregularized) - `xi_modes.clebsch_psi1`: ∂ξ^ψ/∂ψ regularized for PENTRC - `xi_modes.clebsch_alpha`: ξ^α/χ₁ regularized for PENTRC (divided by χ₁ per gpout_xclebsch) - - `xi_modes.theta_reg`: ξ^θ regularized (= xmt, from gpeq_contra with reg_spot smoothing) - - `xi_modes.zeta_reg`: ξ^ζ regularized (= xmz, from gpeq_contra with reg_spot smoothing) + - `xi_modes.theta_reg`: ξ^θ regularized (= xmt, from gpeq_contra with regularization_width smoothing) + - `xi_modes.zeta_reg`: ξ^ζ regularized (= xmz, from gpeq_contra with regularization_width smoothing) - `xi_modes.cova_psi/theta/zeta`: covariant displacement (from gpeq_cova) - `b_modes.psi`: b^ψ [npsi, mpert] - `b_modes.b_psi_area_weighted`: b^ψ / ⟨J·|∇ψ|⟩_θ (area-normalized, for b_n computation) - `b_modes.theta`: b^θ [npsi, mpert] - `b_modes.zeta`: b^ζ [npsi, mpert] - - `b_modes.theta_reg/zeta_reg`: regularized b^θ, b^ζ (from gpeq_sol with reg_spot smoothing) + - `b_modes.theta_reg/zeta_reg`: regularized b^θ, b^ζ (from gpeq_sol with regularization_width smoothing) - `b_modes.cova_psi/theta/zeta`: covariant field (from gpeq_cova) """ function reconstruct_physical_fields( @@ -177,8 +177,8 @@ function reconstruct_physical_fields( psi_J=xwp_modes, # J·ξ^ψ (Jacobian-weighted, from gpeq_contra) theta=xwt_modes, # ξ^θ contravariant (from gpeq_contra) zeta=xwz_modes, # ξ^ζ contravariant (from gpeq_contra) - theta_reg=xmt_modes, # ξ^θ regularized (from gpeq_contra, smoothed by reg_spot) - zeta_reg=xmz_modes, # ξ^ζ regularized (from gpeq_contra, smoothed by reg_spot) + theta_reg=xmt_modes, # ξ^θ regularized (from gpeq_contra, smoothed by regularization_width) + zeta_reg=xmz_modes, # ξ^ζ regularized (from gpeq_contra, smoothed by regularization_width) clebsch_psi=clebsch_psi, # ξ^ψ for PENTRC clebsch_psi1=clebsch_psi1, # ∂ξ^ψ/∂ψ regularized for PENTRC clebsch_alpha=clebsch_alpha, # ξ^α/χ₁ regularized for PENTRC @@ -192,7 +192,7 @@ function reconstruct_physical_fields( b_psi_area_weighted=Jb_psi_modes, # b^ψ / ⟨J·|∇ψ|⟩_θ (area-normalized, for b_n) theta=b_theta_modes, # b^θ unregularized zeta=b_zeta_modes, # b^ζ unregularized - theta_reg=b_theta_reg, # b^θ regularized (from gpeq_sol with reg_spot) + theta_reg=b_theta_reg, # b^θ regularized (from gpeq_sol with regularization_width) zeta_reg=b_zeta_reg, # b^ζ regularized cova_psi=bvp_modes, # covariant b_ψ (from gpeq_cova) cova_theta=bvt_modes, # covariant b_θ (from gpeq_cova) @@ -329,10 +329,10 @@ Compute Clebsch displacement components for PENTRC output. Matches Fortran gpeq_sol regularization + gpout_xclebsch output convention: - `clebsch_psi` = ξ^ψ (unregularized, same as xi_psi_modes) - - `clebsch_psi1` = xmp1 = ∂ξ^ψ/∂ψ × singfac²/(singfac² + reg_spot²) + - `clebsch_psi1` = xmp1 = ∂ξ^ψ/∂ψ × singfac²/(singfac² + regularization_width²) - `clebsch_alpha` = xms/χ₁ (regularized ξ^α divided by χ₁ per gpout_xclebsch convention) -When reg_spot=0, clebsch_psi1 = xi_psi1 and clebsch_alpha = xi_s/χ₁ (no regularization). +When regularization_width=0, clebsch_psi1 = xi_psi1 and clebsch_alpha = xi_s/χ₁ (no regularization). The regularized xms is computed as -A⁻¹(B·xmp1 + C·xsp) matching Fortran gpeq_sol, where A, B, C are the stability matrices evaluated at each ψ via ffit interpolants. @@ -357,10 +357,10 @@ function compute_clebsch_displacements( clebsch_psi1 = copy(xi_psi1_modes) # will be regularized below clebsch_alpha = xi_s_modes ./ chi1 # ξ^α/χ₁ (will be regularized below) - reg_spot = ctrl.reg_spot - @assert reg_spot >= 0 "reg_spot must be non-negative (got $reg_spot)" + regularization_width = ctrl.regularization_width + @assert regularization_width >= 0 "regularization_width must be non-negative (got $regularization_width)" - if reg_spot == 0 + if regularization_width == 0 return clebsch_psi, clebsch_psi1, clebsch_alpha end @@ -390,7 +390,7 @@ function compute_clebsch_displacements( for ipert in 1:mpert m = mlow + ipert - 1 singfac = m - nn * q - reg_factor = singfac^2 / (singfac^2 + reg_spot^2) + reg_factor = singfac^2 / (singfac^2 + regularization_width^2) clebsch_psi1[ipsi, ipert] = xi_psi1_modes[ipsi, ipert] * reg_factor xmp1_vec[ipert] = clebsch_psi1[ipsi, ipert] end @@ -498,7 +498,7 @@ function compute_contra_displacements( mlow = ffs_intr.mlow nn = ffs_intr.nlow chi1 = 2π * equil.psio - reg_spot = ctrl.reg_spot + regularization_width = ctrl.regularization_width fc = metric.fourier_coeffs xwp_modes = zeros(ComplexF64, npsi, mpert) @@ -581,14 +581,14 @@ function compute_contra_displacements( # Regularize xwt/xwz → xmt/xmz (matches Fortran gpeq_contra) xmt_modes = copy(xwt_modes) xmz_modes = copy(xwz_modes) - if reg_spot > 0 + if regularization_width > 0 Threads.@threads :static for ipsi in 1:npsi psi_norm = psi_grid[ipsi] q = equil.profiles.q_spline(psi_norm) for ipert in 1:mpert m = mlow + ipert - 1 singfac = m - nn * q - reg_factor = singfac^2 / (singfac^2 + reg_spot^2) + reg_factor = singfac^2 / (singfac^2 + regularization_width^2) xmt_modes[ipsi, ipert] = xwt_modes[ipsi, ipert] * reg_factor xmz_modes[ipsi, ipert] = xwz_modes[ipsi, ipert] * reg_factor end diff --git a/src/PerturbedEquilibrium/PerturbedEquilibrium.jl b/src/PerturbedEquilibrium/PerturbedEquilibrium.jl index 5e9429d5b..3b83889c2 100644 --- a/src/PerturbedEquilibrium/PerturbedEquilibrium.jl +++ b/src/PerturbedEquilibrium/PerturbedEquilibrium.jl @@ -116,7 +116,7 @@ function compute_perturbed_equilibrium( # Same control surface as the coil branch above: psilim, the integration # limit (Fortran: gpec/gpec.f:431 `field_bs_psi(psilim, ...)`). Without it # the normalization was taken on the equilibrium-spline limit, which differs - # whenever dmlim/qhigh/psiedge truncation moves psilim inward. + # whenever dmlim/qhigh/dW_edge_scan_start truncation moves psilim inward. convert_forcing_normalization!(modes_n, norm_tag, equil, n, minimum(m_vals), maximum(m_vals); psi=ffs_intr.psilim) end diff --git a/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl b/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl index 96f982386..775b46b90 100644 --- a/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl +++ b/src/PerturbedEquilibrium/PerturbedEquilibriumStructs.jl @@ -27,7 +27,7 @@ Medium Priority (defer for MWE): Regularization: # High Priority (MWE) - - `reg_spot::Float64` - Regularization width for singular surface smoothing (default: 0.05). Set to 0 to disable. Must be ≥ 0. + - `regularization_width::Float64` - Regularization width for singular surface smoothing (default: 0.05). Set to 0 to disable. Must be ≥ 0. """ @kwdef mutable struct PerturbedEquilibriumControl # High Priority (MWE) @@ -47,7 +47,7 @@ Regularization: # Regularization width for singular surface smoothing (matches Fortran gpec.f reg_spot). # Set to 0 to disable regularization. Must be non-negative. - reg_spot::Float64 = 5e-2 + regularization_width::Float64 = 5e-2 end """ diff --git a/src/PerturbedEquilibrium/Utils.jl b/src/PerturbedEquilibrium/Utils.jl index a7f06db8c..f563d4c2a 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,10 +219,103 @@ 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 energy_group["toroidal_torque"] = state.toroidal_torque + + annotate_pe!(pe_group) + end +end + +# Metadata tables for the PerturbedEquilibrium group, applied post-write (paths are +# relative to the PerturbedEquilibrium group). Field-representation naming follows +# docs/src/conventions.md (Pharr 2026): b = bare, b̄ = area-weighted, b̃ = +# root-area-weighted; all in tesla. +const PE_H5_ANNOTATIONS = [ + "ForcingModes/n" => (; long_name="toroidal mode number of each forcing mode"), + "ForcingModes/m" => (; long_name="poloidal mode number of each forcing mode"), + "ForcingModes/amplitude" => (; long_name="complex forcing amplitude of each mode", units="T"), + "forcing_b" => (; long_name="control-surface forcing spectrum, bare normal field b", units="T", dims=("mode",)), + "forcing_b_root_area" => (; long_name="control-surface forcing spectrum, root-area-weighted field b̃ (coordinate-invariant)", units="T", dims=("mode",)), + "forcing_b_area" => (; long_name="control-surface forcing spectrum, area-weighted field b̄ (Φ = A·b̄)", units="T", dims=("mode",)), + "response_b" => (; long_name="control-surface response spectrum, bare normal field b", units="T", dims=("mode",)), + "response_b_root_area" => (; long_name="control-surface response spectrum, root-area-weighted field b̃ (coordinate-invariant)", units="T", dims=("mode",)), + "response_b_area" => (; long_name="control-surface response spectrum, area-weighted field b̄ (Φ = A·b̄)", units="T", dims=("mode",)), + "ResponseMatrices/plasma_inductance" => (; long_name="plasma inductance Λ̃ in root-area-weighted field space", dims=("mode", "mode")), + "ResponseMatrices/surface_inductance" => (; long_name="surface inductance L̃ in root-area-weighted field space", dims=("mode", "mode")), + "ResponseMatrices/permeability" => (; long_name="permeability P̃ = Λ̃·L̃⁻¹ in root-area-weighted field space", dims=("mode", "mode")), + "ResponseMatrices/reluctance" => (; long_name="reluctance ϱ̃ in root-area-weighted field space", dims=("mode", "mode")), + "ResponseMatrices/rootarea_to_area_weight_operator" => (; long_name="operator S = Σ/√A at ψ_lim; b̄ = S·b̃", dims=("mode", "mode")), + "ResponseMatrices/surface_area" => (; long_name="control-surface scalar area A = ∮J|∇ψ|dθ; Φ = A·b̄", units="m^2"), + "Response/psi_n" => (; long_name="normalized poloidal flux ψ_N grid shared by the response profiles"), + "Response/xi_psi" => (; long_name="contravariant radial displacement ξ^ψ = ξ·∇ψ_N", dims=("psi", "mode")), + "Response/xi_psi_J" => (; long_name="Jacobian-weighted contravariant radial displacement J·ξ^ψ", units="m^3", dims=("psi", "mode")), + "Response/xi_theta" => (; long_name="Jacobian-weighted contravariant poloidal displacement J·ξ^θ", units="m^3", dims=("psi", "mode")), + "Response/xi_zeta" => (; long_name="Jacobian-weighted contravariant toroidal displacement J·ξ^ζ", units="m^3", dims=("psi", "mode")), + "Response/xi_theta_reg" => (; long_name="regularized Jacobian-weighted contravariant poloidal displacement J·ξ^θ", units="m^3", dims=("psi", "mode")), + "Response/xi_zeta_reg" => (; long_name="regularized Jacobian-weighted contravariant toroidal displacement J·ξ^ζ", units="m^3", dims=("psi", "mode")), + "Response/xi_cova_psi" => (; long_name="covariant radial displacement ξ_ψ", units="m^2", dims=("psi", "mode")), + "Response/xi_cova_theta" => (; long_name="covariant poloidal displacement ξ_θ", units="m^2", dims=("psi", "mode")), + "Response/xi_cova_zeta" => (; long_name="covariant toroidal displacement ξ_ζ", units="m^2", dims=("psi", "mode")), + "Response/clebsch_psi" => (; long_name="Clebsch displacement component ξ^ψ (PENTRC input, gpout_xclebsch convention)", dims=("psi", "mode")), + "Response/clebsch_psi1" => (; long_name="regularized ψ_N derivative of ξ^ψ (× singfac²/(singfac²+regularization_width²))", dims=("psi", "mode")), + "Response/clebsch_alpha" => (; long_name="Clebsch displacement component ξ^α/χ₁ (PENTRC input, gpout_xclebsch convention)", dims=("psi", "mode")), + "Response/xi_n" => (; long_name="physical normal displacement ξ_n", units="m", dims=("psi", "mode")), + "Response/xi_R" => (; long_name="cylindrical displacement component ξ_R (mode space)", units="m", dims=("psi", "mode")), + "Response/xi_Z" => (; long_name="cylindrical displacement component ξ_Z (mode space)", units="m", dims=("psi", "mode")), + "Response/xi_phi" => (; long_name="cylindrical displacement component ξ_φ (mode space)", units="m", dims=("psi", "mode")), + "Response/b_psi_area_weighted" => (; long_name="area-normalized radial field b^ψ/⟨J|∇ψ|⟩_θ", units="T", dims=("psi", "mode")), + "Response/b_n" => (; long_name="physical normal field b_n", units="T", dims=("psi", "mode")), + "Response/b_theta" => (; long_name="Jacobian-weighted contravariant poloidal field J·b^θ", units="T*m^2", dims=("psi", "mode")), + "Response/b_zeta" => (; long_name="Jacobian-weighted contravariant toroidal field J·b^ζ", units="T*m^2", dims=("psi", "mode")), + "Response/b_theta_reg" => (; long_name="regularized Jacobian-weighted contravariant poloidal field J·b^θ", units="T*m^2", dims=("psi", "mode")), + "Response/b_zeta_reg" => (; long_name="regularized Jacobian-weighted contravariant toroidal field J·b^ζ", units="T*m^2", dims=("psi", "mode")), + "Response/b_cova_psi" => (; long_name="covariant radial field b_ψ", units="T*m", dims=("psi", "mode")), + "Response/b_cova_theta" => (; long_name="covariant poloidal field b_θ", units="T*m", dims=("psi", "mode")), + "Response/b_cova_zeta" => (; long_name="covariant toroidal field b_ζ", units="T*m", dims=("psi", "mode")), + "Response/b_R" => (; long_name="cylindrical field component b_R (mode space)", units="T", dims=("psi", "mode")), + "Response/b_Z" => (; long_name="cylindrical field component b_Z (mode space)", units="T", dims=("psi", "mode")), + "Response/b_phi" => (; long_name="cylindrical field component b_φ (mode space)", units="T", dims=("psi", "mode")), + "SingularCoupling/C_resonant_area_weighted_field" => (; long_name="coupling matrix: applied b̃ → resonant area-weighted field b̄^r = Φ^r/A^r", dims=("surface", "mode")), + "SingularCoupling/C_resonant_current" => (; long_name="coupling matrix: applied b̃ → pitch-resonant current", units="A/T", dims=("surface", "mode")), + "SingularCoupling/C_island_width_sq" => (; long_name="coupling matrix: applied b̃ → squared island half-width", units="1/T", dims=("surface", "mode")), + "SingularCoupling/C_penetrated_area_weighted_field" => (; long_name="coupling matrix: applied b̃ → penetrated area-weighted field", dims=("surface", "mode")), + "SingularCoupling/C_delta_prime" => (; long_name="coupling matrix: applied b̃ → forcing-driven Δ'", units="1/T", dims=("surface", "mode")), + "SingularCoupling/resonant_area_weighted_field" => + (; long_name="resonant area-weighted field b̄^r = Φ^r/A^r per rational surface (coordinate-invariant)", units="T", dims=("surface",)), + "SingularCoupling/resonant_current" => (; long_name="pitch-resonant current per rational surface", units="A", dims=("surface",)), + "SingularCoupling/island_width_sq" => (; long_name="squared island half-width per rational surface (in ψ_N²)", dims=("surface",)), + "SingularCoupling/penetrated_area_weighted_field" => (; long_name="penetrated area-weighted field per rational surface", units="T", dims=("surface",)), + "SingularCoupling/delta_prime" => (; long_name="forcing-driven tearing Δ' per rational surface (Riccati; response to applied forcing)", dims=("surface",)), + "SingularCoupling/forcing_solution_weights" => (; long_name="weights of the forcing solutions in the singular-coupling decomposition", dims=("surface",)), + "SingularCoupling/rational_area" => (; long_name="scalar surface area A^r of each rational surface", units="m^2", dims=("surface",)), + "SingularCoupling/island_half_width" => (; long_name="island half-width per rational surface (in ψ_N)", dims=("surface",)), + "SingularCoupling/chirikov_parameter" => (; long_name="Chirikov overlap parameter: island half-width / half-distance to the neighbouring rational surface", dims=("surface",)), + "SingularCoupling/rational_psi" => (; long_name="normalized poloidal flux ψ_N of each rational surface"), + "SingularCoupling/rational_q" => (; long_name="safety factor q = m/n at each rational surface", dims=("surface",)), + "SingularCoupling/rational_m_res" => (; long_name="resonant poloidal mode number m at each rational surface", dims=("surface",)), + "SingularCoupling/rational_n" => (; long_name="resonant toroidal mode number n at each rational surface", dims=("surface",)), + "Energies/vacuum_energy" => (; long_name="perturbed vacuum energy", units="J"), + "Energies/surface_energy" => (; long_name="perturbed surface energy", units="J"), + "Energies/plasma_energy" => (; long_name="perturbed plasma energy", units="J"), + "Energies/toroidal_torque" => (; long_name="net toroidal torque on the plasma", units="N*m") +] + +# Attach long_name/units/dims + dimension scales to the PerturbedEquilibrium group. +function annotate_pe!(pe_group) + ann = Utilities.HDF5Annotations + ann.annotate!(pe_group, PE_H5_ANNOTATIONS) + ann.make_scale!(pe_group, "Response/psi_n", "psi") + for (path, _) in PE_H5_ANNOTATIONS + startswith(path, "Response/") && path != "Response/psi_n" || continue + ann.attach_scale!(pe_group, path, 1, "Response/psi_n", "psi") + end + ann.make_scale!(pe_group, "SingularCoupling/rational_psi", "psi_rational") + for (path, _) in PE_H5_ANNOTATIONS + startswith(path, "SingularCoupling/") && path != "SingularCoupling/rational_psi" || continue + ann.attach_scale!(pe_group, path, 1, "SingularCoupling/rational_psi", "psi_rational") end + return nothing end diff --git a/src/Rerun.jl b/src/Rerun.jl index 2a5a81237..9455c8001 100644 --- a/src/Rerun.jl +++ b/src/Rerun.jl @@ -28,14 +28,13 @@ end read_equilibrium_ingest(in_h5) -> EquilibriumIngest Reconstruct the [`DirectIngest`](@ref)/[`InverseIngest`](@ref) stored under -`input/raw_inputs/equilibrium/` (the inverse of the field-by-field write in +`Input/RawInputs/Equilibrium/` (the inverse of the field-by-field write in `write_outputs_to_HDF5`). Returns `nothing` when the group is absent, which marks an analytic equilibrium — replayed from its TOML section rather than stored arrays. """ function read_equilibrium_ingest(in_h5) - group_path = "input/raw_inputs/equilibrium" - haskey(in_h5, group_path) || return nothing - group = in_h5[group_path] + haskey(in_h5, H5_RAW_EQUILIBRIUM) || return nothing + group = in_h5[H5_RAW_EQUILIBRIUM] kind = read(group, "ingest_kind") T = kind == "direct" ? Equilibrium.DirectIngest : kind == "inverse" ? Equilibrium.InverseIngest : @@ -209,29 +208,29 @@ function build_inputs_from_h5(args::Vector{String}) # ignores the frozen forcing-mode snapshot. use_coils = cli.coil_source == "coils" toml_raw, ingest, source_git, preloaded_forcing, preloaded_coils = h5open(source_h5, "r") do in_h5 - haskey(in_h5, "input/gpec_toml_raw") || - error("Source HDF5 $source_h5 has no input/gpec_toml_raw — produced by a pre-rerun version of GPEC") - forcing_modes = if use_coils || !haskey(in_h5, "input/raw_inputs/forcing_terms") + haskey(in_h5, H5_INPUT_TOML) || + error("Source HDF5 $source_h5 has no $H5_INPUT_TOML — produced by a pre-rerun version of GPEC") + forcing_modes = if use_coils || !haskey(in_h5, H5_RAW_FORCING) nothing else modes = ForcingTerms.ForcingMode[] - ForcingTerms.load_forcing_from_h5_group!(modes, in_h5["input/raw_inputs/forcing_terms"]) + ForcingTerms.load_forcing_from_h5_group!(modes, in_h5[H5_RAW_FORCING]) modes end coil_sets = if use_coils - haskey(in_h5, "input/raw_inputs/coils") || - error("--coil-source coils requested but $source_h5 has no input/raw_inputs/coils " * + haskey(in_h5, H5_RAW_COILS) || + error("--coil-source coils requested but $source_h5 has no $H5_RAW_COILS " * "(the source run did not use coils, or predates coil-snapshot support)") sets = ForcingTerms.CoilSet[] - ForcingTerms.load_coils_from_h5_group!(sets, in_h5["input/raw_inputs/coils"]) + ForcingTerms.load_coils_from_h5_group!(sets, in_h5[H5_RAW_COILS]) sets else nothing end ( - read(in_h5, "input/gpec_toml_raw"), + read(in_h5, H5_INPUT_TOML), read_equilibrium_ingest(in_h5), - haskey(in_h5, "info/git_version") ? read(in_h5, "info/git_version") : "unknown", + haskey(in_h5, H5_GIT_VERSION) ? read(in_h5, H5_GIT_VERSION) : "unknown", forcing_modes, coil_sets ) @@ -266,6 +265,7 @@ function build_inputs_from_h5(args::Vector{String}) " source: $(abspath(source_h5))\n" * " output: $(abspath(joinpath(output_dir, output_name)))\n$_BANNER" + _rename_keys!(inputs["Equilibrium"], _RENAMED_EQUIL_KEYS, "Equilibrium") _drop_deprecated_keys!(inputs["Equilibrium"], _DEPRECATED_EQUIL_KEYS, "Equilibrium") eq_config = Equilibrium.EquilibriumConfig(inputs["Equilibrium"], output_dir) # Clear eq_filename: unused on replay, and a stale absolute path could mislead downstream code. diff --git a/src/Tearing/Dispersion/CoupledFullMatch.jl b/src/Tearing/Dispersion/CoupledFullMatch.jl index 8bf5fb97c..6360c8fbe 100644 --- a/src/Tearing/Dispersion/CoupledFullMatch.jl +++ b/src/Tearing/Dispersion/CoupledFullMatch.jl @@ -94,7 +94,7 @@ end Construct the 4m × 4m dispersion matrix driver. `dp_raw` must be the 2m × 2m matrix in side-major ordering (the `intr.delta_prime_raw` field populated by `ForceFreeStates.compute_delta_prime_matrix!` on the -`use_parallel=true` path). `rotation[k]` is the per-surface rotation +`integrator="stride"` path). `rotation[k]` is the per-surface rotation frequency; it shifts the per-surface inner Q argument by `i·ntor·rotation[k]`. Default zero rotation matches the static-equilibrium case. diff --git a/src/Tearing/Runner/Control.jl b/src/Tearing/Runner/Control.jl index 17128a3d6..49b5518b0 100644 --- a/src/Tearing/Runner/Control.jl +++ b/src/Tearing/Runner/Control.jl @@ -21,8 +21,8 @@ constructor. - `scan_mode` -- `:amr` (default) or `:brute_force` - `coupling_mode` -- `:uncoupled` (default, per-surface) or `:coupled` (multi-surface determinant) - - `dc_type` -- critical-Δ offset selector, one of `:none`, `:lar`, - `:rfitzp`, `:toroidal` (χ_‖-matching critical-Δ formulas, + - `delta_crit_type` -- critical-Δ offset selector, one of `:none`, `:lar`, + `:fitzpatrick`, `:toroidal` (χ_‖-matching critical-Δ formulas, Connor-Hastie-Helander 2015) - `msing_max` -- number of surfaces to include in the coupled determinant (default 3; capped at `length(sings)` at runtime) @@ -36,10 +36,10 @@ constructor. diffusivity [m²/s], used only when the kinetic file carries no usable `chi_e`/`chi_phi` profile (dataset absent or all-zero); otherwise the file's χ⊥(ψ)/χ_φ(ψ) take precedence - - `dr_val`, `dgeo_val` -- critical-Δ formula inputs. `nothing` (default) - auto-derives them from the equilibrium: `dr_val` from the resistive - interchange index `D_R = E + F + H²` at each surface, `dgeo_val` from the - toroidal geometric factor (required only by `dc_type=:toroidal`). Supply a + - `delta_crit_D_R`, `delta_crit_geo_factor` -- critical-Δ formula inputs. `nothing` (default) + auto-derives them from the equilibrium: `delta_crit_D_R` from the resistive + interchange index `D_R = E + F + H²` at each surface, `delta_crit_geo_factor` from the + toroidal geometric factor (required only by `delta_crit_type=:toroidal`). Supply a scalar only to override the auto-derivation; an explicit `0.0` disables the critical-Δ offset (Δ_crit ≡ 0) - `theta_sample` -- poloidal angle at which to sample minor radius @@ -99,7 +99,7 @@ there is one consistent interface for resistive and kinetic profiles. inner_model::Symbol = :slayer_fitzpatrick scan_mode::Symbol = :amr coupling_mode::Symbol = :uncoupled - dc_type::Symbol = :none + delta_crit_type::Symbol = :none msing_max::Int = 3 bt::Union{Float64,Nothing} = nothing @@ -107,8 +107,8 @@ there is one consistent interface for resistive and kinetic profiles. zeff::Float64 = 1.0 chi_perp::Float64 = 1.0 chi_tor::Float64 = 1.0 - dr_val::Union{Float64,Nothing} = nothing - dgeo_val::Union{Float64,Nothing} = nothing + delta_crit_D_R::Union{Float64,Nothing} = nothing + delta_crit_geo_factor::Union{Float64,Nothing} = nothing theta_sample::Float64 = 0.0 resistivity_model::Symbol = :sauter lnLambda_form::Symbol = :nrl @@ -158,7 +158,7 @@ end const _VALID_INNER_MODELS = (:slayer_fitzpatrick, :ggj_shooting, :ggj_galerkin) const _VALID_SCAN_MODES = (:amr, :brute_force) const _VALID_COUPLING_MODES = (:uncoupled, :coupled) -const _VALID_DC_TYPES = (:none, :lar, :rfitzp, :toroidal) +const _VALID_DELTA_CRIT_TYPES = (:none, :lar, :fitzpatrick, :toroidal) const _VALID_RESISTIVITY_MODELS = (:sauter, :redl, :spitzer, :spitzer_harm) const _VALID_LNLAMBDA_FORMS = (:nrl, :sauter, :wesson) @@ -172,9 +172,9 @@ function validate(ctrl::SLAYERControl) ctrl.coupling_mode in _VALID_COUPLING_MODES || throw(ArgumentError("SLAYERControl: coupling_mode=$(ctrl.coupling_mode) " * "not in $(_VALID_COUPLING_MODES)")) - ctrl.dc_type in _VALID_DC_TYPES || - throw(ArgumentError("SLAYERControl: dc_type=$(ctrl.dc_type) " * - "not in $(_VALID_DC_TYPES)")) + ctrl.delta_crit_type in _VALID_DELTA_CRIT_TYPES || + throw(ArgumentError("SLAYERControl: delta_crit_type=$(ctrl.delta_crit_type) " * + "not in $(_VALID_DELTA_CRIT_TYPES)")) ctrl.resistivity_model in _VALID_RESISTIVITY_MODELS || throw(ArgumentError("SLAYERControl: resistivity_model=$(ctrl.resistivity_model) " * "not in $(_VALID_RESISTIVITY_MODELS)")) @@ -227,6 +227,20 @@ function slayer_control_from_toml(section::AbstractDict) end end + # Deprecated [SLAYER] spellings: renamed keys and the :rfitzp value alias, accepted + # with a warning until removal after v2.0.0. + for (old, new) in ("dc_type" => "delta_crit_type", "dr_val" => "delta_crit_D_R", + "dgeo_val" => "delta_crit_geo_factor") + haskey(flat, old) || continue + @warn "`$old` in [SLAYER] was renamed to `$new`; the old key is deprecated and will be removed after v2.0.0." + haskey(flat, new) || (flat[new] = flat[old]) + delete!(flat, old) + end + if get(flat, "delta_crit_type", "") in ("rfitzp", :rfitzp) + @warn "`delta_crit_type = \"rfitzp\"` in [SLAYER] is deprecated; use `delta_crit_type = \"fitzpatrick\"`. The old value will be removed after v2.0.0." + flat["delta_crit_type"] = "fitzpatrick" + end + # Validate keys against the struct fields field_names = Set(String.(fieldnames(SLAYERControl))) unknown = [k for k in keys(flat) if !(k in field_names)] @@ -239,12 +253,12 @@ function slayer_control_from_toml(section::AbstractDict) kwargs = Dict{Symbol,Any}() for (k, v) in flat sym = Symbol(k) - if sym in (:inner_model, :scan_mode, :coupling_mode, :dc_type, + if sym in (:inner_model, :scan_mode, :coupling_mode, :delta_crit_type, :resistivity_model, :lnLambda_form) kwargs[sym] = v isa Symbol ? v : Symbol(String(v)) elseif sym in (:Q_re_range, :Q_im_range) kwargs[sym] = _as_range(v) - elseif sym in (:bt, :dr_val, :dgeo_val) + elseif sym in (:bt, :delta_crit_D_R, :delta_crit_geo_factor) # Allow explicit nothing (auto-derive) or a number (override) kwargs[sym] = v === nothing ? nothing : Float64(v) elseif sym === :boxes diff --git a/src/Tearing/Runner/HDF5Output.jl b/src/Tearing/Runner/HDF5Output.jl index 107a3be1a..d081d49ee 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,23 @@ 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 + if !result.enabled # nothing else to write + _annotate_tearing!(g) + return g + end - _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) @@ -45,56 +47,113 @@ function write_slayer_hdf5!(parent::Union{HDF5.File,HDF5.Group}, if result.control.store_scan && !isempty(result.scan_data) _write_scan_data!(g, result) end + _annotate_tearing!(g) 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) +# Metadata table for the Tearing group (paths relative to it); ragged Diagnostics +# subgroups and Scan/Surface_ groups are annotated by iteration below. +const TEARING_H5_ANNOTATIONS = [ + "enabled" => (; long_name="flag: SLAYER/tearing stage ran (1) or was disabled (0)"), + "PerSurface/ising" => (; long_name="rational-surface index of each row", dims=("surface",)), + "PerSurface/m" => (; long_name="resonant poloidal mode number m per surface", dims=("surface",)), + "PerSurface/n" => (; long_name="resonant toroidal mode number n per surface", dims=("surface",)), + "PerSurface/tau" => (; long_name="temperature ratio τ = T_i/T_e per surface", dims=("surface",)), + "PerSurface/lu" => (; long_name="Lundquist number S per surface", dims=("surface",)), + "PerSurface/c_beta" => (; long_name="compressibility factor c_β = √(β_local/(1+β_local)) per surface", dims=("surface",)), + "PerSurface/D_norm" => (; long_name="Fitzpatrick normalized ion-sound/drift scale D = (d_β/r_s)·S^(1/3)·√(τ/(1+τ)) per surface", dims=("surface",)), + "PerSurface/P_perp" => (; long_name="perpendicular magnetic Prandtl number per surface", dims=("surface",)), + "PerSurface/P_tor" => (; long_name="toroidal (momentum) magnetic Prandtl number per surface", dims=("surface",)), + "PerSurface/Q_e" => (; long_name="normalized electron diamagnetic frequency Q_e per surface", dims=("surface",)), + "PerSurface/Q_i" => (; long_name="normalized ion diamagnetic frequency Q_i per surface", dims=("surface",)), + "PerSurface/iota_e" => (; long_name="electron fraction ι_e = Q_e/(Q_e − Q_i) per surface", dims=("surface",)), + "PerSurface/tauk" => (; long_name="Q-normalization time S^(1/3)·τ_H per surface (Q = −τ_k·ω)", units="s", dims=("surface",)), + "PerSurface/tau_r" => (; long_name="resistive diffusion time τ_R per surface", units="s", dims=("surface",)), + "PerSurface/delta_n" => (; long_name="Δ'-normalization factor S^(1/3)/r_s per surface", units="1/m", dims=("surface",)), + "PerSurface/rs" => (; long_name="minor radius of each rational surface", units="m", dims=("surface",)), + "PerSurface/R0" => (; long_name="major radius", units="m", dims=("surface",)), + "PerSurface/bt" => (; long_name="toroidal field", units="T", dims=("surface",)), + "PerSurface/sval_r" => (; long_name="r-based magnetic shear r_s·(dq/dr)/q (Fitzpatrick convention)", dims=("surface",)), + "PerSurface/dr_val" => + (; long_name="resistive interchange D_R = E + F + H² for the critical-Δ formula (auto-derived from GGJ coefficients unless overridden)", dims=("surface",)), + "PerSurface/dgeo_val" => (; long_name="Connor-Hastie-Helander 2015 Eq. 59 geometric factor (0 unless supplied)", dims=("surface",)), + "PerSurface/eta" => (; long_name="parallel resistivity at each surface", units="Ohm*m", dims=("surface",)), + "PerSurface/d_beta" => (; long_name="β-weighted ion drift scale d_β", units="m", dims=("surface",)), + "PerSurface/dc_tmp" => (; long_name="critical-Δ offset from χ_∥/χ_⊥ matching (Connor-Hastie-Helander 2015 Eq. 59)", dims=("surface",)), + "PerSurface/dc_type" => (; long_name="per-surface D_c prescription label"), + "PerSurface/E" => (; long_name="Glasser-Greene-Johnson coefficient E per surface", dims=("surface",)), + "PerSurface/F" => (; long_name="Glasser-Greene-Johnson coefficient F per surface", dims=("surface",)), + "PerSurface/G" => (; long_name="Glasser-Greene-Johnson coefficient G per surface", dims=("surface",)), + "PerSurface/H" => (; long_name="Glasser-Greene-Johnson coefficient H per surface", dims=("surface",)), + "PerSurface/K" => (; long_name="Glasser-Greene-Johnson coefficient K per surface", dims=("surface",)), + "PerSurface/M" => (; long_name="Glasser-Greene-Johnson coefficient M per surface", dims=("surface",)), + "PerSurface/taua" => (; long_name="Alfvén time τ_A per surface", units="s", dims=("surface",)), + "PerSurface/taur" => (; long_name="resistive diffusion time τ_R per surface", units="s", dims=("surface",)), + "PerSurface/v1" => (; long_name="dV/dψ_N at each surface", units="m^3", dims=("surface",)), + "PerSurface/DpMatrix/real" => (; long_name="Re of the full Δ' matrix coupling the rational surfaces", dims=("surface", "surface")), + "PerSurface/DpMatrix/imag" => (; long_name="Im of the full Δ' matrix coupling the rational surfaces", dims=("surface", "surface")), + "Roots/Q_root_real" => (; long_name="Re of the dispersion-root normalized frequency Q (NaN = no root)"), + "Roots/Q_root_imag" => (; long_name="Im of the dispersion-root normalized frequency Q (NaN = no root)"), + "Roots/omega_Hz" => (; long_name="mode rotation angular frequency of each root", units="rad/s"), + "Roots/gamma_Hz" => (; long_name="growth rate of each root", units="1/s"), + "Roots/no_root" => (; long_name="flag: no usable dispersion root found (Q_root is NaN, ω/γ are placeholders)"), + "LayerWidths/ising" => (; long_name="rational-surface index of each row", dims=("surface",)), + "LayerWidths/m" => (; long_name="resonant poloidal mode number m per surface", dims=("surface",)), + "LayerWidths/n" => (; long_name="resonant toroidal mode number n per surface", dims=("surface",)), + "LayerWidths/dels_db_real" => (; long_name="Re of the dimensionless layer thickness δ_s/d_β", dims=("surface",)), + "LayerWidths/dels_db_imag" => (; long_name="Im of the dimensionless layer thickness δ_s/d_β", dims=("surface",)), + "LayerWidths/delta_s_real" => (; long_name="Re of the complex resistive layer thickness δ_s (Riccati)", dims=("surface",)), + "LayerWidths/delta_s_imag" => (; long_name="Im of the complex resistive layer thickness δ_s (Riccati)", dims=("surface",)), + "LayerWidths/delta_s_m" => (; long_name="physical resistive layer thickness |δ_s|", units="m", dims=("surface",)), + "LayerWidths/d_beta" => (; long_name="β-weighted ion drift scale d_β", units="m", dims=("surface",)) +] + +const TEARING_RAGGED_H5_ANNOTATIONS = [ + "flat_real" => (; long_name="Re of the concatenated complex entries (rows delimited by offsets)"), + "flat_imag" => (; long_name="Im of the concatenated complex entries (rows delimited by offsets)"), + "offsets" => (; long_name="ragged-array offsets: row k spans offsets[k]+1:offsets[k+1]") +] + +const TEARING_SCAN_H5_ANNOTATIONS = [ + "kind" => (; long_name="scan kind: brute_force or amr"), + "Q_real" => (; long_name="Re of the sampled normalized frequency Q"), + "Q_imag" => (; long_name="Im of the sampled normalized frequency Q"), + "Delta_real" => (; long_name="Re of the inner-layer matching Δ(Q)"), + "Delta_imag" => (; long_name="Im of the inner-layer matching Δ(Q)"), + "re_axis" => (; long_name="Re(Q) axis of the brute-force scan grid"), + "im_axis" => (; long_name="Im(Q) axis of the brute-force scan grid"), + "n_cells" => (; long_name="number of AMR cells sampled"), + "truncated" => (; long_name="flag: AMR refinement stopped at the cell cap") +] + +# Attach long_name/units/dims to everything write_slayer_hdf5! wrote. +function _annotate_tearing!(g) + ann = Utilities.HDF5Annotations + ann.annotate!(g, TEARING_H5_ANNOTATIONS) + if haskey(g, "Diagnostics") + for sub in keys(g["Diagnostics"]) + ann.annotate!(g["Diagnostics"][sub], TEARING_RAGGED_H5_ANNOTATIONS) + end + end + if haskey(g, "Scan") + for sub in keys(g["Scan"]) + sg = g["Scan"][sub] + ann.annotate!(sg, TEARING_SCAN_H5_ANNOTATIONS) + # Brute-force scans store 2-D (re, im) grids; AMR stores flat samples. + if haskey(sg, "Q_real") && ndims(sg["Q_real"]) == 2 + for a in ("Q_real", "Q_imag", "Delta_real", "Delta_imag") + haskey(sg, a) && (attrs(sg[a])["dims"] = "(re_axis, im_axis)") + end + end + end + end 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) @@ -103,15 +162,18 @@ function _write_per_surface!(g, params::AbstractVector{SLAYERParameters}, for fname in (:tau, :lu, :c_beta, :D_norm, :P_perp, :P_tor, :Q_e, :Q_i, :iota_e, :tauk, :tau_r, :delta_n, - :rs, :R0, :bt, :sval_r, :dr_val, :dgeo_val, + :rs, :R0, :bt, :sval_r, :eta, :d_beta, :dc_tmp) ps[String(fname)] = Float64[getfield(p, fname) for p in params] end - # Store dc_type per-surface as string array - ps["dc_type"] = String[String(p.dc_type) for p in params] + # HDF5 leaf names keep the legacy spellings (schema stability); the struct fields were renamed. + ps["dr_val"] = Float64[getfield(p, :delta_crit_D_R) for p in params] + ps["dgeo_val"] = Float64[getfield(p, :delta_crit_geo_factor) for p in params] + # Store the per-surface critical-Δ prescription label as string array + ps["dc_type"] = String[String(p.delta_crit_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 +183,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 +196,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 +213,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 +230,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 +238,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 +269,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/src/Tearing/Runner/run_slayer.jl b/src/Tearing/Runner/run_slayer.jl index 0e254e625..ea82968aa 100644 --- a/src/Tearing/Runner/run_slayer.jl +++ b/src/Tearing/Runner/run_slayer.jl @@ -396,9 +396,9 @@ function run_slayer(equil, ffs_intr, control::SLAYERControl; zeff=control.zeff, chi_perp=chi_perp, chi_tor=chi_tor, - dr_val=control.dr_val, - dgeo_val=control.dgeo_val, - dc_type=control.dc_type, + delta_crit_D_R=control.delta_crit_D_R, + delta_crit_geo_factor=control.delta_crit_geo_factor, + delta_crit_type=control.delta_crit_type, theta=control.theta_sample, resistivity_model=_build_resistivity_model(control.resistivity_model), lnLambda_form=control.lnLambda_form) diff --git a/src/Utilities/HDF5Annotations.jl b/src/Utilities/HDF5Annotations.jl new file mode 100644 index 000000000..416152c8f --- /dev/null +++ b/src/Utilities/HDF5Annotations.jl @@ -0,0 +1,93 @@ +""" + HDF5Annotations + +Self-describing metadata for `gpec.h5` (the contract in +`docs/development/hdf5-conventions.md`): every dataset carries a `long_name` and +`units` attribute, array datasets carry a `dims` axis-name attribute, and coordinate +datasets are marked as HDF5 Dimension Scales (netCDF-4 coordinate variables) attached +to the arrays that share their axis, so h5py/xarray/HDFView read the file unaided. + +Writers stay table-driven: each writer keeps a table of `path => (; long_name, units, +dims)` entries next to it and calls [`annotate!`](@ref) once after its datasets are +written. Paths absent from the file are skipped silently (many writes are conditional). +""" +module HDF5Annotations + +using HDF5 +using Dates + +export annotate!, make_scale!, attach_scale!, write_root_attrs! + +""" + annotate!(parent, table) + +Apply a metadata table to datasets under `parent` (an open `HDF5.File` or group). +`table` iterates `path => meta` pairs where `meta` is a NamedTuple with fields +`long_name` (required), `units` (default `"1"` = dimensionless), and optionally +`dims` — a tuple of axis names in Julia (column-major) order, axis 1 first, stored +as the greppable string attribute `dims = "(psi, m)"`. Missing paths are skipped. +""" +function annotate!(parent::Union{HDF5.File,HDF5.Group}, table) + for (path, meta) in table + haskey(parent, path) || continue + a = attrs(parent[path]) + a["long_name"] = String(meta.long_name) + a["units"] = String(get(meta, :units, "1")) + d = get(meta, :dims, nothing) + d === nothing || (a["dims"] = "(" * join(d, ", ") * ")") + end + return parent +end + +""" + make_scale!(parent, path, name) + +Mark the dataset at `path` as an HDF5 Dimension Scale named `name`. No-op when the +path is absent. +""" +function make_scale!(parent::Union{HDF5.File,HDF5.Group}, path::AbstractString, name::AbstractString) + haskey(parent, path) || return nothing + HDF5.API.h5ds_set_scale(parent[path], String(name)) + return nothing +end + +""" + attach_scale!(parent, path, julia_axis, scale_path, label) + +Attach the Dimension Scale at `scale_path` to Julia axis `julia_axis` (axis 1 first) +of the dataset at `path`, and label that dimension. The H5DS C API indexes file +(row-major) dimensions, so Julia axis `k` of an `N`-d dataset is C index `N - k`. +No-op when either path is absent or the axis lengths disagree. +""" +function attach_scale!(parent::Union{HDF5.File,HDF5.Group}, path::AbstractString, julia_axis::Int, + scale_path::AbstractString, label::AbstractString) + (haskey(parent, path) && haskey(parent, scale_path)) || return nothing + dset = parent[path] + sc = parent[scale_path] + size(dset, julia_axis) == length(sc) || return nothing + cdim = ndims(dset) - julia_axis + HDF5.API.h5ds_attach_scale(dset, sc, cdim) + # h5ds_set_label's wrapper types the C `const char*` as Ref{UInt8}; pass a + # NUL-terminated byte buffer instead of a String. + HDF5.API.h5ds_set_label(dset, cdim, Vector{UInt8}(codeunits(String(label) * "\0"))) + return nothing +end + +""" + write_root_attrs!(file; title) + +Stamp the file-level contract: `schema_version`, `Conventions`, `references`, +`title` (run description), `date_created` (ISO 8601 UTC). The code version lives in +`Info/git_version`. +""" +function write_root_attrs!(file::HDF5.File; title::AbstractString) + a = attrs(file) + a["schema_version"] = "2.0" + a["Conventions"] = "GPEC-HDF5-2.0" + a["references"] = "docs/development/hdf5-conventions.md; https://openfusiontoolkit.github.io/GPEC/dev/" + a["title"] = String(title) + a["date_created"] = Dates.format(Dates.now(UTC), dateformat"yyyy-mm-dd\THH:MM:SS\Z") + return file +end + +end # module HDF5Annotations diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 5dab6bdb2..1dfaf8bd1 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -13,6 +13,8 @@ mathematical utilities. - `PhysicalConstants`: SI physical constants matching Fortran GPEC/SLAYER values - `NeoclassicalResistivity`: Spitzer/Sauter/Redl resistivity closures shared by the GGJ and SLAYER inner-layer models + - `HDF5Annotations`: self-describing metadata (long_name/units/dims attributes and + HDF5 Dimension Scales) for the gpec.h5 output """ module Utilities @@ -22,6 +24,7 @@ include("PhysicalConstants.jl") include("KineticProfiles.jl") include("NeoclassicalResistivity.jl") include("GridUtilities.jl") +include("HDF5Annotations.jl") using .FourierTransforms export FourierTransform, inverse, compute_fourier_coefficients @@ -34,6 +37,9 @@ export MU_0, M_E, M_P, E_CHG, K_B, EPS_0 export KineticProfiles +using .HDF5Annotations +export HDF5Annotations + using .NeoclassicalResistivity export NeoclassicalResistivity export NeoResistivityModel, SpitzerModel, SpitzerHarmModel, SauterNeoModel, RedlNeoModel diff --git a/test/runtests.jl b/test/runtests.jl index 2036e610c..7368d3b38 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -44,9 +44,11 @@ else include("./runtests_dispersion_amr.jl") include("./runtests_dispersion_polish.jl") include("./runtests_slayer_runner.jl") + include("./runtests_toml_backcompat.jl") include("./runtests_kinetic.jl") include("./runtests_fullruns.jl") 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_equil.jl b/test/runtests_equil.jl index dd1e49ce5..5fb0e22ac 100644 --- a/test/runtests_equil.jl +++ b/test/runtests_equil.jl @@ -10,7 +10,7 @@ eq_filename=joinpath(data_dir, "EQDSK_COCOS_02"), eq_type="efit", jac_type="boozer", - grid_type="ldp", + grid_type="rational_packed", psilow=0.01, psihigh=0.994 ) @@ -25,7 +25,7 @@ eq_filename=joinpath(data_dir, "EQDSK_COCOS_02"), eq_type="efit_arclength", jac_type="boozer", - grid_type="ldp", + grid_type="rational_packed", psilow=0.01, psihigh=0.994 ) @@ -49,7 +49,7 @@ eq_filename=joinpath(data_dir, "EQDSK_COCOS_02"), eq_type="efit_by_inversion", jac_type="boozer", - grid_type="ldp", + grid_type="rational_packed", psilow=0.01, psihigh=0.994 ) @@ -90,7 +90,7 @@ eq_filename=joinpath(data_dir, "INP1_binary"), eq_type="chease_binary", jac_type="boozer", - grid_type="ldp", + grid_type="rational_packed", psilow=0.01, psihigh=0.994, r0exp=6.8, @@ -106,7 +106,7 @@ eq_filename=joinpath(data_dir, "INP1_ascii"), eq_type="chease_ascii", jac_type="boozer", - grid_type="ldp", + grid_type="rational_packed", psilow=0.01, psihigh=0.994, r0exp=6.8, @@ -201,7 +201,7 @@ lar_config = GeneralizedPerturbedEquilibrium.Equilibrium.EquilibriumConfig(; eq_type="lar", jac_type="boozer", - grid_type="ldp", + grid_type="rational_packed", psilow=0.01, psihigh=0.99 ) @@ -367,7 +367,7 @@ mpsi=64, mtheta=128) eq_config = Eq.EquilibriumConfig(; eq_type="sol", eq_filename="unused", - jac_type="pest", grid_type="ldp", + jac_type="pest", grid_type="rational_packed", psilow=1e-4, psihigh=0.99999, mpsi=mpsi, mtheta=mtheta) sol_config = Eq.SolovevConfig(64, 64, 64, e, a, r0, q0, 1.0, 1.0, 1.0) dri = Eq.sol_run(eq_config, sol_config) diff --git a/test/runtests_eulerlagrange.jl b/test/runtests_eulerlagrange.jl index 7adb75203..27e6804c3 100644 --- a/test/runtests_eulerlagrange.jl +++ b/test/runtests_eulerlagrange.jl @@ -432,7 +432,7 @@ end example_dir = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example") inputs = TOML.parsefile(joinpath(example_dir, "gpec.toml")) inputs["ForceFreeStates"]["verbose"] = false - inputs["ForceFreeStates"]["use_parallel"] = false + inputs["ForceFreeStates"]["integrator"] = "serial" inputs["ForceFreeStates"]["write_outputs_to_HDF5"] = false intr = FFS.ForceFreeStatesInternal(; dir_path=example_dir) ctrl = FFS.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) 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..3e11f978a --- /dev/null +++ b/test/runtests_h5_schema.jl @@ -0,0 +1,109 @@ +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 + +# Metadata contract (docs/development/hdf5-conventions.md): every dataset carries +# long_name + units, and rank ≥ 2 datasets carry a dims axis-name attribute. Exempt: +# the Input/ raw snapshot and the debug-only GalerkinIntegration Match/ group. +_metadata_exempt(path) = startswith(path, "Input/") || occursin("/Match/", path) + +function _collect_metadata_violations(h5) + bad = String[] + function walk(node, prefix) + for k in keys(node) + child = node[k] + full = isempty(prefix) ? k : prefix * "/" * k + if child isa HDF5.Group + walk(child, full) + elseif !_metadata_exempt(full) + a = attrs(child) + haskey(a, "long_name") || push!(bad, "$full: missing long_name") + haskey(a, "units") || push!(bad, "$full: missing units") + ndims(child) >= 2 && !haskey(a, "dims") && push!(bad, "$full: missing dims") + end + end + end + walk(h5, "") + return bad +end + +@testset "gpec.h5 schema naming" begin + template_dir = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example") + + mktempdir() do run_dir + for name in readdir(template_dir) + cp(joinpath(template_dir, name), joinpath(run_dir, name)) + end + toml_path = joinpath(run_dir, "gpec.toml") + write(toml_path, replace(read(toml_path, String), "write_outputs_to_HDF5 = false" => "write_outputs_to_HDF5 = true")) + + GeneralizedPerturbedEquilibrium.main([run_dir]) + h5_path = joinpath(run_dir, "gpec.h5") + @test isfile(h5_path) + + h5open(h5_path, "r") do h5 + bad = _collect_bad_groups(h5) + isempty(bad) || @error "non-CamelCase group paths in gpec.h5" bad + @test isempty(bad) + + # Retired/renamed legacy top-level groups must not reappear. + for legacy in ("info", "input", "equil", "splines", "integration", "locstab", + "singular", "matrices", "kinetic", "galerkin", "slayer", "kinetic_forces", + "perturbed_equilibrium", "vacuum", "FreeBoundaryStability", "EdgeScan") + @test !haskey(h5, legacy) + end + + # Inputs live only under Input/; spot-check the rerun-critical paths. + @test haskey(h5, "Input/gpec_toml_raw") + @test haskey(h5, "Info/git_version") + + # Metadata contract: long_name/units everywhere, dims on rank ≥ 2 arrays. + viol = _collect_metadata_violations(h5) + isempty(viol) || @error "metadata contract violations in gpec.h5" viol + @test isempty(viol) + + # File-level attributes. + ra = attrs(h5) + for k in ("schema_version", "Conventions", "title", "date_created") + @test haskey(ra, k) + end + @test ra["schema_version"] == "2.0" + + # Dimension scales: the ψ_N coordinate of the forward integration is a + # scale and is attached to its q profile (netCDF-4 pattern). + fwd = "ForceFreeStates/Solutions/ForwardIntegration" + @test HDF5.API.h5ds_is_scale(h5["$fwd/psi"]) + @test HDF5.API.h5ds_is_attached(h5["$fwd/q"], h5["$fwd/psi"], 0) + end + end +end diff --git a/test/runtests_parallel_integration.jl b/test/runtests_parallel_integration.jl index 666191d17..4e672a63b 100644 --- a/test/runtests_parallel_integration.jl +++ b/test/runtests_parallel_integration.jl @@ -227,14 +227,14 @@ using TOML # The energy eigenvalue et[1] should match to within 2%. # # Bidirectional FM integration (crossing chunks integrated backward) is the - # default for use_parallel=true. It keeps FM propagators well-conditioned for + # default for integrator="stride". It keeps FM propagators well-conditioned for # both small-N (Solovev N=8, tested here) and large-N (DIIID N=26, tested below). ex = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example") - function run_solovev(use_parallel) + function run_solovev(integrator) inputs = TOML.parsefile(joinpath(ex, "gpec.toml")) inputs["ForceFreeStates"]["verbose"] = false - inputs["ForceFreeStates"]["use_parallel"] = use_parallel + inputs["ForceFreeStates"]["integrator"] = integrator intr = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesInternal(; dir_path=ex) ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in inputs["ForceFreeStates"])...) @@ -258,14 +258,14 @@ using TOML return real(vac.et[1]), intr end - et_std, intr_std = run_solovev(false) - et_par, intr_par = run_solovev(true) + et_std, intr_std = run_solovev("serial") + et_par, intr_par = run_solovev("stride") # Energy eigenvalue matches to 2% @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 @@ -279,10 +279,10 @@ using TOML # This is the key regression test for the bidirectional parallel FM fix. ex = joinpath(@__DIR__, "..", "examples", "DIIID-like_ideal_example") - function run_diiid(use_parallel) + function run_diiid(integrator) inputs = TOML.parsefile(joinpath(ex, "gpec.toml")) inputs["ForceFreeStates"]["verbose"] = false - inputs["ForceFreeStates"]["use_parallel"] = use_parallel + inputs["ForceFreeStates"]["integrator"] = integrator inputs["ForceFreeStates"]["write_outputs_to_HDF5"] = false intr = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesInternal(; dir_path=ex) ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; @@ -317,7 +317,7 @@ using TOML return real(vac.et[1]), intr end - et_par, intr_par = run_diiid(true) + et_par, intr_par = run_diiid("stride") # Parallel FM et[1] regression — pinned tightly, NOT bracketed. et[1] is grid- and # equilibrium-sensitive (auto-mpsi gives a spurious value; a wrong grid/Ip shifts it), so @@ -386,12 +386,12 @@ using TOML # physically meaningful. BVP Δ' regression is concentrated on the DIIID-like # fixture below (intrinsically stable, well-conditioned BVP Δ'). - @testset "ξ functions bit-identical between use_parallel modes (populate_dense_xi)" begin - # When `ctrl.use_parallel = true` and `ctrl.populate_dense_xi = true` + @testset "ξ functions bit-identical between integrator modes (populate_dense_xi)" begin + # When `ctrl.integrator = "stride"` and `ctrl.populate_dense_xi = true` # (default), `parallel_eulerlagrange_integration` appends a serial # Euler-Lagrange pass and returns that fresh `odet` instead of the # propagator-BVP one. That dense pass invokes the SAME - # `eulerlagrange_integration` code path the serial `use_parallel = false` + # `eulerlagrange_integration` code path the serial `integrator = "serial"` # benchmark goes through with the SAME `(ctrl, equil, ffit, intr)` # inputs (BVP-only state on `intr` saved/restored across the pass), so # the resulting `psi_store` / `q_store` / `u_store` / `du_store` / @@ -403,10 +403,10 @@ using TOML # Run on both the small-N Solovev case and the large-N DIIID-like case # to catch any (m, IC, ψ)-dependent regression. - function run_and_capture(example_dir, use_parallel; populate_dense_xi=true) + function run_and_capture(example_dir, integrator; populate_dense_xi=true) inputs = TOML.parsefile(joinpath(example_dir, "gpec.toml")) inputs["ForceFreeStates"]["verbose"] = false - inputs["ForceFreeStates"]["use_parallel"] = use_parallel + inputs["ForceFreeStates"]["integrator"] = integrator inputs["ForceFreeStates"]["populate_dense_xi"] = populate_dense_xi inputs["ForceFreeStates"]["write_outputs_to_HDF5"] = false intr = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesInternal(; dir_path=example_dir) @@ -457,15 +457,15 @@ using TOML @testset "Solovev (small N)" begin ex = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example") - odet_std = run_and_capture(ex, false) - odet_par = run_and_capture(ex, true; populate_dense_xi=true) + odet_std = run_and_capture(ex, "serial") + odet_par = run_and_capture(ex, "stride"; populate_dense_xi=true) assert_bit_identical(odet_std, odet_par) end @testset "DIIID-like (large N)" begin ex = joinpath(@__DIR__, "..", "examples", "DIIID-like_ideal_example") - odet_std = run_and_capture(ex, false) - odet_par = run_and_capture(ex, true; populate_dense_xi=true) + odet_std = run_and_capture(ex, "serial") + odet_par = run_and_capture(ex, "stride"; populate_dense_xi=true) assert_bit_identical(odet_std, odet_par) end @@ -477,8 +477,8 @@ using TOML # test above is meaningful — it's NOT trivially passing because # both modes accidentally produce the same sparse data. ex = joinpath(@__DIR__, "test_data", "regression_solovev_ideal_example") - odet_std = run_and_capture(ex, false) - odet_sparse = run_and_capture(ex, true; populate_dense_xi=false) + odet_std = run_and_capture(ex, "serial") + odet_sparse = run_and_capture(ex, "stride"; populate_dense_xi=false) @test odet_sparse.step < odet_std.step @test length(odet_sparse.psi_store) < length(odet_std.psi_store) # The sparse solution is in the Riccati basis, so the derivative stores cannot be @@ -498,7 +498,7 @@ using TOML ex = joinpath(@__DIR__, "..", "examples", "DIIID-like_ideal_example") inputs = TOML.parsefile(joinpath(ex, "gpec.toml")) inputs["ForceFreeStates"]["verbose"] = false - inputs["ForceFreeStates"]["use_parallel"] = true + inputs["ForceFreeStates"]["integrator"] = "stride" inputs["ForceFreeStates"]["write_outputs_to_HDF5"] = false intr = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesInternal(; dir_path=ex) ctrl = GeneralizedPerturbedEquilibrium.ForceFreeStates.ForceFreeStatesControl(; diff --git a/test/runtests_rerun_from_h5.jl b/test/runtests_rerun_from_h5.jl index 12344f5b6..67c816d07 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") @@ -250,7 +250,7 @@ end Equil = GeneralizedPerturbedEquilibrium.Equilibrium config = Equil.EquilibriumConfig(; eq_filename=joinpath(@__DIR__, "test_data", "CHEASE_test_data", "INP1_ascii"), - eq_type="chease_ascii", jac_type="boozer", grid_type="ldp", + eq_type="chease_ascii", jac_type="boozer", grid_type="rational_packed", psilow=0.01, psihigh=0.994, r0exp=6.8, b0exp=7.4 ) src = Equil.read_chease_ascii(config) diff --git a/test/runtests_slayer_inputs.jl b/test/runtests_slayer_inputs.jl index 5e2c5a34f..7fb0925e1 100644 --- a/test/runtests_slayer_inputs.jl +++ b/test/runtests_slayer_inputs.jl @@ -67,13 +67,13 @@ @testset "build_slayer_inputs: returns correct per-surface data" begin sings = [_mk_sing(psi=0.3, q=2.0, q1=1.5, m=2, n=1), _mk_sing(psi=0.6, q=3.0, q1=2.5, m=3, n=1)] - # dr_val=0.0 bypasses the build_slayer_inputs requirement that sing.restype be + # delta_crit_D_R=0.0 bypasses the build_slayer_inputs requirement that sing.restype be # pre-populated by ForceFreeStates.resist_eval_all! — the test sings here are - # minimal stubs without restype, so we supply dr_val explicitly. + # minimal stubs without restype, so we supply delta_crit_D_R explicitly. # compute_omega_star=false makes Q_e/Q_i pass through directly from profiles.omega_e/i # rather than being recomputed from n_e/T_e/T_i gradients — required for the Q_e == # -tauk·omega_e(ψ) identity check below. - sl = build_slayer_inputs(equil, sings, profiles; bt=2.0, dr_val=0.0, + sl = build_slayer_inputs(equil, sings, profiles; bt=2.0, delta_crit_D_R=0.0, compute_omega_star=false) @test length(sl) == 2 @@ -110,43 +110,43 @@ @testset "build_slayer_inputs: chi_perp/chi_tor as scalars and callables" begin sings = [_mk_sing(psi=0.5, q=2.4, q1=1.2, m=2, n=1)] - # Scalar (dr_val=0.0 bypasses the sing.restype requirement; see comment above) + # Scalar (delta_crit_D_R=0.0 bypasses the sing.restype requirement; see comment above) sl_s = build_slayer_inputs(equil, sings, profiles; - bt=2.0, chi_perp=2.0, chi_tor=1.5, dr_val=0.0) + bt=2.0, chi_perp=2.0, chi_tor=1.5, delta_crit_D_R=0.0) # Callable with matching value chi_p(psi) = 2.0 + 0.0*psi chi_t(psi) = 1.5 + 0.0*psi sl_c = build_slayer_inputs(equil, sings, profiles; - bt=2.0, chi_perp=chi_p, chi_tor=chi_t, dr_val=0.0) + bt=2.0, chi_perp=chi_p, chi_tor=chi_t, delta_crit_D_R=0.0) @test sl_s[1].P_perp ≈ sl_c[1].P_perp @test sl_s[1].P_tor ≈ sl_c[1].P_tor # Callable with ψ-dependence changes the result chi_p_var(psi) = 1.0 + 10.0 * psi # χ⊥(0.5) = 6.0 > 2.0 sl_var = build_slayer_inputs(equil, sings, profiles; - bt=2.0, chi_perp=chi_p_var, chi_tor=1.5, dr_val=0.0) + bt=2.0, chi_perp=chi_p_var, chi_tor=1.5, delta_crit_D_R=0.0) # P_perp = τ_r · χ⊥ / r² grows with χ⊥, so the varying-χ case at # ψ=0.5 (χ⊥=6) gives a *larger* P_perp than the scalar χ⊥=2. @test sl_var[1].P_perp > sl_s[1].P_perp @test sl_var[1].P_perp ≈ sl_s[1].P_perp * 6.0 / 2.0 rtol = 1e-10 end - @testset "build_slayer_inputs: dc_type propagates and dr_val activates offset" begin + @testset "build_slayer_inputs: delta_crit_type propagates and delta_crit_D_R activates offset" begin sings = [_mk_sing(psi=0.5, q=2.4, q1=1.2, m=2, n=1)] - # dc_type=:none and dr_val=0.0 → dc_tmp = 0 regardless of dr_val + # delta_crit_type=:none and delta_crit_D_R=0.0 → dc_tmp = 0 regardless of delta_crit_D_R sl_none = build_slayer_inputs(equil, sings, profiles; - bt=2.0, dc_type=:none, dr_val=0.0) + bt=2.0, delta_crit_type=:none, delta_crit_D_R=0.0) @test sl_none[1].dc_tmp == 0.0 - # dc_type=:rfitzp with dr_val = 0 still gives zero + # delta_crit_type=:fitzpatrick with delta_crit_D_R = 0 still gives zero sl_rf0 = build_slayer_inputs(equil, sings, profiles; - bt=2.0, dc_type=:rfitzp, dr_val=0.0) + bt=2.0, delta_crit_type=:fitzpatrick, delta_crit_D_R=0.0) @test sl_rf0[1].dc_tmp == 0.0 - # dc_type=:rfitzp with dr_val > 0 → nonzero negative offset + # delta_crit_type=:fitzpatrick with delta_crit_D_R > 0 → nonzero negative offset sl_rf = build_slayer_inputs(equil, sings, profiles; - bt=2.0, dc_type=:rfitzp, dr_val=0.01) + bt=2.0, delta_crit_type=:fitzpatrick, delta_crit_D_R=0.01) @test sl_rf[1].dc_tmp < 0 @test isfinite(sl_rf[1].dc_tmp) end diff --git a/test/runtests_slayer_params.jl b/test/runtests_slayer_params.jl index 330ba7297..d1715bd38 100644 --- a/test/runtests_slayer_params.jl +++ b/test/runtests_slayer_params.jl @@ -6,7 +6,7 @@ # Reference inputs: a simple deuterium plasma case suitable for # hand-checking the SLAYER params formulas. - function _ref_kwargs(; dr_val=0.0, dc_type=:none) + function _ref_kwargs(; delta_crit_D_R=0.0, delta_crit_type=:none) return ( n_e=5.0e19, t_e=1000.0, t_i=1000.0, omega=0.0, omega_e=1.0e4, omega_i=5.0e3, @@ -14,7 +14,7 @@ rs=0.5, R0=1.7, mu_i=2.0, zeff=1.0, chi_perp=1.0, chi_tor=1.0, m=2, n=1, - dr_val=dr_val, dgeo_val=0.5, dc_type=dc_type, + delta_crit_D_R=delta_crit_D_R, delta_crit_geo_factor=0.5, delta_crit_type=delta_crit_type, ising=3 ) end @@ -31,8 +31,8 @@ @test p.R0 == 1.7 @test p.bt == 2.0 @test p.sval_r == 1.0 - @test p.dc_tmp == 0.0 # dr_val == 0 ⇒ no offset - @test p.dc_type === :none + @test p.dc_tmp == 0.0 # delta_crit_D_R == 0 ⇒ no offset + @test p.delta_crit_type === :none # Trivially exact ratios @test p.tau ≈ 1.0 @@ -91,34 +91,34 @@ @test p.delta_n ≈ p.lu^(1 / 3) / p.rs rtol = 1e-12 end - @testset "Test 1b: dc_tmp formulas activate when dr_val ≠ 0" begin - # All four dc_type branches must produce finite, non-NaN values + @testset "Test 1b: dc_tmp formulas activate when delta_crit_D_R ≠ 0" begin + # All four delta_crit_type branches must produce finite, non-NaN values # and respect the signs/structure of the formulas in # the SLAYER params dc_tmp formulas. - p_none = slayer_parameters(; _ref_kwargs(; dr_val=0.01, dc_type=:none)...) - @test p_none.dc_tmp == 0.0 # :none ignores dr_val + p_none = slayer_parameters(; _ref_kwargs(; delta_crit_D_R=0.01, delta_crit_type=:none)...) + @test p_none.dc_tmp == 0.0 # :none ignores delta_crit_D_R - p_lar = slayer_parameters(; _ref_kwargs(; dr_val=0.01, dc_type=:lar)...) - p_rf = slayer_parameters(; _ref_kwargs(; dr_val=0.01, dc_type=:rfitzp)...) - p_tor = slayer_parameters(; _ref_kwargs(; dr_val=0.01, dc_type=:toroidal)...) + p_lar = slayer_parameters(; _ref_kwargs(; delta_crit_D_R=0.01, delta_crit_type=:lar)...) + p_rf = slayer_parameters(; _ref_kwargs(; delta_crit_D_R=0.01, delta_crit_type=:fitzpatrick)...) + p_tor = slayer_parameters(; _ref_kwargs(; delta_crit_D_R=0.01, delta_crit_type=:toroidal)...) @test isfinite(p_lar.dc_tmp) @test isfinite(p_rf.dc_tmp) @test isfinite(p_tor.dc_tmp) - # dr_val > 0 with the (-dr_val) prefactor ⇒ negative dc_tmp for - # :lar, :rfitzp, :toroidal branches. + # delta_crit_D_R > 0 with the (-delta_crit_D_R) prefactor ⇒ negative dc_tmp for + # :lar, :fitzpatrick, :toroidal branches. @test p_lar.dc_tmp < 0 @test p_rf.dc_tmp < 0 @test p_tor.dc_tmp < 0 - # Sign flips with sign of dr_val + # Sign flips with sign of delta_crit_D_R p_lar_neg = slayer_parameters(; - _ref_kwargs(; dr_val=-0.01, dc_type=:lar)...) + _ref_kwargs(; delta_crit_D_R=-0.01, delta_crit_type=:lar)...) @test sign(p_lar_neg.dc_tmp) == -sign(p_lar.dc_tmp) - # Reject unknown dc_type + # Reject unknown delta_crit_type @test_throws ArgumentError slayer_parameters(; - _ref_kwargs(; dr_val=0.01, dc_type=:bogus)...) + _ref_kwargs(; delta_crit_D_R=0.01, delta_crit_type=:bogus)...) end @testset "Test 1c: SLAYERParameters direct kwarg construction" begin @@ -134,8 +134,8 @@ ) @test p.tau == 1.0 @test p.dc_tmp == 0.0 - @test p.dc_type === :none - @test p.dr_val == 0.0 + @test p.delta_crit_type === :none + @test p.delta_crit_D_R == 0.0 @test p.ising == 0 end diff --git a/test/runtests_slayer_runner.jl b/test/runtests_slayer_runner.jl index c3ab86806..9a1ba026d 100644 --- a/test/runtests_slayer_runner.jl +++ b/test/runtests_slayer_runner.jl @@ -37,7 +37,7 @@ @test_throws ArgumentError Runner.validate( SLAYERControl(; coupling_mode=:bogus)) @test_throws ArgumentError Runner.validate( - SLAYERControl(; dc_type=:bogus)) + SLAYERControl(; delta_crit_type=:bogus)) @test_throws ArgumentError Runner.validate( SLAYERControl(; msing_max=0)) @test_throws ArgumentError Runner.validate( @@ -50,11 +50,11 @@ "inner_model" => "slayer_fitzpatrick", "scan_mode" => "brute_force", "coupling_mode" => "coupled", - "dc_type" => "rfitzp", + "delta_crit_type" => "fitzpatrick", "msing_max" => 2, "bt" => 1.8, "mu_i" => 2.0, - "dr_val" => 0.01, + "delta_crit_D_R" => 0.01, "scan_grid" => Dict{String,Any}( "Q_re_range" => [-5.0, 5.0], "Q_im_range" => [-1.0, 3.0], @@ -73,10 +73,10 @@ @test c.inner_model === :slayer_fitzpatrick @test c.scan_mode === :brute_force @test c.coupling_mode === :coupled - @test c.dc_type === :rfitzp + @test c.delta_crit_type === :fitzpatrick @test c.msing_max == 2 @test c.bt === 1.8 - @test c.dr_val == 0.01 + @test c.delta_crit_D_R == 0.01 @test c.Q_re_range == (-5.0, 5.0) @test c.Q_im_range == (-1.0, 3.0) @test c.nre == 50 @@ -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/runtests_toml_backcompat.jl b/test/runtests_toml_backcompat.jl new file mode 100644 index 000000000..44699c35b --- /dev/null +++ b/test/runtests_toml_backcompat.jl @@ -0,0 +1,145 @@ +# Back-compat tests for deprecated TOML spellings: decks written with the old key/value +# names must load with a deprecation warning and produce control structs identical to the +# new spellings. +using Test +using TOML +using Logging +using GeneralizedPerturbedEquilibrium +using GeneralizedPerturbedEquilibrium.ForceFreeStates: ForceFreeStatesControl +using GeneralizedPerturbedEquilibrium.Equilibrium: EquilibriumConfig +using GeneralizedPerturbedEquilibrium.Runner: slayer_control_from_toml + +const GPE = GeneralizedPerturbedEquilibrium + +# Field-by-field struct equality (generic == is identity for mutable structs; isequal so NaN sentinels compare equal) +fields_equal(a::T, b::T) where {T} = all(isequal(getfield(a, f), getfield(b, f)) for f in fieldnames(T)) + +# Run f while discarding its log output (deprecation warnings are asserted separately) +quietly(f) = with_logger(f, NullLogger()) + +@testset "TOML deprecated-spelling back-compat" begin + @testset "_rename_keys! warns, remaps, and lets an explicit new key win" begin + t = Dict{String,Any}("newq0" => 1.5, "mpsi" => 64) + @test_logs (:warn, r"`newq0` in \[Equilibrium\] was renamed to `q0_override`") GPE._rename_keys!(t, GPE._RENAMED_EQUIL_KEYS, "Equilibrium") + @test !haskey(t, "newq0") + @test t["q0_override"] == 1.5 + @test t["mpsi"] == 64 + + t = Dict{String,Any}("newq0" => 1.5, "q0_override" => 2.5) + @test_logs (:warn, r"renamed to `q0_override`") GPE._rename_keys!(t, GPE._RENAMED_EQUIL_KEYS, "Equilibrium") + @test t["q0_override"] == 2.5 + end + + @testset "_rename_value! warns and remaps deprecated enum values" begin + t = Dict{String,Any}("f0type" => "jkp") + @test_logs (:warn, r"`f0type = \"jkp\"` in \[KineticForces\] is deprecated") GPE._rename_value!(t, "f0type", "jkp", "park", "KineticForces") + @test t["f0type"] == "park" + # Non-matching values pass through silently + t = Dict{String,Any}("f0type" => "park") + @test_logs GPE._rename_value!(t, "f0type", "jkp", "park", "KineticForces") + @test t["f0type"] == "park" + end + + @testset "integrator remap mirrors the old use_parallel/use_riccati dispatch" begin + # Old dispatch order: use_parallel (default true) wins, then use_riccati, else serial. + for (tbl, expect) in [ + Dict{String,Any}("use_parallel" => true) => "stride", + Dict{String,Any}("use_parallel" => false) => "serial", + Dict{String,Any}("use_parallel" => false, "use_riccati" => true) => "riccati", + Dict{String,Any}("use_riccati" => true) => "stride", + Dict{String,Any}("use_riccati" => false) => "stride" + ] + @test_logs (:warn, r"replaced by the `integrator` enum") GPE._remap_integrator_keys!(tbl) + @test tbl["integrator"] == expect + @test !haskey(tbl, "use_parallel") && !haskey(tbl, "use_riccati") + end + # An explicit integrator wins over the old flags + t = Dict{String,Any}("use_parallel" => false, "integrator" => "riccati") + @test_logs (:warn, r"ignored because `integrator` is also set") GPE._remap_integrator_keys!(t) + @test t["integrator"] == "riccati" + # No old keys: no warning, table untouched + t = Dict{String,Any}("integrator" => "stride") + @test_logs GPE._remap_integrator_keys!(t) + @test t["integrator"] == "stride" + end + + @testset "old FFS keys build an identical ForceFreeStatesControl" begin + old = Dict{String,Any}("use_parallel" => false, "parallel_threads" => 3, + "psiedge" => 0.97, "nstep" => 100, "diagnose_ca" => true, + "nn_low" => 1, "nn_high" => 1) + new = Dict{String,Any}("integrator" => "serial", "integrator_threads" => 3, + "dW_edge_scan_start" => 0.97, "nn_low" => 1, "nn_high" => 1) + quietly() do + GPE._rename_keys!(old, GPE._RENAMED_FFS_KEYS, "ForceFreeStates") + GPE._remap_integrator_keys!(old) + GPE._drop_deprecated_keys!(old, GPE._DEPRECATED_FFS_KEYS, "ForceFreeStates") + end + ctrl_old = ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in old)...) + ctrl_new = ForceFreeStatesControl(; (Symbol(k) => v for (k, v) in new)...) + @test fields_equal(ctrl_old, ctrl_new) + end + + @testset "old Equilibrium keys/values build an identical EquilibriumConfig" begin + old = Dict{String,Any}("eq_type" => "efit", "eq_filename" => "g0.eqdsk", + "newq0" => 2, "use_galgrid" => false, "grid_type" => "ldp") + new = Dict{String,Any}("eq_type" => "efit", "eq_filename" => "g0.eqdsk", + "q0_override" => 2.0, "use_galerkin_grid" => false, "grid_type" => "rational_packed") + cfg_old = quietly() do + GPE._rename_keys!(old, GPE._RENAMED_EQUIL_KEYS, "Equilibrium") + EquilibriumConfig(old, ".") + end + cfg_new = quietly() do + EquilibriumConfig(new, ".") + end + @test cfg_old.q0_override == 2.0 + @test cfg_old.grid_type == "rational_packed" + @test fields_equal(cfg_old, cfg_new) + end + + @testset "old SLAYER keys/values build an identical SLAYERControl" begin + old = Dict{String,Any}("enabled" => true, "dc_type" => "rfitzp", + "dr_val" => 0.01, "dgeo_val" => 0.2) + new = Dict{String,Any}("enabled" => true, "delta_crit_type" => "fitzpatrick", + "delta_crit_D_R" => 0.01, "delta_crit_geo_factor" => 0.2) + ctrl_old = quietly() do + slayer_control_from_toml(old) + end + ctrl_new = slayer_control_from_toml(new) + @test ctrl_old.delta_crit_type === :fitzpatrick + @test fields_equal(ctrl_old, ctrl_new) + # The rename warnings actually fire + @test_logs (:warn, r"`dc_type` in \[SLAYER\] was renamed") match_mode = :any slayer_control_from_toml(Dict{String,Any}("dc_type" => "lar")) + end + + @testset "old coil_set keys build an identical CoilSetConfig" begin + old = Dict{String,Any}("name" => "c79", "xnom" => [1.0], "ynom" => [2.0], "znom" => [3.0]) + new = Dict{String,Any}("name" => "c79", "rotation_center_x" => [1.0], + "rotation_center_y" => [2.0], "rotation_center_z" => [3.0]) + cfg_old = quietly() do + GPE.ForcingTerms._parse_coil_set_config(old) + end + cfg_new = GPE.ForcingTerms._parse_coil_set_config(new) + @test cfg_old.rotation_center_x == [1.0] + @test fields_equal(cfg_old, cfg_new) + @test_logs (:warn, r"`xnom` in \[\[ForcingTerms.coil_set\]\] was renamed") match_mode = :any GPE.ForcingTerms._parse_coil_set_config(Dict{String,Any}("xnom" => [1.0])) + end + + @testset "build_inputs_from_toml applies the Equilibrium renames on a real deck" begin + mktempdir() do dir + write(joinpath(dir, "gpec.toml"), + """ + [Equilibrium] + eq_type = "efit" + eq_filename = "g_unused.eqdsk" + newq0 = 0 + use_galgrid = true + """) + inputs, eq_config, _ = quietly() do + GPE.build_inputs_from_toml(dir) + end + @test !haskey(inputs["Equilibrium"], "newq0") + @test eq_config.q0_override == 0.0 + @test eq_config.use_galerkin_grid === true + end + end +end diff --git a/test/runtests_vacuum.jl b/test/runtests_vacuum.jl index 630c60436..578dd6e1e 100644 --- a/test/runtests_vacuum.jl +++ b/test/runtests_vacuum.jl @@ -465,7 +465,7 @@ @testset "extract_plasma_surface_at_psi" begin # Self-contained analytic Solovev equilibrium (same recipe as runtests_equil.jl). - eq_config = Equilibrium.EquilibriumConfig(; eq_type="sol", eq_filename="unused", jac_type="pest", grid_type="ldp", psilow=1e-4, psihigh=0.99999, mpsi=64, mtheta=128) + eq_config = Equilibrium.EquilibriumConfig(; eq_type="sol", eq_filename="unused", jac_type="pest", grid_type="rational_packed", psilow=1e-4, psihigh=0.99999, mpsi=64, mtheta=128) sol_config = Equilibrium.SolovevConfig(64, 64, 64, 1.6, 0.33, 1.0, 1.9, 1.0, 1.0, 1.0) pe = Equilibrium.equilibrium_solver(Equilibrium.sol_run(eq_config, sol_config)) diff --git a/test/test_data/regression_solovev_ideal_example/gpec.toml b/test/test_data/regression_solovev_ideal_example/gpec.toml index e6a97d2ec..28e5fb9fe 100644 --- a/test/test_data/regression_solovev_ideal_example/gpec.toml +++ b/test/test_data/regression_solovev_ideal_example/gpec.toml @@ -5,12 +5,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 16 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -28,7 +28,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -46,8 +46,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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 dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) diff --git a/test/test_data/regression_solovev_ideal_example_multi_n/gpec.toml b/test/test_data/regression_solovev_ideal_example_multi_n/gpec.toml index 0f0bc5c47..3e939e62a 100644 --- a/test/test_data/regression_solovev_ideal_example_multi_n/gpec.toml +++ b/test/test_data/regression_solovev_ideal_example_multi_n/gpec.toml @@ -5,12 +5,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 16 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -28,7 +28,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -46,8 +46,8 @@ 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 (multi-n Δ' matrix has open issues — sing_lim! warns and skips — but ξ and energies are valid) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (multi-n Δ' matrix has open issues — sing_lim! warns and skips — but ξ and energies are valid) +integrator_threads = 2 # Stride 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 multi-n — dmlim truncation is ambiguous when n varies (sing_lim! skips anyway) dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) diff --git a/test/test_data/regression_solovev_kinetic_calculated/gpec.toml b/test/test_data/regression_solovev_kinetic_calculated/gpec.toml index 5b87267a9..f8df5d802 100644 --- a/test/test_data/regression_solovev_kinetic_calculated/gpec.toml +++ b/test/test_data/regression_solovev_kinetic_calculated/gpec.toml @@ -5,12 +5,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 16 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -29,7 +29,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) diff --git a/test/test_data/regression_solovev_kinetic_example/gpec.toml b/test/test_data/regression_solovev_kinetic_example/gpec.toml index 4d8f03108..9f7235e86 100644 --- a/test/test_data/regression_solovev_kinetic_example/gpec.toml +++ b/test/test_data/regression_solovev_kinetic_example/gpec.toml @@ -5,12 +5,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 16 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -28,7 +28,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -46,8 +46,8 @@ 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) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (unlocks SingularSurfaces/delta_prime_matrix) +integrator_threads = 2 # Stride 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 dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) diff --git a/test/test_data/regression_solovev_kinetic_multi_n/gpec.toml b/test/test_data/regression_solovev_kinetic_multi_n/gpec.toml index b11caab8b..c80cc8aa9 100644 --- a/test/test_data/regression_solovev_kinetic_multi_n/gpec.toml +++ b/test/test_data/regression_solovev_kinetic_multi_n/gpec.toml @@ -5,12 +5,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized poloidal flux psihigh = 0.9995 # Upper limit of normalized poloidal flux mpsi = 16 # Number of radial grid intervals (0 = two-pass auto grid from psi_accuracy) mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -28,7 +28,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW(ψ) diagnostic scan band [psiedge, psilim]; set ≥ psilim to disable +dW_edge_scan_start = 0.99 # Edge dW(ψ) diagnostic scan band [dW_edge_scan_start, psilim]; set ≥ psilim to disable qlow = 1.02 # Integration initiated at q determined by min(q0, qlow) qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh) sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow) @@ -46,8 +46,8 @@ 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 (multi-n Δ' matrix has open issues — sing_lim! warns and skips — but ξ and energies are valid) -parallel_threads = 2 # BVP thread cap (1 = serial/bit-deterministic; 2 ≈ +20% speedup; ≥3 saturates) +integrator = "stride" # Integration algorithm: "stride" FM-propagator BVP (multi-n Δ' matrix has open issues — sing_lim! warns and skips — but ξ and energies are valid) +integrator_threads = 2 # Stride 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 multi-n — dmlim truncation is ambiguous when n varies (sing_lim! skips anyway) dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim)/n (used when set_psilim_via_dmlim = true) diff --git a/test/test_data/regression_solovev_kinetic_nuzero/gpec.toml b/test/test_data/regression_solovev_kinetic_nuzero/gpec.toml index ca6eeb003..825e561b0 100644 --- a/test/test_data/regression_solovev_kinetic_nuzero/gpec.toml +++ b/test/test_data/regression_solovev_kinetic_nuzero/gpec.toml @@ -6,12 +6,12 @@ [Equilibrium] eq_type = "sol" # Type of the input 2D equilibrium file jac_type = "pest" # Coordinate system (hamada, pest, boozer, equal_arc, park, custom) -grid_type = "ldp" # Radial grid packing type +grid_type = "rational_packed" # Radial grid packing type psilow = 1e-4 # Lower limit of normalized flux coordinate psihigh = 0.9995 # Upper limit of normalized flux coordinate mpsi = 16 # Number of radial grid points mtheta = 256 # Number of poloidal grid points -newq0 = 0 # Override for on-axis safety factor (0 = use input value) +q0_override = 0.0 # Override for on-axis safety factor (0 = use input value) etol = 1e-7 # Error tolerance for equilibrium solver force_termination = false # Terminate after equilibrium setup (skip stability calculations) @@ -29,7 +29,7 @@ equal_arc_wall = true # Equal arc length distribution of nodes local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes -psiedge = 0.99 # Edge dW scan band: dW(ψ) computed for ψ ∈ [psiedge, psilim], integration truncated at peak +dW_edge_scan_start = 0.99 # Edge dW scan band: dW(ψ) computed for ψ ∈ [dW_edge_scan_start, psilim], integration truncated at peak qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)... qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)... sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow)