From a5dea9fedebece5d2902ed54088f0932f01e5165 Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 28 Aug 2026 02:07:42 -0400 Subject: [PATCH 1/7] InnerLayer.SLAYER - BUGFIX! - Normalize D by the electron diamagnetic share, not the temperature ratio D_norm used sqrt(tau/(1+tau)) with tau = T_i/T_e. Fitzpatrick defines the ion-sound-radius normalization as D = S^(1/3) iota_e^(1/2) (d_beta/r_s), and iota_e reduces to 1/(1+tau), not tau/(1+tau) -- the same expression with the electron and ion temperatures transposed. The two agree only at T_i = T_e. Use the iota_e that slayer_parameters already computes from omega_*e and omega_*i. That is Fitzpatrick's own definition, iota_e = Q_e/(Q_e - Q_i), and it is strictly more general than either closed form: it carries the ratio of the electron and ion pressure-gradient scale lengths rather than assuming they match. Guard against a non-positive iota_e so a same-sign diamagnetic input raises a clear error instead of a DomainError under the square root. Results move on any deck with T_i != T_e, including the shipped SLAYER deck. Co-Authored-By: Claude Opus 5 --- src/InnerLayer/SLAYER/LayerParameters.jl | 20 ++++++++++++++++++-- test/runtests_slayer_params.jl | 5 +++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/InnerLayer/SLAYER/LayerParameters.jl b/src/InnerLayer/SLAYER/LayerParameters.jl index 9718ad666..f8edd7306 100644 --- a/src/InnerLayer/SLAYER/LayerParameters.jl +++ b/src/InnerLayer/SLAYER/LayerParameters.jl @@ -27,7 +27,7 @@ de-normalization. The parametrization uses `P_perp`, `P_tor`, and | `tau` | T_i / T_e | | `lu` | Lundquist number S = τ_R / τ_H | | `c_beta` | Compressibility √(β_local / (1 + β_local)) | -| `D_norm` | (d_β/r_s) · S^(1/3) · √(τ/(1+τ)) (Fitzpatrick normalized scale) | +| `D_norm` | (d_β/r_s) · S^(1/3) · √ι_e (Fitzpatrick normalized scale) | | `P_perp` | Perpendicular Prandtl number τ_R / τ_⊥ | | `P_tor` | Toroidal-direction Prandtl number τ_R / τ_‖tor | | `Q_e` | Normalized electron diamagnetic: −tauk · ω_*e | @@ -329,6 +329,19 @@ function slayer_parameters(; ) ) iota_e = Q_e / Q_e_minus_Q_i + # D below takes sqrt(iota_e). iota_e lies in (0,1) whenever ω_*e and ω_*i have + # opposite signs — the physical case and the convention stated above. Surface a + # negative value as a clear error rather than a bare DomainError. + iota_e > 0 || + throw( + ArgumentError( + "slayer_parameters: iota_e = Q_e/(Q_e - Q_i) = $iota_e is " * + "not positive, so the ion-sound-radius normalization " * + "D_norm = (d_beta/r_s)·S^(1/3)·sqrt(iota_e) is undefined. " * + "Check the diamagnetic-frequency inputs ω_*e, ω_*i — they " * + "are expected to carry opposite signs." + ) + ) # Plasma beta and compressibility lbeta = (5.0 / 3.0) * MU_0 * n_e * E_CHG * (t_e + t_i) / bt^2 @@ -342,7 +355,10 @@ function slayer_parameters(; # Normalized beta-related width and Δ-normalization d_beta = c_beta * d_i - D_norm = (d_beta / rs) * lu^(1.0 / 3.0) * sqrt(tau / (1.0 + tau)) + # D = S^(1/3)·iota_e^(1/2)·(d_beta/r_s). iota_e is the electron share of the + # total diamagnetic frequency; it reduces to 1/(1+tau) only when the electron + # and ion pressure-gradient scale lengths match, so use the computed value. + D_norm = (d_beta / rs) * lu^(1.0 / 3.0) * sqrt(iota_e) delta_n = lu^(1.0 / 3.0) / rs # Critical-Δ offset from chi_parallel matching diff --git a/test/runtests_slayer_params.jl b/test/runtests_slayer_params.jl index 330ba7297..c6f42a531 100644 --- a/test/runtests_slayer_params.jl +++ b/test/runtests_slayer_params.jl @@ -83,8 +83,9 @@ @test p.P_perp ≈ p.P_tor @test p.P_perp > 0 - # D_norm = (d_β/r_s)·S^(1/3)·√(τ/(1+τ)) - D_norm_expected = (p.d_beta / p.rs) * p.lu^(1 / 3) * sqrt(p.tau / (1 + p.tau)) + # D_norm = (d_β/r_s)·S^(1/3)·√ι_e — the electron share of the total + # diamagnetic frequency, not the temperature ratio. + D_norm_expected = (p.d_beta / p.rs) * p.lu^(1 / 3) * sqrt(p.iota_e) @test p.D_norm ≈ D_norm_expected rtol = 1e-12 # delta_n = S^(1/3)/r_s From 8df7535e3c8b6d487f223167ef838ca881602c1b Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 28 Aug 2026 13:23:33 -0400 Subject: [PATCH 2/7] InnerLayer.SLAYER - MINOR - Deduplicate the iota_e normalization commentary Co-Authored-By: Claude Fable 5 --- src/InnerLayer/SLAYER/LayerParameters.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/InnerLayer/SLAYER/LayerParameters.jl b/src/InnerLayer/SLAYER/LayerParameters.jl index f8edd7306..15bc6aac1 100644 --- a/src/InnerLayer/SLAYER/LayerParameters.jl +++ b/src/InnerLayer/SLAYER/LayerParameters.jl @@ -355,9 +355,8 @@ function slayer_parameters(; # Normalized beta-related width and Δ-normalization d_beta = c_beta * d_i - # D = S^(1/3)·iota_e^(1/2)·(d_beta/r_s). iota_e is the electron share of the - # total diamagnetic frequency; it reduces to 1/(1+tau) only when the electron - # and ion pressure-gradient scale lengths match, so use the computed value. + # iota_e is the electron share of the total diamagnetic frequency; it reduces to + # 1/(1+tau) only when the electron and ion pressure-gradient scale lengths match. D_norm = (d_beta / rs) * lu^(1.0 / 3.0) * sqrt(iota_e) delta_n = lu^(1.0 / 3.0) / rs From 9b44f8cfc6b8e34002a4c952ccce2148c47efcc5 Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 28 Aug 2026 14:40:20 -0400 Subject: [PATCH 3/7] InnerLayer.SLAYER - MINOR - Restore the D-normalization formula citation Put the explicit D = S^(1/3)*iota_e^(1/2)*(d_beta/r_s) statement back above the code so the formula-to-theory traceability survives next to what it computes. Co-Authored-By: Claude Opus 5 --- src/InnerLayer/SLAYER/LayerParameters.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/InnerLayer/SLAYER/LayerParameters.jl b/src/InnerLayer/SLAYER/LayerParameters.jl index 15bc6aac1..ea707e17c 100644 --- a/src/InnerLayer/SLAYER/LayerParameters.jl +++ b/src/InnerLayer/SLAYER/LayerParameters.jl @@ -355,6 +355,7 @@ function slayer_parameters(; # Normalized beta-related width and Δ-normalization d_beta = c_beta * d_i + # D = S^(1/3)·iota_e^(1/2)·(d_beta/r_s), Fitzpatrick's ion-sound-radius normalization. # iota_e is the electron share of the total diamagnetic frequency; it reduces to # 1/(1+tau) only when the electron and ion pressure-gradient scale lengths match. D_norm = (d_beta / rs) * lu^(1.0 / 3.0) * sqrt(iota_e) From e32740a80895f6ba13e3835e92fbaef5501260dc Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 28 Aug 2026 16:14:45 -0400 Subject: [PATCH 4/7] InnerLayer.SLAYER - TEST - Give the shared fixtures the physical opposite-drift diamagnetic signs The shared test inputs set omega_e and omega_i both positive, which no plasma produces: electrons and ions drift oppositely, the pipeline computes omega_*e < 0 < omega_*i (all six shipped-deck surfaces have Q_e > 0 > Q_i, matching TJ), and same-sign inputs put iota_e = 2 outside its physical (0, 1) range. Flip omega_e to -1e4 so iota_e = 2/3, matching the hand-built small-D fixture, and update the two assertions that encoded the old values. Branch selection cannot move: iota_e appears on both sides of the regime test and cancels. Co-Authored-By: Claude Opus 5 --- test/runtests_dispersion_coupled.jl | 2 +- test/runtests_dispersion_residual.jl | 2 +- test/runtests_slayer_params.jl | 14 +++++++------- test/runtests_slayer_riccati.jl | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/runtests_dispersion_coupled.jl b/test/runtests_dispersion_coupled.jl index 5a65539ff..1b950016e 100644 --- a/test/runtests_dispersion_coupled.jl +++ b/test/runtests_dispersion_coupled.jl @@ -21,7 +21,7 @@ function _slayer_ref() return slayer_parameters( n_e=5.0e19, t_e=1000.0, t_i=1000.0, - omega=0.0, omega_e=1.0e4, omega_i=5.0e3, + omega=0.0, omega_e=-1.0e4, omega_i=5.0e3, qval=2.0, sval_r=1.0, bt=2.0, rs=0.5, R0=1.7, mu_i=2.0, zeff=1.0, chi_perp=1.0, chi_tor=1.0, m=2, n=1) diff --git a/test/runtests_dispersion_residual.jl b/test/runtests_dispersion_residual.jl index d0235a0da..e8af06da4 100644 --- a/test/runtests_dispersion_residual.jl +++ b/test/runtests_dispersion_residual.jl @@ -21,7 +21,7 @@ function _slayer_ref() return slayer_parameters( n_e=5.0e19, t_e=1000.0, t_i=1000.0, - omega=0.0, omega_e=1.0e4, omega_i=5.0e3, + omega=0.0, omega_e=-1.0e4, omega_i=5.0e3, qval=2.0, sval_r=1.0, bt=2.0, rs=0.5, R0=1.7, mu_i=2.0, zeff=1.0, chi_perp=1.0, chi_tor=1.0, m=2, n=1) diff --git a/test/runtests_slayer_params.jl b/test/runtests_slayer_params.jl index c6f42a531..397bee29e 100644 --- a/test/runtests_slayer_params.jl +++ b/test/runtests_slayer_params.jl @@ -9,7 +9,7 @@ function _ref_kwargs(; dr_val=0.0, dc_type=:none) return ( n_e=5.0e19, t_e=1000.0, t_i=1000.0, - omega=0.0, omega_e=1.0e4, omega_i=5.0e3, + omega=0.0, omega_e=-1.0e4, omega_i=5.0e3, qval=2.0, sval_r=1.0, bt=2.0, rs=0.5, R0=1.7, mu_i=2.0, zeff=1.0, chi_perp=1.0, chi_tor=1.0, @@ -36,13 +36,13 @@ # Trivially exact ratios @test p.tau ≈ 1.0 - # Q_e = −tauk·1e4 = negative; Q_i = −tauk·5e3 = negative - # Q_e − Q_i = −tauk·5e3 = Q_i (since Q_e = 2·Q_i) ⇒ iota_e = Q_e/Q_i = 2 - @test p.iota_e ≈ 2.0 + # Physical opposite-drift inputs: ω_*e < 0 < ω_*i, so Q = −tauk·ω gives + # Q_e > 0 > Q_i and iota_e = Q_e/(Q_e − Q_i) = 1e4/(1e4 + 5e3) = 2/3 + @test p.iota_e ≈ 2 / 3 - # Sign convention check (SLAYER layerinputs) - @test p.Q_e == -p.tauk * 1.0e4 - @test p.Q_i == -p.tauk * 5.0e3 # SLAYER params convention: Q_i = −tauk·ω*i + # Sign convention check (SLAYER layerinputs): Q = −tauk·ω + @test p.Q_e == p.tauk * 1.0e4 + @test p.Q_i == -p.tauk * 5.0e3 # Default resistivity closure is neoclassical (Sauter F_33). The # trapped-particle correction raises η above plain Spitzer at the diff --git a/test/runtests_slayer_riccati.jl b/test/runtests_slayer_riccati.jl index 22f7fc549..9d4a95462 100644 --- a/test/runtests_slayer_riccati.jl +++ b/test/runtests_slayer_riccati.jl @@ -15,7 +15,7 @@ function _ref_params_large_D() return slayer_parameters(; n_e=5.0e19, t_e=3000.0, t_i=3000.0, - omega=0.0, omega_e=1.0e4, omega_i=5.0e3, + omega=0.0, omega_e=-1.0e4, omega_i=5.0e3, qval=2.0, sval_r=1.0, bt=2.0, rs=0.5, R0=1.7, mu_i=2.0, zeff=1.0, chi_perp=1.0, chi_tor=1.0, From bddd428b91a6004fbdeef186d8d3f1e6f703f9d2 Mon Sep 17 00:00:00 2001 From: d-burg Date: Fri, 4 Sep 2026 17:23:10 -0400 Subject: [PATCH 5/7] InnerLayer.SLAYER - MINOR - Drop the redundant D-normalization commentary Both comments restated what the code around them already says. The iota_e guard comment paraphrased the ArgumentError immediately below it, and the D_norm comment repeated a formula the struct docstring table already carries, next to a row that already defines iota_e = Q_e/(Q_e - Q_i). Co-Authored-By: Claude Opus 5 --- src/InnerLayer/SLAYER/LayerParameters.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/InnerLayer/SLAYER/LayerParameters.jl b/src/InnerLayer/SLAYER/LayerParameters.jl index ea707e17c..65ca547ad 100644 --- a/src/InnerLayer/SLAYER/LayerParameters.jl +++ b/src/InnerLayer/SLAYER/LayerParameters.jl @@ -329,9 +329,6 @@ function slayer_parameters(; ) ) iota_e = Q_e / Q_e_minus_Q_i - # D below takes sqrt(iota_e). iota_e lies in (0,1) whenever ω_*e and ω_*i have - # opposite signs — the physical case and the convention stated above. Surface a - # negative value as a clear error rather than a bare DomainError. iota_e > 0 || throw( ArgumentError( @@ -355,9 +352,6 @@ function slayer_parameters(; # Normalized beta-related width and Δ-normalization d_beta = c_beta * d_i - # D = S^(1/3)·iota_e^(1/2)·(d_beta/r_s), Fitzpatrick's ion-sound-radius normalization. - # iota_e is the electron share of the total diamagnetic frequency; it reduces to - # 1/(1+tau) only when the electron and ion pressure-gradient scale lengths match. D_norm = (d_beta / rs) * lu^(1.0 / 3.0) * sqrt(iota_e) delta_n = lu^(1.0 / 3.0) / rs From 2da8edcddc10978df3bb9906a718ca7452a6c023 Mon Sep 17 00:00:00 2001 From: d-burg Date: Wed, 16 Sep 2026 12:07:02 -0400 Subject: [PATCH 6/7] Tearing - BUGFIX - Describe D_norm with the iota_e normalization in the HDF5 annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PerSurface/D_norm long_name still gave the retired √(τ/(1+τ)) factor, so every new gpec.h5 labelled the corrected values with the old formula. Match the struct docstring: D = (d_β/r_s)·S^(1/3)·√ι_e. Co-Authored-By: Claude Opus 5 --- src/Tearing/Runner/HDF5Output.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tearing/Runner/HDF5Output.jl b/src/Tearing/Runner/HDF5Output.jl index e3052d6b4..b0fc8d2e4 100644 --- a/src/Tearing/Runner/HDF5Output.jl +++ b/src/Tearing/Runner/HDF5Output.jl @@ -74,7 +74,7 @@ const TEARING_H5_ANNOTATIONS = [ "PerSurface/tau" => (; long_name="temperature ratio τ = T_i/T_e per surface", dims=("surface",)), "PerSurface/lu" => (; long_name="Lundquist number S per surface", dims=("surface",)), "PerSurface/c_beta" => (; long_name="compressibility factor c_β = √(β_local/(1+β_local)) per surface", dims=("surface",)), - "PerSurface/D_norm" => (; long_name="Fitzpatrick normalized ion-sound/drift scale D = (d_β/r_s)·S^(1/3)·√(τ/(1+τ)) per surface", dims=("surface",)), + "PerSurface/D_norm" => (; long_name="Fitzpatrick normalized ion-sound/drift scale D = (d_β/r_s)·S^(1/3)·√ι_e per surface", dims=("surface",)), "PerSurface/P_perp" => (; long_name="perpendicular magnetic Prandtl number per surface", dims=("surface",)), "PerSurface/P_tor" => (; long_name="toroidal (momentum) magnetic Prandtl number per surface", dims=("surface",)), "PerSurface/Q_e" => (; long_name="normalized electron diamagnetic frequency Q_e per surface", dims=("surface",)), From 74b6c94f5f8b7d91b945dcada15021b2cc4dac9c Mon Sep 17 00:00:00 2001 From: d-burg Date: Wed, 16 Sep 2026 12:07:02 -0400 Subject: [PATCH 7/7] InnerLayer.SLAYER - TEST - Cover the diamagnetic-frequency guards in slayer_parameters Neither ArgumentError guard on the diamagnetic frequencies was exercised. Assert the degenerate case (iota_e singular), same-sign drifts with the ion drift dominant (iota_e < 0), and zero electron drift (iota_e = 0). Co-Authored-By: Claude Opus 5 --- test/runtests_slayer_params.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/runtests_slayer_params.jl b/test/runtests_slayer_params.jl index 63d74ba83..1f665fb1d 100644 --- a/test/runtests_slayer_params.jl +++ b/test/runtests_slayer_params.jl @@ -140,6 +140,14 @@ @test p.ising == 0 end + @testset "Test 1d: reject diamagnetic frequencies that leave D undefined" begin + # iota_e = ω_*e/(ω_*e − ω_*i) must be finite and positive for D ∝ √iota_e. + _drifts(ωe, ωi) = merge(_ref_kwargs(), (omega_e=ωe, omega_i=ωi)) + @test_throws ArgumentError slayer_parameters(; _drifts(-1.0e4, -1.0e4)...) # degenerate: iota_e singular + @test_throws ArgumentError slayer_parameters(; _drifts(-1.0e4, -2.0e4)...) # same sign, |ω_*i| > |ω_*e|: iota_e = -1 + @test_throws ArgumentError slayer_parameters(; _drifts(0.0, 5.0e3)...) # no electron drift: iota_e = 0 + end + @testset "Test 2: r-based shear conversion" begin # Direct application of r_s · (dq/dψ) / (q · da/dψ). @test r_based_shear(0.5, 2.0, 4.0, 0.5) ≈ 2.0