From 62540fedae71aff9b01bf3da9745be85df0f02c6 Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 30 Oct 2026 14:30:44 -0400 Subject: [PATCH 1/5] Tearing - BUGFIX - Stop the false chi_e fallback warning under chi_perp_Dc and test its forwarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With chi_perp_Dc set the critical Δ no longer uses χ⊥,e, so a run whose P_perp model does not use it either should not warn about a chi_e fallback. Setting the key with D_c_type = none now warns that it has no effect. Documents that the deck key is one value for every surface and that it reaches the critical resonant field, and tests the callable form and the forwarding through the TearingProblem assembly. Co-Authored-By: Claude Opus 5.5 --- docs/src/stability.md | 4 +++- src/Tearing/Runner/Control.jl | 6 ++++-- src/Tearing/Runner/run_slayer.jl | 11 ++++++++--- test/runtests_slayer_inputs.jl | 20 ++++++++++++++++++++ test/runtests_slayer_runner.jl | 6 ++++++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/src/stability.md b/docs/src/stability.md index 0b07e6e77..d473ee127 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -350,7 +350,9 @@ total pressure; under the closure `T_s = T_s(n)` it is set by the perpendicular `n T_s′/p′`. The critical-Δ island width `W_d ∝ (χ_⊥/χ_∥)^{1/4}` describes electron-temperature flattening against an electron parallel conductivity, so its χ_⊥ is the electron channel `chi_e` in every case. `[SLAYER] chi_perp_Dc` (optional) sets that electron diffusivity for the critical Δ -alone, leaving `P_perp` on its own χ inputs. It is for a run whose `P_perp` uses a diffusivity +alone, leaving `P_perp` on its own χ inputs. The deck key is one value for every surface, over +any `chi_e` profile; `build_slayer_inputs` also takes a function of ψ. It moves the critical +resonant field through the critical Δ. It is for a run whose `P_perp` uses a diffusivity the critical Δ should not, for example a one-fluid χ_eff in both channels of `D_⊥` when an interpretive power balance splits the conducted heat between species unreliably, while the critical Δ takes the electron χ that balance allows. `[SLAYER] P_perp_model` selects how `P_perp` is built: from `D_⊥` (`"D_perp"`, diff --git a/src/Tearing/Runner/Control.jl b/src/Tearing/Runner/Control.jl index 67db4626f..d6fd17d02 100644 --- a/src/Tearing/Runner/Control.jl +++ b/src/Tearing/Runner/Control.jl @@ -98,8 +98,10 @@ constructor. unless `chi_perp_Dc` is set - `chi_perp_Dc` -- electron perpendicular heat diffusivity [m²/s] used only by the critical Δ (default `nothing`: the critical Δ uses the same χ⊥,e as P_perp, from the kinetic file's - `chi_e` or `chi_perp_e`). When set, this scalar replaces χ⊥,e in the critical Δ only; - P_perp keeps the file's profiles or the `chi_perp_e`/`chi_perp_i` fallbacks. Must be positive + `chi_e` or `chi_perp_e`). When set, this one value replaces χ⊥,e in the critical Δ at every + surface, over any `chi_e` profile; P_perp keeps the file's profiles or the + `chi_perp_e`/`chi_perp_i` fallbacks. It reaches the critical resonant field through the + critical Δ, and has no effect with `D_c_type = "none"`. Must be positive and finite - `D_R`, `D_geo` -- critical-Δ formula inputs. `nothing` (default) auto-derives them from the equilibrium: `D_R` from the resistive interchange index `D_R = E + F + H²` at each surface, `D_geo` from the diff --git a/src/Tearing/Runner/run_slayer.jl b/src/Tearing/Runner/run_slayer.jl index 2e99129de..ef53cbc3b 100644 --- a/src/Tearing/Runner/run_slayer.jl +++ b/src/Tearing/Runner/run_slayer.jl @@ -76,7 +76,7 @@ function _slayer_chi_inputs(loaded, control::SLAYERControl; warn::Bool=true) ) ) if warn - uses_e = model in (:chi_perp_e, :D_perp) || control.D_c_type !== :none + uses_e = model in (:chi_perp_e, :D_perp) || (control.D_c_type !== :none && control.chi_perp_Dc === nothing) uses_i = model in (:chi_perp_i, :D_perp) uses_tor = model !== :tau_E fallbacks = String[] @@ -85,8 +85,13 @@ function _slayer_chi_inputs(loaded, control::SLAYERControl; warn::Bool=true) uses_tor && loaded.chi_tor === nothing && push!(fallbacks, "chi_phi → chi_tor = $(control.chi_tor)") isempty(fallbacks) || @warn "SLAYER: no usable kinetic-file profile (dataset absent or all-zero) for " * "$(join(fallbacks, ", ")); using the scalar fallback." - control.chi_perp_Dc !== nothing && control.D_c_type !== :none && - @info "SLAYER: the critical Δ uses chi_perp_Dc = $(control.chi_perp_Dc) m²/s for χ⊥,e; P_perp keeps its own χ inputs." + if control.chi_perp_Dc !== nothing + if control.D_c_type === :none + @warn "SLAYER: chi_perp_Dc has no effect with D_c_type = \"none\"; there is no critical Δ to set." + else + @info "SLAYER: the critical Δ uses chi_perp_Dc = $(control.chi_perp_Dc) m²/s for χ⊥,e at every surface; P_perp keeps its own χ inputs." + end + end model === :tau_E && @info "SLAYER: P_perp_model = tau_E sets P_perp and P_tor from a uniform " * "χ_E = (3/16)a²/τ̄_E with τ̄_E = $(control.tau_E) s; the χ_φ profile is not used." end diff --git a/test/runtests_slayer_inputs.jl b/test/runtests_slayer_inputs.jl index a43d98c53..17f097ba7 100644 --- a/test/runtests_slayer_inputs.jl +++ b/test/runtests_slayer_inputs.jl @@ -167,6 +167,26 @@ @test sl_var[1].P_perp ≈ sl_s[1].P_perp * 6.0 / 2.0 rtol = 1e-10 end + @testset "chi_perp_Dc reaches the critical Δ alone, as a callable and through TearingProblem" begin + sings = [_mk_sing(; psi=0.5, q=2.4, q1=1.2, m=2, n=1)] + kw = (; P_perp_model=:chi_perp_e, bt=2.0, chi_perp_e=2.0, chi_tor=1.5, dr_val=0.01, dc_type=:fitzpatrick) + plain = build_slayer_inputs(equil, sings, profiles; kw...)[1] + scalar = build_slayer_inputs(equil, sings, profiles; kw..., chi_perp_Dc=6.0)[1] + varying = build_slayer_inputs(equil, sings, profiles; kw..., chi_perp_Dc=psi -> 1.0 + 10.0 * psi)[1] # 6.0 at ψ = 0.5 + @test varying.dc_tmp == scalar.dc_tmp + @test scalar.dc_tmp != plain.dc_tmp + @test scalar.P_perp == plain.P_perp + + # The deck key, through the same assembly TearingProblem uses. + Runner = GeneralizedPerturbedEquilibrium.Runner + loaded = (; profiles, chi_perp_e=nothing, chi_perp_i=nothing, chi_tor=nothing, zeff=nothing, zeff_source=:scalar) + control(; c...) = Runner.SLAYERControl(; P_perp_model=:chi_perp_e, bt=2.0, chi_perp_e=2.0, chi_tor=1.5, D_R=0.01, D_c_type=:fitzpatrick, c...) + params(c) = Runner._tearing_params(SLAYERModel(), equil, sings, loaded, c)[1] + without, with = params(control()), params(control(; chi_perp_Dc=6.0)) + @test with.dc_tmp != without.dc_tmp + @test with.P_perp == without.P_perp + end + @testset "build_slayer_inputs: P_perp_model forwards the profile gradients and minor radius" begin sings = [_mk_sing(psi=0.5, q=2.4, q1=1.2, m=2, n=1)] kw = (; bt=2.0, chi_perp_e=2.0, chi_perp_i=1.0, chi_tor=1.5, dr_val=0.0) diff --git a/test/runtests_slayer_runner.jl b/test/runtests_slayer_runner.jl index 31b0a7f3c..e4fd5f279 100644 --- a/test/runtests_slayer_runner.jl +++ b/test/runtests_slayer_runner.jl @@ -122,6 +122,12 @@ @test_throws "chi_perp_Dc must be positive" Runner.validate(SLAYERControl(; chi_perp_Dc=-1.0)) @test Runner._slayer_chi_inputs(no_chi, SLAYERControl(; chi_perp_i=0.7, chi_perp_Dc=1.4); warn=false).chi_perp_Dc == 1.4 @test Runner._slayer_chi_inputs(no_chi, SLAYERControl(; chi_perp_i=0.7); warn=false).chi_perp_Dc === nothing + # With it set, the critical Δ needs no χ⊥,e fallback; without a critical Δ, it does nothing and says so. + tau_E_ctrl(; kw...) = SLAYERControl(; P_perp_model=:tau_E, tau_E=0.1, D_c_type=:toroidal, kw...) + @test_logs (:warn, r"chi_e → chi_perp_e") min_level = Base.CoreLogging.Warn Runner._slayer_chi_inputs(no_chi, tau_E_ctrl()) + @test_logs min_level = Base.CoreLogging.Warn Runner._slayer_chi_inputs(no_chi, tau_E_ctrl(; chi_perp_Dc=1.4)) + @test_logs (:warn, r"chi_perp_Dc has no effect") min_level = Base.CoreLogging.Warn Runner._slayer_chi_inputs(no_chi, + SLAYERControl(; P_perp_model=:tau_E, tau_E=0.1, chi_perp_Dc=1.4)) end @testset "TearingProblem solve forwards surfaces and Δ'" begin From fbf66a5d955ff941495292db1fb41291c71d4610 Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 30 Oct 2026 16:38:40 -0400 Subject: [PATCH 2/5] Tearing - DOCS - Rewrite the chi_perp_Dc motivation as a modeling choice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The critical Δ still needs an electron diffusivity when P_perp uses a one-fluid χ_eff, so the key states that choice rather than implying the power balance supplies the electron χ. Names the chi_e/chi_i closure that reaches the same split, and where it fails. Co-Authored-By: Claude Opus 5.5 --- docs/src/stability.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/src/stability.md b/docs/src/stability.md index d473ee127..8bd17410a 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -352,10 +352,15 @@ flattening against an electron parallel conductivity, so its χ_⊥ is the elect in every case. `[SLAYER] chi_perp_Dc` (optional) sets that electron diffusivity for the critical Δ alone, leaving `P_perp` on its own χ inputs. The deck key is one value for every surface, over any `chi_e` profile; `build_slayer_inputs` also takes a function of ψ. It moves the critical -resonant field through the critical Δ. It is for a run whose `P_perp` uses a diffusivity -the critical Δ should not, for example a one-fluid χ_eff in both channels of `D_⊥` when an -interpretive power balance splits the conducted heat between species unreliably, while the -critical Δ takes the electron χ that balance allows. `[SLAYER] P_perp_model` selects how `P_perp` is built: from `D_⊥` (`"D_perp"`, +resonant field through the critical Δ. It is for a run where one χ⊥,e cannot serve +both formulas. `D_⊥` needs only the total conducted heat, so a run can use a one-fluid χ_eff in both +channels when an interpretive power balance cannot separate the species; where T_i ≈ T_e the +electron-ion exchange term makes that split uncertain, and it can return χ⊥,i < 0. The critical Δ +still needs an electron diffusivity, which χ_eff is not, and `chi_perp_Dc` makes that choice +explicit. Where the split is uncertain the electron χ is uncertain too, so treat the value as a +modeling choice and scan it. The same split can often be reached without this key, by choosing +`chi_e` and `chi_i` to keep the `D_⊥` total, but not when the wanted `chi_e` would leave `chi_i ≤ 0`. +`[SLAYER] P_perp_model` selects how `P_perp` is built: from `D_⊥` (`"D_perp"`, the default), from `chi_e` alone (the single-χ convention of Fitzpatrick's TJ code), from `chi_i`, from the toroidal Prandtl number, from its `c_β²` floor, or from a whole-plasma confinement time (`"tau_E"`); see `slayer_parameters`. `"tau_E"` is for a plasma with no χ From d9a119eb6a8ed7dd16a5762c3605a1d5fec23dcb Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 30 Oct 2026 16:44:54 -0400 Subject: [PATCH 3/5] =?UTF-8?q?Tearing=20-=20DOCS=20-=20State=20that=20the?= =?UTF-8?q?=20electron=20chi=5Fperp=20in=20the=20critical=20=CE=94=20is=20?= =?UTF-8?q?a=20modeling=20choice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The W_d and critical-Δ sources are single-fluid, so the electron channel is GPEC's reading of a single χ_⊥ rather than a statement those papers make. Co-Authored-By: Claude Opus 5.5 --- docs/src/stability.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/src/stability.md b/docs/src/stability.md index 8bd17410a..18814bcfd 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -347,16 +347,21 @@ on or off, GPEC warns. different quantities. The layer width goes through `P_perp`, which multiplies the diffusion of the total pressure; under the closure `T_s = T_s(n)` it is set by the perpendicular particle diffusivity `D_⊥` of Fitzpatrick, Phys. Plasmas **29**, 032507 (2022), Eq. 16, which weights the electron and ion energy diffusivities by -`n T_s′/p′`. The critical-Δ island width `W_d ∝ (χ_⊥/χ_∥)^{1/4}` describes electron-temperature -flattening against an electron parallel conductivity, so its χ_⊥ is the electron channel `chi_e` -in every case. `[SLAYER] chi_perp_Dc` (optional) sets that electron diffusivity for the critical Δ +`n T_s′/p′`. The critical-Δ island width `W_d ∝ (χ_⊥/χ_∥)^{1/4}` is the +width above which parallel transport, through an electron parallel conductivity, makes the electron +temperature a flux function (Fitzpatrick, Phys. Plasmas **32**, 062509 (2025), Eqs. 230–234). That +paper and the `"lar"` and `"toroidal"` forms of Connor et al., Plasma Phys. Control. Fusion **57**, +065001 (2015), Eqs. 59 and 61, are single-fluid: they have one χ_⊥, which in Connor et al. diffuses +the total pressure. GPEC reads it as the electron channel and uses `chi_e` for every `D_c_type`, +since the width is set by electron heat transport; that reading is a modeling choice, not a result of +those papers. `[SLAYER] chi_perp_Dc` (optional) sets that electron diffusivity for the critical Δ alone, leaving `P_perp` on its own χ inputs. The deck key is one value for every surface, over any `chi_e` profile; `build_slayer_inputs` also takes a function of ψ. It moves the critical resonant field through the critical Δ. It is for a run where one χ⊥,e cannot serve both formulas. `D_⊥` needs only the total conducted heat, so a run can use a one-fluid χ_eff in both channels when an interpretive power balance cannot separate the species; where T_i ≈ T_e the -electron-ion exchange term makes that split uncertain, and it can return χ⊥,i < 0. The critical Δ -still needs an electron diffusivity, which χ_eff is not, and `chi_perp_Dc` makes that choice +electron-ion exchange term makes that split uncertain, and it can return χ⊥,i < 0. On that reading the +critical Δ still needs an electron diffusivity, which χ_eff is not, and `chi_perp_Dc` makes the choice explicit. Where the split is uncertain the electron χ is uncertain too, so treat the value as a modeling choice and scan it. The same split can often be reached without this key, by choosing `chi_e` and `chi_i` to keep the `D_⊥` total, but not when the wanted `chi_e` would leave `chi_i ≤ 0`. From 0223e90e9b0d4c72fb5de9914030282d1cb3f401 Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 30 Oct 2026 16:48:23 -0400 Subject: [PATCH 4/5] Tearing - DOCS - Name the electron reading in the chi_perp_Dc motivation Co-Authored-By: Claude Opus 5.5 --- docs/src/stability.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/stability.md b/docs/src/stability.md index 18814bcfd..83602334a 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -360,8 +360,8 @@ any `chi_e` profile; `build_slayer_inputs` also takes a function of ψ. It moves resonant field through the critical Δ. It is for a run where one χ⊥,e cannot serve both formulas. `D_⊥` needs only the total conducted heat, so a run can use a one-fluid χ_eff in both channels when an interpretive power balance cannot separate the species; where T_i ≈ T_e the -electron-ion exchange term makes that split uncertain, and it can return χ⊥,i < 0. On that reading the -critical Δ still needs an electron diffusivity, which χ_eff is not, and `chi_perp_Dc` makes the choice +electron-ion exchange term makes that split uncertain, and it can return χ⊥,i < 0. On the electron reading, +the critical Δ still needs an electron diffusivity, which χ_eff is not, and `chi_perp_Dc` makes the choice explicit. Where the split is uncertain the electron χ is uncertain too, so treat the value as a modeling choice and scan it. The same split can often be reached without this key, by choosing `chi_e` and `chi_i` to keep the `D_⊥` total, but not when the wanted `chi_e` would leave `chi_i ≤ 0`. From be370929430699f8a1cb100b7ab796c6e76c7533 Mon Sep 17 00:00:00 2001 From: logan-nc Date: Sun, 1 Nov 2026 18:49:20 -0500 Subject: [PATCH 5/5] =?UTF-8?q?Tearing=20-=20BUGFIX=20-=20Skip=20the=20fil?= =?UTF-8?q?e's=20chi=5Fe=20in=20the=20SLAYER=20view=20when=20chi=5Fperp=5F?= =?UTF-8?q?Dc=20replaces=20it=20in=20the=20critical=20=CE=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186CLfjeJeSusNTva5RMfGU --- src/InnerLayer/SLAYER/KineticView.jl | 3 ++- test/runtests_slayer_inputs.jl | 3 ++- test/runtests_slayer_runner.jl | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/InnerLayer/SLAYER/KineticView.jl b/src/InnerLayer/SLAYER/KineticView.jl index b8b646baa..b404b6b7d 100644 --- a/src/InnerLayer/SLAYER/KineticView.jl +++ b/src/InnerLayer/SLAYER/KineticView.jl @@ -27,7 +27,8 @@ function _slayer_profiles(data::KineticProfileData, control) end # `control === nothing` reads no diffusivity, for callers that need only the densities, temperatures and Z_eff. model = control === nothing ? nothing : control.P_perp_model - uses_e = model in (:chi_perp_e, :D_perp) || (control !== nothing && control.D_c_type !== :none) + # With chi_perp_Dc set, the critical Δ takes it in place of χ⊥,e, so only P_perp can need the file's chi_e. + uses_e = model in (:chi_perp_e, :D_perp) || (control !== nothing && control.D_c_type !== :none && control.chi_perp_Dc === nothing) chi_perp_e = uses_e ? _chi_spline("chi_e", data.chi_e) : nothing chi_perp_i = model in (:chi_perp_i, :D_perp) ? _chi_spline("chi_i", data.chi_i) : nothing chi_tor = model !== nothing && model !== :tau_E ? _chi_spline("chi_phi", data.chi_phi) : nothing diff --git a/test/runtests_slayer_inputs.jl b/test/runtests_slayer_inputs.jl index 7a2741445..851d5dbda 100644 --- a/test/runtests_slayer_inputs.jl +++ b/test/runtests_slayer_inputs.jl @@ -182,7 +182,8 @@ loaded = (; profiles, chi_perp_e=nothing, chi_perp_i=nothing, chi_tor=nothing, zeff=nothing, zeff_source=:scalar) model(; c...) = SLAYERModel(; P_perp_model=:chi_perp_e, D_R=0.01, D_c_type=:fitzpatrick, c...) attached = equil.kinetic - equil.kinetic = Equilibrium.KineticProfileData(; psi=psi_pts, n_e=profiles.n_e, T_e=profiles.T_e, T_i=profiles.T_i, + equil.kinetic = Equilibrium.KineticProfileData(; psi=psi_pts, n_e=fill(5.0e19, length(psi_pts)), + T_e=1000.0 .* (1.0 .- 0.7 .* psi_pts), T_i=1000.0 .* (1.0 .- 0.6 .* psi_pts), chi_e_default=2.0, chi_phi_default=1.5) params(m) = Tearing._tearing_params(m, equil, sings, loaded)[1] without, with = params(model()), params(model(; chi_perp_Dc=6.0)) diff --git a/test/runtests_slayer_runner.jl b/test/runtests_slayer_runner.jl index 34ce69a7e..947a2e7c1 100644 --- a/test/runtests_slayer_runner.jl +++ b/test/runtests_slayer_runner.jl @@ -129,6 +129,11 @@ @test bare_view.profiles.omega_E(data.psi[j]) == 0.0 @test_throws "missing required dataset 'T_i'" Tearing._slayer_profiles( GeneralizedPerturbedEquilibrium.Equilibrium.KineticProfileData(; psi=data.psi, n_e=data.n_e, T_e=data.T_e), SLAYERModel()) + # A non-positive chi_e stops the run only where it is used: chi_perp_Dc replaces it in the critical Δ. + bad_chi_e = GeneralizedPerturbedEquilibrium.Equilibrium.KineticProfileData(; psi=data.psi, n_e=data.n_e, T_e=data.T_e, T_i=data.T_i, + chi_e=fill(-1.0, length(data.psi))) + @test_throws "chi_e" Tearing._slayer_profiles(bad_chi_e, SLAYERModel(; P_perp_model=:tau_E, D_c_type=:toroidal)) + @test Tearing._slayer_profiles(bad_chi_e, SLAYERModel(; P_perp_model=:tau_E, D_c_type=:toroidal, chi_perp_Dc=1.4)).chi_perp_e === nothing end # Δ′ is unified across formalisms, so a Galerkin run feeds SLAYER exactly as a Riccati one