Skip to content

Commit 5e646b3

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 c42d558 commit 5e646b3

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
@@ -95,6 +95,9 @@ const TEARING_H5_ANNOTATIONS = [
9595
"PerSurface/d_beta" => (; long_name="β-weighted ion drift scale d_β", units="m", dims=("surface",)),
9696
"PerSurface/D_c_offset" => (; long_name="critical-Δ offset from χ_∥/χ_⊥ matching (Connor-Hastie-Helander 2015 Eq. 59)", dims=("surface",)),
9797
"PerSurface/D_c_type" => (; long_name="per-surface D_c prescription label", dims=("surface",)),
98+
"PerSurface/k_ref" => (; long_name="reference-length ratio K = r_s·(dψ_N/dr) at each surface", dims=("surface",)),
99+
"PerSurface/mu_mercier" => (; long_name="Mercier exponent μ = √(−D_I) at each surface", dims=("surface",)),
100+
"PerSurface/delta_prime_conversion" => (; long_name="ψ_N → r_s reference-length factor K^(2μ) applied to each Δ' diagonal", dims=("surface",)),
98101
"PerSurface/E" => (; long_name="Glasser-Greene-Johnson coefficient E per surface", dims=("surface",)),
99102
"PerSurface/F" => (; long_name="Glasser-Greene-Johnson coefficient F per surface", dims=("surface",)),
100103
"PerSurface/G" => (; long_name="Glasser-Greene-Johnson coefficient G per surface", dims=("surface",)),
@@ -103,7 +106,7 @@ const TEARING_H5_ANNOTATIONS = [
103106
"PerSurface/M" => (; long_name="Glasser-Greene-Johnson coefficient M per surface", dims=("surface",)),
104107
"PerSurface/tau_A" => (; long_name="Alfvén time τ_A per surface (GGJ layer parameters)", units="s", dims=("surface",)),
105108
"PerSurface/dVdpsi" => (; long_name="dV/dψ_N at each surface", units="m^3", dims=("surface",)),
106-
"PerSurface/Delta_prime_matrix" => (; long_name="full complex Δ' matrix coupling the rational surfaces", dims=("surface_row", "surface_col")),
109+
"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")),
107110
"Roots/Q_root" => (; long_name="complex dispersion-root normalized frequency Q (NaN = no root)", dims=("surface",)),
108111
"Roots/omega" =>
109112
(; long_name="mode rotation angular frequency ω = Re(Q)/τ_k of each root", units="rad/s", dims=("surface",)),
@@ -184,6 +187,11 @@ function _write_per_surface!(g, params::AbstractVector{SLAYERParameters},
184187
ps["D_geo"] = Float64[p.dgeo_val for p in params]
185188
# Store dc_type per-surface as string array
186189
ps["D_c_type"] = String[String(p.dc_type) for p in params]
190+
# Reference-length conversion applied to Δ' (ψ_N → r_s): K, μ, and the
191+
# diagonal factor K^(2μ) actually multiplying each Δ'_kk.
192+
ps["k_ref"] = Float64[p.k_ref for p in params]
193+
ps["mu_mercier"] = Float64[p.mu_mercier for p in params]
194+
ps["delta_prime_conversion"] = Float64[p.k_ref^(2 * p.mu_mercier) for p in params]
187195

188196
ps["Delta_prime_matrix"] = dp_matrix
189197
return nothing

src/Tearing/Runner/Result.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ downstream inspection and HDF5 output.
2020
factor q of each analyzed surface, aligned with `params`. Empty when the
2121
analysis was built from bare parameters (`run_slayer_from_inputs` without
2222
the surface list), in which case the HDF5 writer skips them.
23-
- `dp_matrix` -- outer-region Δ' matrix used in the analysis
23+
- `dp_matrix` -- outer-region Δ' matrix used in the analysis.
24+
SLAYER path: r_s-referenced (the ψ_N BVP matrix transformed by
25+
`delta_prime_to_rs_reference`); GGJ path: the ψ_N matrix unchanged
2426
- `Q_root` -- tearing eigenvalue(s) in normalized Q
2527
* length `nsurfaces` in `:uncoupled` mode
2628
* 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
# ---------------------------------------------------------------------
@@ -194,6 +227,14 @@ function run_slayer_from_inputs(params::AbstractVector{<:InnerLayerParameters},
194227
"(build_slayer_inputs for SLAYER, build_ggj_inputs for GGJ).")
195228
)
196229

230+
# Slab-layer path: convert Δ' from its ψ_N reference length to the
231+
# r_s-based convention shared by the layer Δ(Q) and the critical-Δ (see
232+
# `delta_prime_to_rs_reference`). GGJ is genuinely toroidal/ψ-based
233+
# (its `rescale_delta` handles inner→outer units natively) — no conversion.
234+
if !_is_ggj(model)
235+
dp = delta_prime_to_rs_reference(dp, params)
236+
end
237+
197238
# The coupled determinant uses the reduced m×m (tearing-only) form, which
198239
# drops the interchange channel. For GGJ that channel carries the Glasser
199240
# 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
@@ -153,6 +153,41 @@
153153
@test_throws ArgumentError run_slayer_from_inputs(params, bad_dp, c)
154154
end
155155

156+
@testset "delta_prime_to_rs_reference: ψ_N → r_s conversion" begin
157+
# Two surfaces with distinct K and μ; the transform is
158+
# Δ̂_ij = K_i^(1/2+μ_i) · Δ'_ij · K_j^(μ_j−1/2).
159+
K1, mu1 = 0.9, 0.54
160+
K2, mu2 = 1.2, 0.60
161+
p1 = SLAYERParameters(; tau=1.0, lu=1e7, c_beta=0.1, D_norm=2.0,
162+
P_perp=20.0, P_tor=10.0, Q_e=-1.0, Q_i=0.5, iota_e=2 / 3,
163+
tauk=1e-4, tau_r=1.0, delta_n=1.0, rs=0.4, R0=1.7, bt=2.0,
164+
sval_r=1.0, eta=2.5e-8, d_beta=4e-3, m=2, n=1, ising=1,
165+
k_ref=K1, mu_mercier=mu1)
166+
p2 = SLAYERParameters(; tau=1.0, lu=1e7, c_beta=0.1, D_norm=2.0,
167+
P_perp=20.0, P_tor=10.0, Q_e=-1.0, Q_i=0.5, iota_e=2 / 3,
168+
tauk=1e-4, tau_r=1.0, delta_n=1.0, rs=0.5, R0=1.7, bt=2.0,
169+
sval_r=1.0, eta=2.5e-8, d_beta=4e-3, m=3, n=1, ising=2,
170+
k_ref=K2, mu_mercier=mu2)
171+
dp = ComplexF64[10.0+1im 2.0-0.5im; 3.0+0im 1.5+2im]
172+
out = Runner.delta_prime_to_rs_reference(dp, [p1, p2])
173+
# Diagonal carries the scalar K^(2μ)
174+
@test out[1, 1] K1^(2mu1) * dp[1, 1]
175+
@test out[2, 2] K2^(2mu2) * dp[2, 2]
176+
# Off-diagonals carry the split row/column factors
177+
@test out[1, 2] K1^(0.5 + mu1) * K2^(mu2 - 0.5) * dp[1, 2]
178+
@test out[2, 1] K2^(0.5 + mu2) * K1^(mu1 - 0.5) * dp[2, 1]
179+
# At the slab point μ = 1/2 the diagonal factor is exactly K
180+
pslab = SLAYERParameters(; tau=1.0, lu=1e7, c_beta=0.1, D_norm=2.0,
181+
P_perp=20.0, P_tor=10.0, Q_e=-1.0, Q_i=0.5, iota_e=2 / 3,
182+
tauk=1e-4, tau_r=1.0, delta_n=1.0, rs=0.4, R0=1.7, bt=2.0,
183+
sval_r=1.0, eta=2.5e-8, d_beta=4e-3, m=2, n=1, ising=1,
184+
k_ref=0.8, mu_mercier=0.5)
185+
dp1 = ComplexF64[5.0+0im;;]
186+
@test Runner.delta_prime_to_rs_reference(dp1, [pslab])[1, 1] 0.8 * 5.0
187+
# Default parameters (k_ref = 1) give the identity regardless of μ
188+
@test Runner.delta_prime_to_rs_reference(dp, [_mk_params(), _mk_params()]) dp
189+
end
190+
156191
@testset "run_slayer_from_inputs: coupled mode finds known root" begin
157192
# Build a 2-surface problem with a known coupled root by construction.
158193
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)