Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/DIIID-like_SLAYER_example/gpec.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
51 changes: 20 additions & 31 deletions src/GeneralizedPerturbedEquilibrium.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

# ----------------------------------------------------------------
Expand Down Expand Up @@ -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

"""
Expand Down
9 changes: 6 additions & 3 deletions src/Tearing/Runner/Control.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -164,14 +164,17 @@ 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)
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 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) " *
"not in $(_VALID_INNER_MODELS)"))
Expand Down
2 changes: 1 addition & 1 deletion src/Tearing/Runner/TearingProblem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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 not currently supported; use SLAYERModel()")
validate(control)
control.enabled || return empty_slayer_result(control)
isempty(surfaces) && return empty_slayer_result(control)
Expand Down
6 changes: 0 additions & 6 deletions src/Tearing/Runner/run_slayer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions test/runtests_slayer_runner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 2 additions & 0 deletions test/runtests_solve_api.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading