From 29a7577b8136dec7ee26060a1d0f746d4b5a476a Mon Sep 17 00:00:00 2001 From: Matthew Pharr Date: Tue, 20 Oct 2026 13:26:38 +0200 Subject: [PATCH 1/2] Tearing - BUGFIX! - Reject GGJ tearing models at parse time and stop swallowing stage errors A deck with inner_model = "ggj_*" passed validation, then solve(TearingProblem, GGJModel) errored and run_slayer_stage's try/catch logged it, so the run finished with no Tearing/ group. validate now rejects the ggj_* models, since GGJ tearing growth rates are not implemented in v2.0.0. run_slayer_stage no longer catches errors, so a failing tearing stage stops the run after every earlier stage has written its output. Co-Authored-By: Claude Opus 5.5 --- examples/DIIID-like_SLAYER_example/gpec.toml | 2 +- src/GeneralizedPerturbedEquilibrium.jl | 51 ++++++++------------ src/Tearing/Runner/Control.jl | 9 ++-- src/Tearing/Runner/TearingProblem.jl | 2 +- src/Tearing/Runner/run_slayer.jl | 6 --- test/runtests_slayer_runner.jl | 7 ++- test/runtests_solve_api.jl | 2 + 7 files changed, 35 insertions(+), 44 deletions(-) diff --git a/examples/DIIID-like_SLAYER_example/gpec.toml b/examples/DIIID-like_SLAYER_example/gpec.toml index 61ce57700f..08df13c11b 100644 --- a/examples/DIIID-like_SLAYER_example/gpec.toml +++ b/examples/DIIID-like_SLAYER_example/gpec.toml @@ -67,7 +67,7 @@ rational_offset_fraction = 0.2 # Truncate integration at (last_rational_q + rati # invalid, no Re/Im contour crossing polishes to a true zero, and the validity # gate correctly reports them as no_root rather than a spurious growth rate. enabled = true # Run the SLAYER tearing-mode analysis -inner_model = "slayer_fitzpatrick" # Inner-layer Δ(Q) model: "slayer_fitzpatrick", "ggj_ray", "ggj_shooting", or "ggj_galerkin" +inner_model = "slayer_fitzpatrick" # Inner-layer Δ(Q) model: only "slayer_fitzpatrick" (GGJ tearing growth rates are not implemented) scan_mode = "amr" # Q-plane scan strategy: "amr" (adaptive refinement) or "brute_force" coupling_mode = "uncoupled" # "uncoupled" (per-surface) or "coupled" (multi-surface determinant) D_c_type = "none" # Critical-Δ offset selector: "none", "lar", "fitzpatrick", or "toroidal" diff --git a/src/GeneralizedPerturbedEquilibrium.jl b/src/GeneralizedPerturbedEquilibrium.jl index c048367661..251e6da84c 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -292,7 +292,7 @@ function main_from_inputs( # Early exit if user only requested force-free states (SLAYER still runs). if ctrl.force_termination slayer_result = run_slayer_stage(ffs_result, inputs, nothing; runtimes=runtimes) - # A non-nothing result means the Tearing/ group was appended inside the guarded stage. + # A non-nothing result means the Tearing/ group was appended. slayer_result === nothing || (written_h5 = ctrl.HDF5_filename) total_dt = time() - total_start push!(runtimes, "total" => total_dt) @@ -349,7 +349,7 @@ function main_from_inputs( end slayer_result = run_slayer_stage(ffs_result, inputs, pe_file; runtimes=runtimes) - # A non-nothing result means the Tearing/ group was appended inside the guarded stage. + # A non-nothing result means the Tearing/ group was appended. slayer_result === nothing || (written_h5 = pe_file) # ---------------------------------------------------------------- @@ -1464,40 +1464,29 @@ Run the SLAYER tearing-mode analysis off the force-free-states `result`, appendi `pe_file` (or the force-free-states output when PE did not run). Needs only the result, so it runs in both the `force_termination = true` path and the full pipeline. A `runtimes` collector, when given, receives the solver's wall-clock seconds as a `"tearing" => dt` pair for the -`Info/Runtimes` record. +`Info/Runtimes` record. Errors propagate; every earlier stage has already written its output. """ function run_slayer_stage(result::ForceFreeStatesResult, inputs::Dict{String,Any}, pe_file::Union{String,Nothing}; runtimes::Union{Nothing,Vector{Pair{String,Float64}}}=nothing) ("SLAYER" in keys(inputs)) || return nothing - # SLAYER is a post-processing diagnostic. A failure here must not - # discard the equilibrium / stability / PE results already computed, - # so the whole stage is guarded: on error we log loudly and return - # `nothing` for the `slayer` field rather than propagating. - try - slayer_ctrl = Runner.slayer_control_from_toml(inputs["SLAYER"]) - slayer_ctrl.enabled || return nothing - @info "\n SLAYER\n$_SECTION" - slayer_start = time() - model = Runner._build_inner_model(slayer_ctrl.inner_model) - slayer_result = solve(Runner.TearingProblem(result, slayer_ctrl), model) - slayer_dt = time() - slayer_start - runtimes === nothing || push!(runtimes, "tearing" => slayer_dt) - @info "SLAYER completed in $(@sprintf("%.3f", slayer_dt)) s" - h5_filename = pe_file === nothing ? result.control.HDF5_filename : pe_file - h5_path = joinpath(result.dir_path, h5_filename) - # 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, slayer_result) - end - @info "SLAYER results written to $h5_filename" - return slayer_result - catch err - @error "SLAYER stage failed; continuing without tearing results. " * - "Equilibrium / stability / PE outputs are unaffected." exception = - (err, catch_backtrace()) - return nothing + slayer_ctrl = Runner.slayer_control_from_toml(inputs["SLAYER"]) + slayer_ctrl.enabled || return nothing + @info "\n SLAYER\n$_SECTION" + slayer_start = time() + model = Runner._build_inner_model(slayer_ctrl.inner_model) + slayer_result = solve(Runner.TearingProblem(result, slayer_ctrl), model) + slayer_dt = time() - slayer_start + runtimes === nothing || push!(runtimes, "tearing" => slayer_dt) + @info "SLAYER completed in $(@sprintf("%.3f", slayer_dt)) s" + h5_filename = pe_file === nothing ? result.control.HDF5_filename : pe_file + h5_path = joinpath(result.dir_path, h5_filename) + # 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, slayer_result) end + @info "SLAYER results written to $h5_filename" + return slayer_result end """ diff --git a/src/Tearing/Runner/Control.jl b/src/Tearing/Runner/Control.jl index a82a2ce181..10d8c8c7db 100644 --- a/src/Tearing/Runner/Control.jl +++ b/src/Tearing/Runner/Control.jl @@ -16,8 +16,8 @@ constructor. # Core toggles - `enabled` -- run the analysis at all (default `false`) - - `inner_model` -- `:slayer_fitzpatrick` (default), `:ggj_ray`, - `:ggj_shooting`, or `:ggj_galerkin` + - `inner_model` -- `:slayer_fitzpatrick` (default and only option; the + `:ggj_*` models are rejected because GGJ tearing growth rates are not implemented) - `scan_mode` -- `:amr` (default) or `:brute_force` - `coupling_mode` -- `:uncoupled` (default, per-surface) or `:coupled` (multi-surface determinant) @@ -164,7 +164,7 @@ there is one consistent interface for resistive and kinetic profiles. store_scan::Bool = false end -const _VALID_INNER_MODELS = (:slayer_fitzpatrick, :ggj_ray, :ggj_shooting, :ggj_galerkin) +const _VALID_INNER_MODELS = (:slayer_fitzpatrick,) const _VALID_SCAN_MODES = (:amr, :brute_force) const _VALID_COUPLING_MODES = (:uncoupled, :coupled) const _VALID_D_C_TYPES = (:none, :lar, :fitzpatrick, :toroidal) @@ -172,6 +172,9 @@ const _VALID_RESISTIVITY_MODELS = (:sauter, :redl, :spitzer, :spitzer_harm) const _VALID_LNLAMBDA_FORMS = (:nrl, :sauter, :wesson) function validate(ctrl::SLAYERControl) + startswith(String(ctrl.inner_model), "ggj_") && + throw(ArgumentError("SLAYERControl: inner_model=$(ctrl.inner_model) is unsupported in v2.0.0 because GGJ tearing " * + "growth rates are not implemented; use inner_model=slayer_fitzpatrick")) ctrl.inner_model in _VALID_INNER_MODELS || throw(ArgumentError("SLAYERControl: inner_model=$(ctrl.inner_model) " * "not in $(_VALID_INNER_MODELS)")) diff --git a/src/Tearing/Runner/TearingProblem.jl b/src/Tearing/Runner/TearingProblem.jl index 0ba447eb4b..0c73615af1 100644 --- a/src/Tearing/Runner/TearingProblem.jl +++ b/src/Tearing/Runner/TearingProblem.jl @@ -72,7 +72,7 @@ function CommonSolve.solve(prob::TearingProblem, model::InnerLayer.InnerLayerMod equil = ffs.equil surfaces = ffs.surfaces - model isa SLAYERModel || error("TearingProblem: GGJ tearing growth rates are not implemented yet; use SLAYERModel()") + model isa SLAYERModel || error("TearingProblem: GGJ tearing growth rates are unsupported in v2.0.0; use SLAYERModel()") validate(control) control.enabled || return empty_slayer_result(control) isempty(surfaces) && return empty_slayer_result(control) diff --git a/src/Tearing/Runner/run_slayer.jl b/src/Tearing/Runner/run_slayer.jl index 93f67163cc..7055d8f723 100644 --- a/src/Tearing/Runner/run_slayer.jl +++ b/src/Tearing/Runner/run_slayer.jl @@ -73,12 +73,6 @@ end function _build_inner_model(name::Symbol) if name === :slayer_fitzpatrick return SLAYERModel(; variant=:fitzpatrick) - elseif name === :ggj_ray - return GGJModel(; solver=:ray) - elseif name === :ggj_shooting - return GGJModel(; solver=:shooting) - elseif name === :ggj_galerkin - return GGJModel(; solver=:galerkin) end throw(ArgumentError("_build_inner_model: unknown model $name")) end diff --git a/test/runtests_slayer_runner.jl b/test/runtests_slayer_runner.jl index 530b2d0eaa..8fdfa7dfb4 100644 --- a/test/runtests_slayer_runner.jl +++ b/test/runtests_slayer_runner.jl @@ -109,8 +109,11 @@ @test r_off.enabled == false end - @testset "GGJ tearing errors until its growth-rate extraction exists" begin - @test SLAYERControl(; inner_model=:ggj_ray).inner_model === :ggj_ray + @testset "GGJ tearing is rejected until its growth-rate extraction exists" begin + for name in ("ggj_ray", "ggj_shooting", "ggj_galerkin") + @test_throws ArgumentError Runner.validate(SLAYERControl(; inner_model=Symbol(name))) + @test_throws ArgumentError Runner.slayer_control_from_toml(Dict{String,Any}("enabled" => true, "inner_model" => name)) + end no_surfaces = (equil=nothing, surfaces=GeneralizedPerturbedEquilibrium.ForceFreeStates.SingType[], delta_prime=nothing, dir_path=".") @test_throws ErrorException solve(TearingProblem(no_surfaces, SLAYERControl(; enabled=true, profile_file="unused.h5")), GGJModel()) diff --git a/test/runtests_solve_api.jl b/test/runtests_solve_api.jl index 3de6f581f2..2a238b3a38 100644 --- a/test/runtests_solve_api.jl +++ b/test/runtests_solve_api.jl @@ -295,6 +295,8 @@ using TOML @test_throws ErrorException MatchProblem(gal; eta=fill(1e-7, length(gal.surfaces) + 1), rho=1e-7, rotation=1.0) # The tearing model is the solve argument, never a TearingProblem keyword. @test_throws ErrorException TearingProblem(gal; inner_model=:ggj_ray) + # The deck's tearing stage propagates errors rather than finishing without a Tearing/ group. + @test_throws ArgumentError GPEC.run_slayer_stage(gal, Dict{String,Any}("SLAYER" => Dict{String,Any}("enabled" => true, "inner_model" => "ggj_ray")), nothing) # The ideal reference match keeps the ideal closure and replaces the solution with # the bare coil columns in the identity-at-edge basis. matched = solve(prob, GGJModel()) From 9fadd79c5883da721a2324088fcb8facb94df5cd Mon Sep 17 00:00:00 2001 From: logan-nc Date: Thu, 22 Oct 2026 03:49:28 -0400 Subject: [PATCH 2/2] Tearing - MINOR - Drop the release number from the GGJ tearing rejection messages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF --- src/Tearing/Runner/Control.jl | 2 +- src/Tearing/Runner/TearingProblem.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tearing/Runner/Control.jl b/src/Tearing/Runner/Control.jl index 10d8c8c7db..94aed97f4c 100644 --- a/src/Tearing/Runner/Control.jl +++ b/src/Tearing/Runner/Control.jl @@ -173,7 +173,7 @@ const _VALID_LNLAMBDA_FORMS = (:nrl, :sauter, :wesson) function validate(ctrl::SLAYERControl) startswith(String(ctrl.inner_model), "ggj_") && - throw(ArgumentError("SLAYERControl: inner_model=$(ctrl.inner_model) is unsupported in v2.0.0 because GGJ tearing " * + throw(ArgumentError("SLAYERControl: inner_model=$(ctrl.inner_model) is not currently supported because GGJ tearing " * "growth rates are not implemented; use inner_model=slayer_fitzpatrick")) ctrl.inner_model in _VALID_INNER_MODELS || throw(ArgumentError("SLAYERControl: inner_model=$(ctrl.inner_model) " * diff --git a/src/Tearing/Runner/TearingProblem.jl b/src/Tearing/Runner/TearingProblem.jl index 0c73615af1..d82aeff9ae 100644 --- a/src/Tearing/Runner/TearingProblem.jl +++ b/src/Tearing/Runner/TearingProblem.jl @@ -72,7 +72,7 @@ function CommonSolve.solve(prob::TearingProblem, model::InnerLayer.InnerLayerMod equil = ffs.equil surfaces = ffs.surfaces - model isa SLAYERModel || error("TearingProblem: GGJ tearing growth rates are unsupported in v2.0.0; use SLAYERModel()") + model isa SLAYERModel || error("TearingProblem: GGJ tearing growth rates are not currently supported; use SLAYERModel()") validate(control) control.enabled || return empty_slayer_result(control) isempty(surfaces) && return empty_slayer_result(control)