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
7 changes: 5 additions & 2 deletions src/GeneralizedPerturbedEquilibrium.jl
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ function main_from_inputs(
@info "\n Force-Free States\n$_SECTION"
ffs_start = time()

# _overlap_slayer_inputs also rejects an enabled [SLAYER] section's unusable profile_file, so keep it ahead of the solve.
metric, mats, layer_overlap = prepare_force_free_states!(intr, ctrl, equil, kf_ctrl, kinetic_profiles;
species=kf_species,
overlap_inputs=_overlap_slayer_inputs(inputs, intr.dir_path))
Expand Down Expand Up @@ -567,14 +568,16 @@ end

# Kinetic profiles for the overlap scan come from the [SLAYER] section, through the same control
# and loader the SLAYER analysis itself uses, so the scan sees the same profiles, the same HDF5
# group and the same chi(psi). Returns `nothing` when there is no usable [SLAYER] profile_file.
# group and the same chi(psi). Returns `nothing` when there is no usable [SLAYER] profile_file; with
# tearing enabled that is an error instead, so the run fails before the solve rather than at its end.
function _overlap_slayer_inputs(inputs, dir_path::AbstractString)
(inputs isa AbstractDict && haskey(inputs, "SLAYER")) || return nothing
ctrl = Runner.slayer_control_from_toml(inputs["SLAYER"])
isempty(ctrl.profile_file) && return nothing
isempty(ctrl.profile_file) && !ctrl.enabled && return nothing
loaded = try
Runner._load_profiles(ctrl, dir_path)
catch err
ctrl.enabled && rethrow()
@warn "Layer-overlap scan skipped: could not read the [SLAYER] profile_file." exception = err
return nothing
end
Expand Down
14 changes: 8 additions & 6 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 and only option; the
`:ggj_*` models are rejected because GGJ tearing growth rates are not implemented)
- `inner_model` -- `:slayer_fitzpatrick` (default and only supported option; the `:ggj_*` names
are accepted only when `enabled = false`, 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 @@ -173,19 +173,21 @@ there is one consistent interface for resistive and kinetic profiles.
end

const _VALID_INNER_MODELS = (:slayer_fitzpatrick,)
const _UNSUPPORTED_INNER_MODELS = (:ggj_ray, :ggj_shooting, :ggj_galerkin)
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 ||
ctrl.inner_model in _VALID_INNER_MODELS || ctrl.inner_model in _UNSUPPORTED_INNER_MODELS ||
throw(ArgumentError("SLAYERControl: inner_model=$(ctrl.inner_model) " *
"not in $(_VALID_INNER_MODELS)"))
# A disabled section is never solved, so it may keep a GGJ model while serving only its profiles.
!ctrl.enabled || ctrl.inner_model in _VALID_INNER_MODELS ||
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.scan_mode in _VALID_SCAN_MODES ||
throw(ArgumentError("SLAYERControl: scan_mode=$(ctrl.scan_mode) " *
"not in $(_VALID_SCAN_MODES)"))
Expand Down
8 changes: 8 additions & 0 deletions test/runtests_layer_overlap_fitzpatrick.jl
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ end
oi = overlap_inputs(Dict("ion_mass" => 3.0, "bt" => 1.7, "theta_sample" => 0.3, "lnLambda_form" => "sauter"))
@test (oi.mu_i, oi.bt, oi.theta, oi.lnLambda_form) == (3.0, 1.7, 0.3, :sauter)

# A disabled section feeds the scan whatever its inner model, and a missing or empty file only skips the scan;
# with tearing enabled the same file is an error, raised here rather than after every other stage.
@test overlap_inputs(Dict("inner_model" => "ggj_ray")) !== nothing
@test (@test_logs (:warn, r"Layer-overlap scan skipped") match_mode = :any overlap_inputs(Dict("profile_file" => "missing.h5"))) === nothing
@test (@test_logs overlap_inputs(Dict("profile_file" => ""))) === nothing
@test_throws ErrorException overlap_inputs(Dict("enabled" => true, "profile_file" => "missing.h5"))
@test_throws ErrorException overlap_inputs(Dict("enabled" => true, "profile_file" => ""))

# Z_eff(psi) comes from the kinetic file's own n_i/n_e with the [SLAYER] impurity charge.
kin_path = joinpath(trunc_dir, slayer_section["profile_file"])
kin = Equilibrium.read_kinetic_file(kin_path)
Expand Down
4 changes: 3 additions & 1 deletion test/runtests_slayer_runner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,10 @@

@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.validate(SLAYERControl(; enabled=true, inner_model=Symbol(name)))
@test_throws ArgumentError Runner.slayer_control_from_toml(Dict{String,Any}("enabled" => true, "inner_model" => name))
# A disabled section is never solved, so its inner model is not checked.
@test Runner.slayer_control_from_toml(Dict{String,Any}("inner_model" => name)).inner_model === Symbol(name)
end
no_surfaces = (equil=nothing, surfaces=GeneralizedPerturbedEquilibrium.ForceFreeStates.SingType[],
delta_prime=nothing, dir_path=".")
Expand Down
Loading