Skip to content

Commit af67255

Browse files
d-burgclaude
andcommitted
TEARING - BUG FIX - Convert Δ' to the r_s reference length before slab-layer matching
The SLAYER dispersion relation paired a ψ_N-referenced BVP Δ' with an r_s-referenced layer side (Δ(Q), S=τ_R/τ_H on r_s, and the critical-Δ, whose Ŵ_d is W/r_s). Apply the Frobenius reference-length transform Δ̂_ij = K_i^(1/2+μ_i)·Δ'_ij·K_j^(μ_j−1/2), K = r_s·(dψ_N/dr)|_s, μ = √(−D_I), at the matching point. Verified parameter-free against the TJ circular benchmarks (median residual +1% over 23 points; absolute 2/1 agreement 17.5%→2.5% (β) and 17.6%→1.1% (ε)). BVP Δ' outputs are unchanged; only the SLAYER matching (γ, Δ_eff) moves. GGJ untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9491f89 commit af67255

7 files changed

Lines changed: 137 additions & 9 deletions

File tree

src/InnerLayer/SLAYER/LayerInputs.jl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,20 @@ function build_slayer_inputs(equil, sings, profiles::KineticProfiles;
289289
_eval(dgeo_val, psi)
290290
end
291291

292+
# Reference-length conversion inputs for the outer Δ': K = r_s·(dψ_N/dr)|_s
293+
# and μ = √(−D_I) with D_I = E + F + H − 1/4 (Glasser-Greene-Johnson 1975).
294+
# A Mercier-unstable surface (D_I ≥ 0) has no real exponent; μ → 0 there,
295+
# which makes the conversion factor K^(2μ) → 1 continuously (Δ' left raw).
296+
k_ref_k = da_dpsi == 0.0 ? 1.0 : rs / da_dpsi
297+
mu_k = if rg === nothing
298+
@warn("build_slayer_inputs: sing.restype not populated; using the " *
299+
"slab Mercier exponent μ = 1/2 for the Δ' reference-length " *
300+
"conversion at all such surfaces.", maxlog=1)
301+
0.5
302+
else
303+
sqrt(max(-(rg.E + rg.F + rg.H - 0.25), 0.0))
304+
end
305+
292306
out[k] = slayer_parameters(;
293307
n_e=prof.n_e, t_e=prof.T_e, t_i=prof.T_i,
294308
omega=prof.omega, omega_e=ω_e_use, omega_i=ω_i_use,
@@ -304,7 +318,9 @@ function build_slayer_inputs(equil, sings, profiles::KineticProfiles;
304318
f_trap=f_trap_kw,
305319
nu_e_star=nu_e_star_kw,
306320
R_major_eff=R_major_eff,
307-
lnLambda_form=lnLambda_form
321+
lnLambda_form=lnLambda_form,
322+
k_ref=k_ref_k,
323+
mu_mercier=mu_k
308324
)
309325
end
310326
return out

src/InnerLayer/SLAYER/LayerParameters.jl

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ de-normalization. The parametrization uses `P_perp`, `P_tor`, and
4646
| `d_beta` | Beta-weighted ion length scale c_β · d_i [m] |
4747
| `dc_tmp` | Critical-Δ offset from chi_parallel matching |
4848
| `dc_type` | Selector for `dc_tmp` formula |
49+
| `k_ref` | Reference-length ratio K = r_s · (dψ_N/dr) at this surface (1 = no Δ' conversion) |
50+
| `mu_mercier` | Mercier exponent μ = √(−D_I) governing the Δ' reference-length conversion (1/2 = slab/cylindrical value) |
51+
52+
`k_ref` and `mu_mercier` feed the ψ_N → r_s reference-length conversion of
53+
the outer Δ' matrix (see `delta_prime_to_rs_reference` in the Tearing
54+
runner): the slab layer, its `dc_tmp` critical-Δ, and the `S^(1/3)` Δ(Q)
55+
scale are all referenced to unit `x̂ = (r−r_s)/r_s`, while the outer BVP Δ'
56+
is referenced to unit Δψ_N. Hand-built parameters default to `k_ref = 1`,
57+
which makes the conversion the identity.
4958
5059
The complex normalized growth rate `Q = ω + iγ` is **not** stored here;
5160
it is passed as a separate argument to `solve_inner`.
@@ -85,6 +94,10 @@ Base.@kwdef struct SLAYERParameters <: InnerLayerParameters
8594
# Critical-Δ offset
8695
dc_tmp::Float64 = 0.0
8796
dc_type::Symbol = :none
97+
98+
# Reference-length conversion inputs for the outer Δ' (ψ_N → r_s-based x̂)
99+
k_ref::Float64 = 1.0
100+
mu_mercier::Float64 = 0.5
88101
end
89102

90103
# Allowed dc_type values for the critical-Δ offset. `:none` is the default
@@ -202,6 +215,12 @@ parametrization (P_perp/P_tor/D_norm; the older magnetic/electron Prandtl
202215
- `dr_val`, `dgeo_val` -- inputs for the critical-Δ formula
203216
- `dc_type` -- one of `:none`, `:lar`, `:rfitzp`, `:toroidal`
204217
- `ising` -- singular-surface index for traceability
218+
- `k_ref` -- reference-length ratio K = r_s·(dψ_N/dr) at the surface,
219+
used by the Tearing runner to convert the ψ_N-referenced outer Δ' to
220+
the r_s-referenced convention this layer works in (default `1.0`,
221+
i.e. no conversion; `build_slayer_inputs` fills it from the equilibrium)
222+
- `mu_mercier` -- Mercier exponent μ = √(−D_I) for the same conversion
223+
(default `0.5`, the slab/cylindrical value at D_I = −1/4)
205224
206225
# Resistivity kwargs
207226
@@ -252,7 +271,9 @@ function slayer_parameters(;
252271
f_trap::Union{Real,Nothing}=nothing,
253272
nu_e_star::Union{Real,Nothing}=nothing,
254273
R_major_eff::Union{Real,Nothing}=nothing,
255-
lnLambda_form::Symbol=:nrl)
274+
lnLambda_form::Symbol=:nrl,
275+
k_ref::Real=1.0,
276+
mu_mercier::Real=0.5)
256277

257278
# Coulomb logarithm shared by the resistivity closure and τ_ee.
258279
lnLamb = coulomb_log_e(n_e, t_e; form=lnLambda_form)
@@ -358,6 +379,7 @@ function slayer_parameters(;
358379
rs=rs, R0=R0, bt=bt, sval_r=sval_r,
359380
dr_val=dr_val, dgeo_val=dgeo_val,
360381
eta=eta, d_beta=d_beta,
361-
dc_tmp=dc_tmp, dc_type=dc_type
382+
dc_tmp=dc_tmp, dc_type=dc_type,
383+
k_ref=k_ref, mu_mercier=mu_mercier
362384
)
363385
end

src/Tearing/Dispersion/SurfaceCoupling.jl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@ end
5353
surface_coupling(model::SLAYERModel, params::SLAYERParameters,
5454
dp_diag::Number; dc::Real=0.0) -> SurfaceCoupling
5555
56-
SLAYER convenience constructor. `scale` is set to `params.lu^(1/3)` so that
57-
the dimensionless Δ from `riccati_f` is mapped to outer ψ-units before
58-
subtraction from the Δ' diagonal. `tauk` is taken from `params.tauk` for use
59-
by `MultiSurfaceCoupling` Q rescaling.
56+
SLAYER convenience constructor. `scale` is set to `params.lu^(1/3)`, which
57+
maps the dimensionless inner-layer Δ from `riccati_f` to the r_s-referenced
58+
outer convention (S^(1/3) with S = τ_R/τ_H built on r_s; the Lundquist
59+
number, `dc`, and the layer all share the `x̂ = (r−r_s)/r_s` reference
60+
length). `dp_diag` must already be in that same r_s reference — the Tearing
61+
runner converts the ψ_N-referenced BVP Δ' via `delta_prime_to_rs_reference`
62+
before building couplings. `tauk` is taken from `params.tauk` for use by
63+
`MultiSurfaceCoupling` Q rescaling.
6064
"""
6165
function surface_coupling(model::SLAYERModel, params::SLAYERParameters,
6266
dp_diag::Number; dc::Real=0.0)

src/Tearing/Runner/HDF5Output.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ const TEARING_H5_ANNOTATIONS = [
9090
"PerSurface/d_beta" => (; long_name="β-weighted ion drift scale d_β", units="m", dims=("surface",)),
9191
"PerSurface/D_c_offset" => (; long_name="critical-Δ offset from χ_∥/χ_⊥ matching (Connor-Hastie-Helander 2015 Eq. 59)", dims=("surface",)),
9292
"PerSurface/D_c_type" => (; long_name="per-surface D_c prescription label", dims=("surface",)),
93+
"PerSurface/k_ref" => (; long_name="reference-length ratio K = r_s·(dψ_N/dr) at each surface", dims=("surface",)),
94+
"PerSurface/mu_mercier" => (; long_name="Mercier exponent μ = √(−D_I) at each surface", dims=("surface",)),
95+
"PerSurface/delta_prime_conversion" => (; long_name="ψ_N → r_s reference-length factor K^(2μ) applied to each Δ' diagonal", dims=("surface",)),
9396
"PerSurface/E" => (; long_name="Glasser-Greene-Johnson coefficient E per surface", dims=("surface",)),
9497
"PerSurface/F" => (; long_name="Glasser-Greene-Johnson coefficient F per surface", dims=("surface",)),
9598
"PerSurface/G" => (; long_name="Glasser-Greene-Johnson coefficient G per surface", dims=("surface",)),
@@ -98,7 +101,7 @@ const TEARING_H5_ANNOTATIONS = [
98101
"PerSurface/M" => (; long_name="Glasser-Greene-Johnson coefficient M per surface", dims=("surface",)),
99102
"PerSurface/tau_A" => (; long_name="Alfvén time τ_A per surface (GGJ layer parameters)", units="s", dims=("surface",)),
100103
"PerSurface/dVdpsi" => (; long_name="dV/dψ_N at each surface", units="m^3", dims=("surface",)),
101-
"PerSurface/Delta_prime_matrix" => (; long_name="full complex Δ' matrix coupling the rational surfaces", dims=("surface_row", "surface_col")),
104+
"PerSurface/Delta_prime_matrix" => (; long_name="full complex Δ' matrix coupling the rational surfaces, as used in the matching (SLAYER path: r_s-referenced via K^(2μ); the ψ_N-referenced BVP matrix is SingularSurfaces/Delta_prime_matrix)", dims=("surface_row", "surface_col")),
102105
"Roots/Q_root" => (; long_name="complex dispersion-root normalized frequency Q (NaN = no root)", dims=("surface",)),
103106
"Roots/omega" =>
104107
(; long_name="mode rotation angular frequency ω = Re(Q)/τ_k of each root", units="rad/s", dims=("surface",)),
@@ -179,6 +182,11 @@ function _write_per_surface!(g, params::AbstractVector{SLAYERParameters},
179182
ps["D_geo"] = Float64[p.dgeo_val for p in params]
180183
# Store dc_type per-surface as string array
181184
ps["D_c_type"] = String[String(p.dc_type) for p in params]
185+
# Reference-length conversion applied to Δ' (ψ_N → r_s): K, μ, and the
186+
# diagonal factor K^(2μ) actually multiplying each Δ'_kk.
187+
ps["k_ref"] = Float64[p.k_ref for p in params]
188+
ps["mu_mercier"] = Float64[p.mu_mercier for p in params]
189+
ps["delta_prime_conversion"] = Float64[p.k_ref^(2 * p.mu_mercier) for p in params]
182190

183191
ps["Delta_prime_matrix"] = dp_matrix
184192
return nothing

src/Tearing/Runner/Result.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ downstream inspection and HDF5 output.
1616
- `enabled` -- `true` only when the analysis actually ran
1717
- `control` -- the `SLAYERControl` used (frozen snapshot)
1818
- `params` -- `Vector{SLAYERParameters}`, one per surface
19-
- `dp_matrix` -- outer-region Δ' matrix used in the analysis
19+
- `dp_matrix` -- outer-region Δ' matrix used in the analysis.
20+
SLAYER path: r_s-referenced (the ψ_N BVP matrix transformed by
21+
`delta_prime_to_rs_reference`); GGJ path: the ψ_N matrix unchanged
2022
- `Q_root` -- tearing eigenvalue(s) in normalized Q
2123
* length `nsurfaces` in `:uncoupled` mode
2224
* length `1` in `:coupled` mode (global eigenvalue normalized by

src/Tearing/Runner/run_slayer.jl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,39 @@ function _build_surface_coupling(model::GGJModel, params::GGJParameters,
151151
return surface_coupling(model, params, dp_diag)
152152
end
153153

154+
# ---------------------------------------------------------------------
155+
# Reference-length conversion of the outer Δ' for the slab layer
156+
# ---------------------------------------------------------------------
157+
"""
158+
delta_prime_to_rs_reference(dp_matrix, params) -> Matrix{ComplexF64}
159+
160+
Convert the outer-region Δ' matrix from its ψ_N reference length (the
161+
STRIDE/BVP convention: Frobenius coefficients normalized per unit Δψ_N) to
162+
the r_s-based `x̂ = (r − r_s)/r_s` reference the slab layer works in
163+
(Fitzpatrick 2023 convention — the same reference used by `dc_tmp` and by
164+
the `S^(1/3)` Δ(Q) scale, both built on r_s).
165+
166+
Near surface `k` the tearing-parity outer solution is
167+
`A_L·|x|^(1/2−μ) + A_S·|x|^(1/2+μ)` with `μ = √(−D_I)` (Glasser, Wang &
168+
Park 2016, Eq. 26). Rescaling the radial variable `x_ψ = K·x̂` with
169+
`K = r_s·(dψ_N/dr)|_s` maps the coefficients as `Â_L = A_L·K^(1/2−μ)` and
170+
`Â_S = A_S·K^(1/2+μ)`, so the response matrix (small coefficient at surface
171+
`i` per unit large coefficient at surface `j`) transforms as
172+
173+
Δ̂_ij = K_i^(1/2+μ_i) · Δ'_ij · K_j^(μ_j−1/2)
174+
175+
whose diagonal is `K^(2μ)·Δ'_kk`; at D_I = −1/4 (μ = 1/2) this reduces to
176+
the textbook `Δ̂ = r_s·Δ'_phys`. `K` and `μ` are carried per surface in
177+
`SLAYERParameters.k_ref` / `.mu_mercier`; hand-built parameters default to
178+
`k_ref = 1`, making the conversion the identity.
179+
"""
180+
function delta_prime_to_rs_reference(dp_matrix::AbstractMatrix,
181+
params::AbstractVector)
182+
dl = [p.k_ref^(0.5 + p.mu_mercier) for p in params]
183+
dr = [p.k_ref^(p.mu_mercier - 0.5) for p in params]
184+
return Diagonal(dl) * Matrix{ComplexF64}(dp_matrix) * Diagonal(dr)
185+
end
186+
154187
# ---------------------------------------------------------------------
155188
# Core analysis entry point that takes pre-built parameters.
156189
# ---------------------------------------------------------------------
@@ -192,6 +225,14 @@ function run_slayer_from_inputs(params::AbstractVector{<:InnerLayerParameters},
192225
"(build_slayer_inputs for SLAYER, build_ggj_inputs for GGJ).")
193226
)
194227

228+
# Slab-layer path: convert Δ' from its ψ_N reference length to the
229+
# r_s-based convention shared by the layer Δ(Q) and the critical-Δ (see
230+
# `delta_prime_to_rs_reference`). GGJ is genuinely toroidal/ψ-based
231+
# (its `rescale_delta` handles inner→outer units natively) — no conversion.
232+
if !_is_ggj(model)
233+
dp = delta_prime_to_rs_reference(dp, params)
234+
end
235+
195236
# The coupled determinant uses the reduced m×m (tearing-only) form, which
196237
# drops the interchange channel. For GGJ that channel carries the Glasser
197238
# interchange stabilization, so coupled-GGJ results omit real physics.

test/runtests_slayer_runner.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,41 @@
111111
@test_throws ArgumentError run_slayer_from_inputs(params, bad_dp, c)
112112
end
113113

114+
@testset "delta_prime_to_rs_reference: ψ_N → r_s conversion" begin
115+
# Two surfaces with distinct K and μ; the transform is
116+
# Δ̂_ij = K_i^(1/2+μ_i) · Δ'_ij · K_j^(μ_j−1/2).
117+
K1, mu1 = 0.9, 0.54
118+
K2, mu2 = 1.2, 0.60
119+
p1 = SLAYERParameters(; tau=1.0, lu=1e7, c_beta=0.1, D_norm=2.0,
120+
P_perp=20.0, P_tor=10.0, Q_e=-1.0, Q_i=0.5, iota_e=2 / 3,
121+
tauk=1e-4, tau_r=1.0, delta_n=1.0, rs=0.4, R0=1.7, bt=2.0,
122+
sval_r=1.0, eta=2.5e-8, d_beta=4e-3, m=2, n=1, ising=1,
123+
k_ref=K1, mu_mercier=mu1)
124+
p2 = SLAYERParameters(; tau=1.0, lu=1e7, c_beta=0.1, D_norm=2.0,
125+
P_perp=20.0, P_tor=10.0, Q_e=-1.0, Q_i=0.5, iota_e=2 / 3,
126+
tauk=1e-4, tau_r=1.0, delta_n=1.0, rs=0.5, R0=1.7, bt=2.0,
127+
sval_r=1.0, eta=2.5e-8, d_beta=4e-3, m=3, n=1, ising=2,
128+
k_ref=K2, mu_mercier=mu2)
129+
dp = ComplexF64[10.0+1im 2.0-0.5im; 3.0+0im 1.5+2im]
130+
out = Runner.delta_prime_to_rs_reference(dp, [p1, p2])
131+
# Diagonal carries the scalar K^(2μ)
132+
@test out[1, 1] K1^(2mu1) * dp[1, 1]
133+
@test out[2, 2] K2^(2mu2) * dp[2, 2]
134+
# Off-diagonals carry the split row/column factors
135+
@test out[1, 2] K1^(0.5 + mu1) * K2^(mu2 - 0.5) * dp[1, 2]
136+
@test out[2, 1] K2^(0.5 + mu2) * K1^(mu1 - 0.5) * dp[2, 1]
137+
# At the slab point μ = 1/2 the diagonal factor is exactly K
138+
pslab = SLAYERParameters(; tau=1.0, lu=1e7, c_beta=0.1, D_norm=2.0,
139+
P_perp=20.0, P_tor=10.0, Q_e=-1.0, Q_i=0.5, iota_e=2 / 3,
140+
tauk=1e-4, tau_r=1.0, delta_n=1.0, rs=0.4, R0=1.7, bt=2.0,
141+
sval_r=1.0, eta=2.5e-8, d_beta=4e-3, m=2, n=1, ising=1,
142+
k_ref=0.8, mu_mercier=0.5)
143+
dp1 = ComplexF64[5.0+0im;;]
144+
@test Runner.delta_prime_to_rs_reference(dp1, [pslab])[1, 1] 0.8 * 5.0
145+
# Default parameters (k_ref = 1) give the identity regardless of μ
146+
@test Runner.delta_prime_to_rs_reference(dp, [_mk_params(), _mk_params()]) dp
147+
end
148+
114149
@testset "run_slayer_from_inputs: coupled mode finds known root" begin
115150
# Build a 2-surface problem with a known coupled root by construction.
116151
p1 = _mk_params(; rs=0.5, lu=1.0e7, tauk=1.0e-4, Q_e=-1.0, Q_i=0.5,

0 commit comments

Comments
 (0)