diff --git a/src/GeneralizedPerturbedEquilibrium.jl b/src/GeneralizedPerturbedEquilibrium.jl index 5ed395dec5..d8cef51741 100755 --- a/src/GeneralizedPerturbedEquilibrium.jl +++ b/src/GeneralizedPerturbedEquilibrium.jl @@ -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)) @@ -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 diff --git a/src/Tearing/Runner/Control.jl b/src/Tearing/Runner/Control.jl index 066b6d43d4..1e9c2ba2eb 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 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) @@ -173,6 +173,7 @@ 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) @@ -180,12 +181,13 @@ 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)")) diff --git a/test/runtests_layer_overlap_fitzpatrick.jl b/test/runtests_layer_overlap_fitzpatrick.jl index d532f205c9..4377f2e1a0 100644 --- a/test/runtests_layer_overlap_fitzpatrick.jl +++ b/test/runtests_layer_overlap_fitzpatrick.jl @@ -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) diff --git a/test/runtests_slayer_runner.jl b/test/runtests_slayer_runner.jl index b157ed04d4..2c4a42d5ac 100644 --- a/test/runtests_slayer_runner.jl +++ b/test/runtests_slayer_runner.jl @@ -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=".")