Tearing Module
The Tearing module groups the resistive tearing-mode analysis stack: InnerLayer (per-surface inner-layer matching data Δ(Q) for the GGJ and SLAYER models), Dispersion (physics-agnostic complex-plane scan and contour-intersection root extraction), and Runner (user-facing TOML configuration, profile loading, and HDF5 output).
Layer Inputs
Equilibrium/ForceFreeStates glue that assembles per-surface inner-layer inputs.
GeneralizedPerturbedEquilibrium.Tearing.build_ggj_inputs — Method
build_ggj_inputs(equil, sings, profiles; mu_i=2.0, zeff=1.0,
v1_scale=1.0,
resistivity_model::NeoResistivityModel=SpitzerModel(),
lnLambda_form::Symbol=:nrl) -> Vector{GGJParameters}Construct a GGJParameters for each rational surface in sings. Each surface's geometric coefficients (E, F, G, H, K, M) come from the sing.restype::ResistGeometry populated by resist_eval_all!. Kinetic timescales are derived from the KineticProfiles at sing.psifac:
ρ(ψ) = μ_i · m_p · n_e(ψ)
η(ψ) = eta_neoclassical(model, n_e, T_e, Z_eff, f_t, ν*_e) [Ω·m]
τ_A = √(ρ · M · μ_0) / |2π · n · q' · χ₁ / V'| [Alfvén time]
τ_R = (⟨B²/|∇ψ|²⟩ / ⟨B²⟩) · μ_0 / η [resistive diffusion]The mode number n is taken from sings[k].n[1] (first resonant mode at the surface). χ₁ = 2π · psio. The v1_scale kwarg is an optional multiplicative factor on V' in the τ_A denominator (a v1 / volume normalization option); default 1.0 means use the raw V'.
Resistivity model
resistivity_model selects the η closure:
SpitzerModel()(default) — Sauter 1999 Eq. 18a (Zeff-aware Spitzer), with the NRL Coulomb log.SauterNeoModel()— multiplies by Sauter 1999 F33 using ft and ν*_e from the surface'sResistGeometry. Produces the physically-correct trapped-particle-corrected η for H-mode tearing stability.RedlNeoModel()— Redl 2021 F_33 (improved high-ν* fit).
lnLambda_form selects :nrl (default), :sauter, or :wesson.
Throws if any surface's restype is still nothing — call ForceFreeStates.resist_eval_all!(intr, equil) first.
The module provides an abstract InnerLayerModel interface and two concrete models: the GGJ (Glasser–Greene–Johnson) layer documented below, with three interchangeable solver backends, and the pressureless SLAYER Fitzpatrick Riccati layer. Further inner-layer models (kinetic layers) plug in through the same interface.
Two source papers define the equations and the asymptotic construction, and are cited by their equation numbers throughout the code and below:
- GWP2016 — A. H. Glasser, Z. R. Wang and J.-K. Park, Computation of resistive instabilities by matched asymptotic expansions, Phys. Plasmas 23, 112506 (2016).
- GW2020 — A. H. Glasser and Z. R. Wang, Asymptotic solutions and convergence studies of the resistive inner region equations, Phys. Plasmas 27, 012506 (2020).
Governing equations
At a rational surface the layer equations couple three fields — the perturbed flux $\Psi$ and two auxiliary layer variables $\Xi$ and $\Upsilon$ — as functions of the scaled distance $x$ from the surface. In the GWP2016 form (Eq. 11 ≡ GW2020 Eq. 1) they are
\[\begin{aligned} \Psi'' &= H\,\Upsilon' + Q\,(\Psi - x\,\Xi),\\[2pt] \Xi'' &= \frac{1}{Q^{2}}\Big[\,Q x^{2}\,\Xi - Q x\,\Psi - (E+F)\,\Upsilon - H\,\Psi'\,\Big],\\[2pt] \Upsilon''&= \frac{1}{Q}\Big[\,x^{2}\,\Upsilon - x\,\Psi - Q^{2}\big(G(\Xi-\Upsilon) - K(E\Xi + F\Upsilon + H\Psi')\big)\Big], \end{aligned}\]
where $E, F, G, H, K$ are the flux-surface-averaged equilibrium coefficients of GWP2016 Eq. (A8) and $Q$ is the dimensionless growth rate defined below. These coefficients are collected in GGJParameters.
Dimensionless scales
The layer is scaled by the local Lundquist number $S = \tau_R/\tau_A$ (ratio of resistive to Alfvén time). The displacement and growth-rate scales are
\[X_0 = S^{-1/3}, \qquad Q_0 = X_0/\tau_A \qquad \text{(GWP2016 Eqs. A14–A15)},\]
so a physical growth rate $\gamma$ maps to the scaled eigenvalue $Q = \gamma/Q_0$ (inner_Q) that appears in the equations above, and the scaled matching data are returned to physical units by the $X_0^{-2\sqrt{-D_I}} = S^{\,2\sqrt{-D_I}/3}$ rescaling (together with the $v_1^{\,2\sqrt{-D_I}}$ linear-scale factor; rescale_delta).
Mercier index and matching exponents
The premise of an inner-layer model is local Mercier stability. The Mercier interchange index and its resistive counterpart are
\[D_I = E + F + H - \tfrac14, \qquad D_R = E + F + H^2 = D_I + \big(H-\tfrac12\big)^2 \qquad \text{(GWP2016 A9–A10)},\]
(mercier_di, mercier_dr). For $D_I < 0$ the large-$x$ solutions are the two power laws $x^{r_\pm}$ with the Frobenius exponents
\[r_\pm = \tfrac32 \pm \sqrt{-D_I}, \qquad p_1 \equiv \sqrt{-D_I} \qquad \text{(GW2020 Eq. 49)}.\]
The matching datum $\Delta$ is the amplitude of the large solution $x^{r_+}$ when the small solution $x^{r_-}$ is normalized to unity (i.e. the ratio of large to small coefficient), in the physical (outer-matching) normalization. Imposing the two parities at $x=0$ (odd: $\Psi'(0)=\Xi(0)=\Upsilon(0)=0$; even: $\Psi(0)=\Xi'(0)=\Upsilon'(0)=0$) gives the pair $(\Delta_\mathrm{odd}, \Delta_\mathrm{even})$ of GWP2016 Eqs. (34)–(35).
Numerical method
Every figure on this page is computed on the $q = 4$ rational-surface benchmark (q4_surface_benchmark) — a fixed set of GGJ layer coefficients ($S = \tau_R/\tau_A \approx 4.6\times10^{6}$, $D_I \approx -0.31$) frozen into code from the $q = 4$ rational surface of the DIII-D-like (TkMkr H-mode) equilibrium of the resistive_resmets benchmark, at that case's per-surface $\eta$ and $\rho$. The well-conditioned real-$Q$ cross-check in the Validation section instead uses the Glasser & Wang (2020) Eq. (55) surface.
Solver backends
The GGJ model exposes three solvers through the solver type parameter of GGJModel; all consume the same large-$x$ asymptotic basis and return $\Delta$ in the same convention:
| backend | method | robust regime |
|---|---|---|
:shooting | backward stable shoot from $X_\mathrm{max}\to 0$ | $\lvert Q\rvert \ll 1$ |
:galerkin | Hermite-cubic finite-element (real axis) | drift onset $\lvert Q\rvert \sim 1$; 1% error by $\lvert Q\rvert \sim 4$ |
:ray (default) | rotated-contour spectral-element collocation | $\lvert Q\rvert \sim 500$ on/near the imaginary axis |
The difficulty is the large-$|Q|$ imaginary-$Q$ axis itself, where rotation and resistivity scans live: there the layer's pseudo-resonance at $x^2 = -Q^2(G+KF)$ sits directly on the real-$x$ integration path and the exponential dichotomy weakens, so both older backends degrade — :shooting through the dichotomy of the backward shoot, :galerkin through real-axis oscillation and the on-axis pseudo-resonance. (The poles of $\Delta(Q)$ itself lie on and near the real-$Q$ axis — they are the layer eigenvalues; $\Delta$ is smooth along the imaginary axis.) Measured against the :ray reference along the imaginary axis, :shooting holds to $|Q|\sim 1$ and :galerkin to $|Q|\sim 4$ (the 1% error crossings) before both lose all accuracy:

The :ray backend was written to reach the large-$|Q|$ imaginary-axis regime (rotation and resistivity scans) where these fail. The remainder of this section describes it.
Entire-solution formulation
:ray works with the plain state $v = (\Psi, \Xi, \Upsilon, \Psi', \Xi', \Upsilon')$ and writes the layer equations as the first-order system
\[\frac{dv}{dx} = M(x)\,v .\]
The coefficient matrix (the GGJ internal ode_matrix) is polynomial in $x$, so $x = 0$ is an ordinary point — the $x^{-2}/x^{-4}$ singularities of the GW2020 Eq. (2) scaled form $(x\Psi,\ \Psi'/x,\ \dots)$ are artifacts of that scaling, not of the equations. Because the coefficients are entire, the system continues analytically to complex $x$, which is what makes the contour rotation below legitimate.
The rotated ray
The equations are continued onto the ray
\[x = e^{i\theta}\, s, \qquad s \in [0, S], \qquad \theta = \tfrac14\arg Q,\]
The angle $\theta = \tfrac14\arg Q$ makes the parabolic-cylinder exponent of the outer solutions exactly real and clears the pseudo-resonance at $x^2 = -Q^2(G + K F)$, which on the imaginary-$Q$ axis is real and large and therefore sits directly on the un-rotated (real-$x$) contour. Rotating the contour lifts it off that point; $\theta = 0$ recovers a real-axis solve.

Spectral-element collocation BVP
On $[0, s_m]$ the system is discretized by a global Chebyshev spectral-element collocation boundary-value problem: right-biased (Radau-like) collocation at the Chebyshev–Lobatto nodes of each cell, three parity conditions at the ordinary-point origin $s = 0$, and six matching conditions at the outer edge,
\[v(S) - \Delta\, U_b - c_1 E_1 - c_2 E_2 = U_s ,\]
with the matching datum $\Delta$ and the two decaying-mode amplitudes $c_1, c_2$ carried as bordered unknowns. Here $U_s, U_b$ are the small and large power-like solutions and $E_{1,2}$ the forward-decaying exponential pair. No quantity is ever propagated across the layer, so the exponential dichotomy that limits the shooting backend never enters; the boundary condition splits it exactly.
The two parities differ only in their three $s=0$ rows, i.e. by a rank-3 update, so both are obtained from a single sparse LU factorization plus a Woodbury correction (the GGJ internal _solve_parities) rather than two factorizations. A residual-driven bisection refinement adds cells until the collocation residual meets tolerance, with a roundoff-plateau guard.
Far-field boundary and the inward march
The large-$x$ boundary data use the same inps Wasow asymptotic basis as the other backends (GW2020 Eqs. 3–52), evaluated at complex $x$ by RayAsymptotics.jl and applied at the series radius $S$ where the series is trusted (residual below tolerance; pick_smax). For large $|Q|$ the trusted radius $S$ can be far outside the collocation domain $s_m$, so the power-pair data are transported inward from $S$ to $s_m$ by an L-stable 2-stage Radau IIA march in the quotient modulo the decaying exponential pair — the subspace in which $\Delta$ is defined (the GGJ internal march_boundary). An L-stable implicit method is essential: the decaying pair grows under backward integration, so any explicit marcher is stability-limited to $O(\rho S^2)$ steps, while the Radau march damps the unresolvable backward-growing directions instead of amplifying them.
The damped-zone march runs in Complex{Double64} extended precision. At large $S$ the near-parallel power-pair geometry amplifies the structured backward error of the ill-conditioned implicit solves into $\Delta$-mixing of order $10^{-4}$ at $|Q| = 500$ in Float64; extended precision removes this floor, while the well-conditioned resolved band stays in Float64.
The result is a seamless numeric↔asymptotic solution: the collocation solution on $[0, s_m]$ and the analytic $u_\mathrm{small} + \Delta\,u_\mathrm{big}$ representation for $s \ge S$ share the same power-law tail — the overlap the outer-region matching relies on.

Validation and benchmarks
Cross-check against the Fortran rmatch solver. At the Glasser & Wang (2020) Eq. (55) operating point $Q = 0.1234$ (real) the :galerkin backend reproduces the Fortran rmatch deltac solver to $\sim 10^{-8}$:
| quantity | :galerkin (= Fortran to $10^{-8}$) | :ray |
|---|---|---|
| $\Delta_\mathrm{odd}$ | $3.698368\times 10^{4}$ | $3.69789\times 10^{4}$ |
| $\Delta_\mathrm{even}$ | $14.759721$ | $14.759715$ |
The large $\lvert\Delta_\mathrm{odd}\rvert \sim 4\times10^{4}$ means this operating point sits near a pole of $\Delta(Q)$, where every solver's error is amplified by the pole geometry; the $1.3\times10^{-4}$ :galerkin↔:ray difference in $\Delta_\mathrm{odd}$ (versus $4\times10^{-7}$ in $\Delta_\mathrm{even}$) is consistent with that amplification, not a defect of either backend.
Physical benchmark on the imaginary axis. On the $q=4$ rational-surface benchmark (q4_surface_benchmark, $S \approx 4.58\times10^6$, $D_I \approx -0.312$) the :ray backend is pinned at $Q = 500i$, a regime entirely beyond :galerkin:
| quantity | value at $Q = 500i$ |
|---|---|
| $\Delta_\mathrm{odd}$ | $2.4720 + 13.3540\,i$ |
| $\Delta_\mathrm{even}$ | $0.13750 + 0.74275\,i$ |
Convergence and contour invariance. Because $\Delta$ is an analytic invariant of the contour angle, re-solving with each numerical knob perturbed on an independent axis (delta_convergence) gives an honest error bar: at $Q = 500i$ the worst-case spread across all knobs is $\sim 5\times10^{-6}$ for $\Delta_\mathrm{odd}$ and $\sim 6\times10^{-7}$ for $\Delta_\mathrm{even}$. That is a single-machine error bar: across machines/BLAS builds the absolute values reproduce to $\sim 10^{-5}$ relative (the damped-zone implicit solves carry platform-dependent structured roundoff), which is why the table above quotes five significant figures.

Choosing a backend
:ray is the default — GGJModel() constructs GGJModel{:ray}(). It is the correct choice for $|Q| \gtrsim 1$ and for any $Q$ near the imaginary axis. :galerkin remains available and may be faster for very small real $|Q|$, but note the backends take disjoint numerical-knob keywords: pass GGJModel(solver=:galerkin) explicitly when supplying the Galerkin nx/xfac knobs — passing them to the default :ray backend throws a MethodError.
using GeneralizedPerturbedEquilibrium.InnerLayer
p = q4_surface_benchmark() # GGJParameters for the q=4 benchmark surface
γ = 500im * GGJ.q0(p) # physical rate placing Q on the imaginary axis at 500i
Δ = solve_inner(GGJModel(), p, γ) # (Δ_odd, Δ_even) with the default :ray backend
# Full result (raw Δ, contour, mesh, nodal solution, residuals) via solve_ray:
res = solve_ray(p, GGJ.inner_Q(p, γ))
res.Δ, res.resid, res.bc_condAPI Reference
InnerLayer
GeneralizedPerturbedEquilibrium.InnerLayer.InnerLayerModel — Type
InnerLayerModelAbstract supertype for resistive inner-layer models. Each concrete model is a small, parameter-free type tag (often parameterized by a solver-choice symbol) that selects a solve_inner method.
Implementations live in submodules of InnerLayer, e.g. InnerLayer.GGJ.
GeneralizedPerturbedEquilibrium.InnerLayer.InnerLayerParameters — Type
InnerLayerParametersAbstract supertype for the per-surface physical-parameter structs consumed by the inner-layer models (SLAYERParameters, GGJParameters). Lets the dispersion runner and HDF5 output dispatch generically over a vector of single-model parameters.
GeneralizedPerturbedEquilibrium.InnerLayer.InnerLayerResponse — Type
InnerLayerResponseParity-projected inner-layer matching data at one rational surface. The two components correspond to the homogeneous parity solutions of the half-domain inner-layer problem (parity boundary conditions imposed at X = 0). They are the Δ_{j,±}(γ) of Glasser, Wang & Park, Phys. Plasmas 23, 112506 (2016), Eqs. (34)–(35).
Fields
tearing— the odd-parity matching coefficient (GWP Δ_+, the "odd mode"). Corresponds to a flux perturbation W that is EVEN in x and a velocity/temperature perturbation that is ODD — i.e., the reconnecting mode with a current sheet at the rational surface. This is the tearing drive that appears as Δ' in the classical constant-ψ tearing equation. Must be populated by every resistive inner-layer model.interchange— the even-parity matching coefficient (GWP Δ_−, the "even mode"). Corresponds to W odd, N and Θ even — i.e., the non-reconnecting interchange/ballooning channel. Its dissipative piece in toroidal geometry is the Glasser, Greene & Johnson stabilization term that opposes tearing growth (Glasser, Greene & Johnson 1975; Lütjens-Bondeson-Roy 1993). Pressureless inner-layer models (e.g. SLAYER's Fitzpatrick Riccati) set this identically zero.
The naming follows the physics channel rather than a mathematical parity label because odd/even carries different meanings across the literature depending on whether you label by the parity of W (GWP paper convention) or the parity of (N, Θ). Using tearing and interchange avoids ambiguity.
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner — Function
solve_inner(model::InnerLayerModel, params, γ::Number; kwargs...) -> InnerLayerResponseCompute the parity-projected matching data (Δ_tearing, Δ_interchange) for the given inner-layer model, physical parameters params, and complex growth rate γ. Concrete models specialize this function.
See InnerLayerResponse for the physics-oriented field definitions. Pressureless models (SLAYER) populate only tearing and leave interchange at zero; two-fluid / finite-β models (GGJ) populate both.
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner_profile — Function
solve_inner_profile(model::InnerLayerModel, params, γ::Number; kwargs...)
-> (; Δ, x, Ψ, Ξ, dψdx, rescale, ...)Compute the inner-layer matching data and the reconstructed layer field profiles for the given model — everything an outer↔inner matching driver needs from the layer, so drivers never touch model internals. Returns a named tuple with at least:
Δ— the same(Δ_odd, Δ_even)matching data assolve_innerx— real ascending grid in the model's stretched inner coordinate,x ≥ 0with the rational surface atx = 0Ψ,Ξ—length(x) × 2profiles, columns (odd, even) parity, in the model's inner normalization:Ψthe normal-field (reconnected-flux) variable,Ξthe displacementdψdx— conversion to poloidal-flux distance,δψ = dψdx · xrescale— amplitude factor converting the inner-normalized profiles to the outer δψ-normalized convention (companion of the Δ rescale)
Concrete models may return additional diagnostic fields (e.g. a solve-quality certificate). Solver-knob keywords are model-specific.
GGJ
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.GGJModel — Type
GGJModel{S} <: InnerLayerModelGlasser–Greene–Johnson resistive inner-layer model. S selects the solver backend: :ray (default; robust at large |Q| on/near the imaginary axis), :galerkin (real-axis Hermite FEM; degrades for |Q| ≳ 1), or :shooting (|Q| ≪ 1 only). The backends take different numerical-knob keywords.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.GGJParameters — Type
GGJParametersDimensionless parameters of the Glasser–Greene–Johnson inner-layer model at a single rational surface, plus the local Alfvén/resistive timescales needed to scale the matching data back to physical Δ. The equilibrium coefficients E, F, G, H, K, M are the flux-surface averages defined in GWP2016 Eq. (A8); they enter the inner-region equations (Eq. 11).
Fields:
| field | meaning |
|---|---|
E | Glasser interchange parameter (enters Mercier D_I = E+F+H−¼) |
F | Glasser interchange parameter |
G | Coupling coefficient (curvature × pressure gradient) |
H | Pfirsch–Schlüter coefficient |
K | Glasser parameter |
M | Mercier-related auxiliary parameter (held but not used here) |
taua | Local Alfvén time at the rational surface |
taur | Local resistive time at the rational surface |
v1 | Linear scale factor used in the V₁ rescaling |
ising | Index of the singular surface (traceability only) |
The growth rate γ is not stored here; it is a separate argument to solve_inner.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.GGJShootingSystem — Type
GGJShootingSystemPrecomputed origin- and infinity-side arrays for the backward shoot. Construct via _build_shooting_system.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.InnerAsymptoticsCache — Type
InnerAsymptoticsCacheFrozen inps Wasow asymptotic-basis construction for one (GGJParameters, Q) pair, stored as SMatrix/SVector for allocation-free evaluation. Index convention: P[k+1] holds the k-th-order matrix P_k, etc.
Fields:
params, Q, kmax— input parameters and series truncation order.λ = 1/√Q— complex scale factor used by the Wasow split.R = (r₊, r₋)— Mercier-shifted Frobenius exponents at infinity (Eq. 49).T, Tinv— 6×6 eigenvector basis of A₀ (Eq. 7–8).J—(J₀, J₁, J₂), the J-rotated coefficient matrices (Eq. 9–10).P, B— splitting matrices, k = 0..kmax+2 (Eqs. 16, 22).K2— 2×2 inner working matrices, k = 0..kmax+2 (Eq. 32; entry k=0 unused).Qmat, Cmat— 2×2 inner-block transformation matrices, k = 0..kmax+2 (Eqs. 32–39).Dmat— 2×2 shearing matrices, k = 0..kmax (Eq. 43).Y0, Y0inv— lowest-order Y matrix and its inverse (Eq. 48; exponents from Eq. 49).Y, Z— 2×2 series matrices, k = 0..kmax (Eq. 52).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.RaySolveResult — Type
RaySolveResultOutput of solve_ray. Δ is the rescaled matching data in the deltac output ordering (index 1 ↔ even-BC solution, 2 ↔ odd-BC solution, i.e. the same swap deltacsolve applies); Δraw is (isol=1, isol=2) in raw inps normalization before `rescaledelta`.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ._build_shooting_system — Method
_build_shooting_system(p::GGJParameters, Q::ComplexF64; nps=8, rtol=1e-6, fmax=1.0)Construct a GGJShootingSystem for the given parameters p and complex frequency Q, precomputing the origin and infinity asymptotic arrays used by the forward/backward shoots.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ._physical_ua_dua — Method
Convert the inps 6×2 output U_inps (the Wasow asymptotic basis U = TPQSY of GW2020 Eq. 53) at coordinate x to the physical (Ψ, Ξ, Υ) and (Ψ', Ξ', Υ') representation. The 6-component first-order state packs (Ψ, Ξ, Υ, Ψ', Ξ', Υ') with the GW2020 Eq. (2) scaling 𝚿 ≡ (xΨ, Ξ, Υ), hence the /x and ·x factors below. Returns (ua, dua) each 3×2 complex, where columns are the two power-like (Mercier) solutions and rows are the components (Ψ, Ξ, Υ).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ._physical_uv — Method
Build the (I, U, V) coefficient matrices of the second-order system I·u'' − V·u' − U·u = 0 for u = (Ψ, Ξ, Υ) at coordinate x. All matrices are 3×3 complex.
These are the matrices A, B, C of GWP2016 Eq. (12), A Ψ'' + B Ψ' + C Ψ = 0, with A given in Eq. (14), B in Eq. (14), and C in Eq. (15). The code's weak-form layout flips the off-diagonal signs: (I, V, U) = (A, −B, −C).
Each matrix row is one GGJ inner-region equation (GWP2016 Eq. 11 ≡ GW2020 Eq. 1); reading I u'' − V u' − U u across a row reproduces the corresponding equation:
row 1 (Ψ): Ψxx − H Υx − Q(Ψ − x Ξ) = 0 ⇒ I=(1,0,0) V=(0,0,H) U=(Q, −Qx, 0) row 2 (Ξ): Q² Ξxx − Q x² Ξ + Q x Ψ + (E+F) Υ + H Ψx = 0 ⇒ I=(0,Q²,0) V=(−H,0,0) U=(−Qx, Q x², −(E+F)) row 3 (Υ): Q Υxx − x² Υ + x Ψ + Q²[G(Ξ−Υ) − K(E Ξ + F Υ + H Ψx)] = 0 ⇒ I=(0,0,Q) V=(K Q² H,0,0) U=(−x, −Q²(G−KE), x²+Q²(G+KF))
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ._solve_parities — Method
_solve_parities(Ic, Jc, Vc, rhs, ndof, N, p) -> (sol_odd, sol_even)Solve both parity systems from one factorization: the two matrices differ only in the 3 parity rows at s = 0 (a rank-3 update), so one sparse LU of the row-equilibrated odd system plus a Woodbury correction replaces the second factorization, which dominates the linear-algebra cost.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.asymptotic_profile — Method
asymptotic_profile(params, res::RaySolveResult, srange) -> (; s, x, Ψ, Ξ, Υ)Evaluate the analytic representation u_small + Δ·u_big on the ray for s ≥ res.S; concatenated with solution_profile it should join the numeric solution seamlessly across the march zone.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.asymptotic_residual — Method
asymptotic_residual(cache, x::Complex) -> SVector{2,Float64}GW2020 Eq. (54) residual of the two series columns at complex x.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.asymptotic_residual — Method
asymptotic_residual(cache::InnerAsymptoticsCache, x::Real) -> SVector{2,Float64}Compute the convergence measure Δ± of the asymptotic basis at x for each of the two algebraic columns (GW2020 Eq. 54). Returns ‖dU − x·matrix·U‖∞ / max(‖dU‖∞, ‖x·matrix·U‖∞) per column, where matrix = J₀ + xfac·J₁ + xfac²·J₂ is the J-rotated coefficient matrix (the residual of v' = xJv, GW2020 Eq. 6).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.barycentric_eval — Method
barycentric_eval(t, w, vals, τ) -> valueEvaluate the interpolant of vals (matrix: nodes × components) at τ. Returns a vector over components.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.barycentric_weights — Method
barycentric_weights(t) -> wBarycentric interpolation weights for nodes t (any distribution).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.boundary_basis — Method
boundary_basis(params, Q, cache, θ, S; growth=30.0)
-> (Us, Ub, E, cond_est)Boundary data at s = S directly from the inps series (no march; used when the series is already valid at the BVP edge): Us, Ub are the small/large power-like solutions as 6-component states in the inps normalization, E the numerically-generated decaying pair, cond_est the condition number of the column-normalized basis.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.build_asymptotics — Method
build_asymptotics(params::GGJParameters, Q::ComplexF64; kmax::Int=8) -> InnerAsymptoticsCacheConstruct the inps Wasow asymptotic basis (GW2020 Eqs. 7–53), truncating each power series at order kmax. Evaluate with evaluate_asymptotics; pick a cutoff with pick_xmax.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.cheblobatto — Method
cheblobatto(p) -> (t, D)Chebyshev–Lobatto nodes on [-1, 1] in ASCENDING order and the spectral differentiation matrix D for those nodes (Trefethen's cheb, reflected).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.decaying_pair — Method
decaying_pair(params, Q, θ, S; growth=30.0, seed=20260707, dp_res=20.0) -> E (6×2)Orthonormal basis of the forward-decaying pair at s = S on the ray: random seeds integrated backward over [S, S+ΔS] with fully-resolved RK4, ΔS sized so the pair separates from everything else by ≈ growth e-folds.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.delta_convergence — Method
delta_convergence(params, Q; verbose=true, refine_tol=1e-9,
max_rounds=16, max_cells=12000, kwargs...)
-> (; Δ, spread, base, table)Error bar for the matching data: solve at a strict baseline, then re-solve with each numerical knob perturbed on an independent axis (contour angle θ, element order p, series order/radius, refinement depth, march tolerance, handoff radius, purification e-folds — see variations below) and report the relative change of (Δ₁, Δ₂) per knob. θ is perturbed outward only: inward moves toward the x² ≈ −Q²(G+KF) pseudo-resonance and measures the perturbed solve, not the baseline. spread is the per-parity worst case over all knobs.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.evaluate_asymptotics — Method
evaluate_asymptotics(cache, x::Complex; derivative=true, apply_T=true) -> (U, dU)Complex-x version of evaluate_asymptotics (GW2020 Eq. 53; principal branch of x^r, valid for |arg x| < π). Columns of the 6×2 U are the two power-like solutions in the GW2020 Eq. (2) state convention u = (xΨ, Ξ, Υ, (xΨ)'/x, Ξ'/x, Υ'/x).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.evaluate_asymptotics — Method
evaluate_asymptotics(cache::InnerAsymptoticsCache, x::Real; derivative=true, apply_T=true) -> (U, dU)Evaluate the inps asymptotic basis at x > 0. Returns the 6×2 complex matrix U whose two columns are the algebraically-decaying ("small") asymptotic solutions of the GGJ system, and (if derivative=true) the 6×2 matrix dU of their derivatives dU/dx.
If apply_T=false, the result is left in the J-rotated coordinate basis (used by asymptotic_residual for residual checks). The default apply_T=true returns the solutions in the original 6-component first-order-system basis used by _physical_uv and the shooting / Galerkin solvers.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.evaluate_solution — Method
evaluate_solution(res::RaySolveResult, s::Real; isol=1) -> SVector{6,ComplexF64}Evaluate the solved state v = (Ψ, Ξ, Υ, Ψ', Ξ', Υ') at ray parameter s (diagnostic; barycentric interpolation on the containing cell).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.glasser_wang_2020_eq55 — Method
glasser_wang_2020_eq55(; taua=1.0, taur=1e6, v1=1.0) -> GGJParametersD-shaped aspect-ratio-2, q = 2 surface from Glasser & Wang, Phys. Plasmas 27, 012506 (2020), Eq. 55. This is the primary benchmark case for validating the inps Wasow basis convergence (their Figs. 1–4). This is useful only for benchmarking the galerkin solver and comparing to published results.
The five coefficients below are transcribed verbatim from Eq. 55; the paper's companion operating point is the scaled growth rate Q = 1.234e-1 (their Fig. 1). Note Eq. 55 does not tabulate an inner-region matching Δ(Q) — its Δ_± (Eq. 54) is a convergence-error norm — so a quantitative Δ(Q) cross-check needs an independent inner-layer reference, not this paper alone.
Timescale parameters (taua, taur, v1) are set to canonical normalization; callers should override them for physical cases.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.initial_breaks — Method
initial_breaks(params, Q, θ, S, p; cfl=0.4, drop=40.0, ratio=1.3,
hmax_frac=0.08) -> Vector{Float64}Build the initial cell-boundary vector 0 = s₀ < s₁ < … = S using the three-zone grading described in the header. cfl ≈ resolved-exponential rate per node; drop = e-folds after which decaying exponential content is considered machine-dead; ratio = geometric growth in the outer zone.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.inner_Q — Method
inner_Q(p::GGJParameters, γ::Number) -> ComplexF64Scaled inner-layer growth rate Q = γ / Q₀ (GWP2016 Eq. A15).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.march_boundary — Method
march_boundary(params, Q, θ, S, s_m, Us, Ub; rtol=1e-9, growth=30.0, seed=20260707)
-> (Us_m, Ub_m, F)Transport the power-pair boundary data from the series radius S inward to s_m along the ray, modulo the decaying exponential pair (the quotient in which Δ is defined). 2-stage Radau IIA (L-stable) with Φ-budgeted purges against a carried decaying frame; explicit marchers cannot do this at large S (see file header).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.mercier_di — Method
mercier_di(p::GGJParameters) -> Float64Mercier interchange index D_I = E + F + H − 1/4 (GWP2016 Eq. A9); D_I > 0 means local ideal interchange instability.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.mercier_dr — Method
mercier_dr(p::GGJParameters) -> Float64Resistive interchange index D_R = E + F + H² (GWP2016 Eq. A10); D_R > 0 means local resistive interchange instability.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.ode_matrix — Method
ode_matrix([CT,] p::GGJParameters, Q, x) -> SMatrix{6,6,CT}Coefficient matrix M(x) of dv/dx = M v, v = (Ψ, Ξ, Υ, Ψ', Ξ', Υ'), for real or complex x. CT selects the element type (default ComplexF64; Complex{Double64} for the extended-precision march).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.p1 — Method
p1(p::GGJParameters) -> Float64p₁ = √(−D_I), setting the large-x Frobenius exponents r± = 3/2 ± p₁ (GW2020 Eq. 49). Throws unless D_I < 0 (Mercier-stable required).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.parity_rows — Method
parity_rows(isol) -> SVector{3,Int}Component indices of v = (Ψ, Ξ, Υ, Ψ', Ξ', Υ') that vanish at s = 0 for each parity solution, mirroring deltacsetboundary:
isol = 1("odd"): Ψ'(0) = 0, Ξ(0) = 0, Υ(0) = 0 → components (4, 2, 3)isol = 2("even"): Ψ(0) = 0, Ξ'(0) = 0, Υ'(0) = 0 → components (1, 5, 6)
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.physical_ua_dua — Method
physical_ua_dua(cache, x::Number) -> (ua, dua)Convert the inps 6×2 basis at (possibly complex) x to physical (Ψ, Ξ, Υ) values and x-derivatives, each 3×2 (column 1 = large power solution, 2 = small). Same map as the deltac inpso_get_ua/dua convention.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.pick_smax — Method
pick_smax(params, Q; θ=angle(Q)/4, eps=1e-9, kmax=12, cache=nothing,
slogmin=-1.0, slogmax=6.5, dslog=0.01) -> (S, cache, achieved)Ray analog of pick_xmax: the smallest s at which the series residual along x = e^{iθ}s drops below eps. If never reached, returns the residual-minimizing s with achieved = false.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.pick_xmax — Method
pick_xmax(params, Q; eps=1e-7, kmax=8, xlogmin=-1.0, xlogmax=4.0, dxlog=0.01) -> (x_max, cache)Sweep x log-uniformly upward from 10^xlogmin and return the smallest x at which max(asymptotic_residual(cache, x)) < eps — the cutoff x_max where the GW2020 Eq. (54) convergence measure drops below tolerance (GW2020 Sec. III, Fig. 3). Also returns the InnerAsymptoticsCache it built so callers can reuse it.
Throws an ErrorException if no x in the sweep range achieves the target tolerance.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.q0 — Method
q0(p::GGJParameters) -> Float64Growth-rate scale Q₀ = X₀ / τ_A (GWP2016 Eq. A15); physical rate = Q₀ Q.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.q4_surface_benchmark — Method
q4_surface_benchmark() -> GGJParametersPhysical q = 4 rational-surface benchmark point (S = τR/τA ≈ 4.58×10⁶, DI ≈ −0.31166, α = √(−DI) ≈ 0.5583). Primary validation point for the rotated-ray backend on the imaginary-Q axis (pinned at Q = 100i, 500i in the test suite).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.rescale_delta — Method
rescale_delta(Δ, p::GGJParameters) -> SVector{2,ComplexF64}Rescale the matching data to physical Δ by S^(2√(−D_I)/3) · v₁^(2√(−D_I)) (GWP2016 Sec. IV), element-wise on (Δ_odd, Δ_even).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.sfac — Method
sfac(p::GGJParameters) -> Float64Lundquist number S = τ_R / τ_A.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.solution_profile — Method
solution_profile(res::RaySolveResult; npc=8) -> (; s, x, Ψ, Ξ, Υ)Reconstruct (Ψ, Ξ, Υ) on the solution contour, npc points per cell; each field is npts × 2 with columns (odd, even) and x = e^{iθ}s. For rotated solves the fields live on the ray — continuation back to real x is a separate (unstable) problem, by design.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.solve_inner_converged — Method
solve_inner_converged(::GGJModel{:galerkin}, params::GGJParameters, γ::Number;
rtol=1e-2, max_levels=6, kwargs...)
-> (; delta, converged, err, kmax, xfac, nx, nlevels)Convergence-guarded Galerkin solve: jointly refines the three coupled accuracy knobs (series order kmax, asymptotic reach xfac, grid nx scaled to hold cells-per-unit-x ≈ cells_per_unit) until the per-component relative change of (Δ₁, Δ₂) drops below rtol, or max_levels is hit (converged=false). The metric is per real/imag component with a 5% significance floor, so a converged norm cannot mask a wrong small component.
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.solve_ray — Method
solve_ray(params::GGJParameters, Q; θ=angle(Q)/4, kmax=12, p=12, S=nothing,
smax_tol=1e-9, refine_tol=1e-8, max_rounds=8, max_cells=6000,
verbose=false, kwargs...) -> RaySolveResultSolve the GGJ inner-layer problem on the rotated ray x = e^{iθ}s. Δ follows the deltac output convention (swap + rescale_delta), directly comparable to the :galerkin backend. θ = 0 reproduces a real-axis solve; S defaults to the inps series-residual radius (pick_smax); p is the polynomial order per spectral element; remaining keywords are march/mesh knobs (see march_boundary, initial_breaks, decaying_pair).
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.x0 — Method
x0(p::GGJParameters) -> Float64Inner-layer length scale X₀ = S^(−1/3) (GWP2016 Eq. A14); physical x = X₀ X.
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner — Method
solve_inner(::GGJModel{:galerkin}, params::GGJParameters, γ::Number;
kmax::Int=8, nx::Int=512, nq::Int=4, pfac::Float64=1.0,
cutoff::Int=5, xfac::Float64=1.0, tol_res::Float64=1e-5)
-> InnerLayerResponseSolve the GGJ inner-layer matching problem using the Hermite-cubic finite element (Galerkin) method (GWP2016 Sec. III), in the "resonant + noexp + inps" configuration.
Returns the parity-projected matching data (GWP2016 Eqs. 34–35) with the X₀^{2√(−D_I)} physical rescaling applied, as an InnerLayerResponse whose tearing/interchange fields are the isol=1/isol=2 element solutions respectively — no parity swap (see the boundary-condition block above for the parity derivation).
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner — Method
solve_inner(::GGJModel{:ray}, params::GGJParameters, γ::Number; kwargs...)
-> InnerLayerResponseRotated-ray backend: converts γ via inner_Q, forwards keywords to solve_ray, and returns the parity-projected matching data. The solve_ray pair is ordered (Δ₁, Δ₂) = (interchange, tearing), so the two channels are swapped into the named fields of InnerLayerResponse. Preferred for |Q| ≳ 1 and near the imaginary axis; use solve_ray directly when the full RaySolveResult is wanted.
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner — Method
solve_inner(::GGJModel{:shooting}, params::GGJParameters, γ::Number;
reltol::Float64=1e-6, abstol::Float64=1e-6,
rtol_origin::Float64=1e-6, nps::Int=8,
fmax::Float64=1.0, solver=Tsit5()) -> InnerLayerResponseSolve the GGJ inner-layer matching problem by stable backward shooting in the origin-diagonalized 4×4 basis.
Returns an InnerLayerResponse(tearing, interchange) with rescaling applied. _delta_from_c0 returns a (Δ₁, Δ₂) pair where Δ₁ is the interchange (anti-symmetric / W-odd) channel and Δ₂ is the tearing (symmetric / W-even) channel. We therefore map Δ₂ → tearing and Δ₁ → interchange into the named fields, matching the physics channel labels used by the Galerkin solver and by the InnerLayerResponse docstring.
Tolerances reltol/abstol are the integrator tolerances; rtol_origin controls the truncation error of the origin Frobenius series and the choice of tmin.
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner_profile — Method
solve_inner_profile(::GGJModel{:galerkin}, params::GGJParameters, γ::Number; kwargs...)
-> (; Δ, x, Ψ, Ξ, dψdx, rescale)Hermite-FEM implementation of the solve_inner_profile interface: real-axis solve, so Δ and the profiles come from the same solution. Same numerics/kwargs as solve_inner(GGJModel(; solver=:galerkin), ...).
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner_profile — Method
solve_inner_profile(::GGJModel{:ray}, params::GGJParameters, γ::Number;
npc=8, certify_rtol=1e-3, kwargs...)
-> (; Δ, x, Ψ, Ξ, dψdx, rescale, certΔ)Rotated-ray implementation of the solve_inner_profile interface. The certified Δ comes from the optimal-contour solve at θ = arg(Q)/4 (robust for |Q| ≳ 1, where real-axis methods drift); the profiles come from a θ = 0 re-solve on the real axis, valid at physical (RPEC) |Q| since the on-axis pseudo-resonance is a regular point resolved by the BVP refinement. The θ=0-vs-optimal-θ Δ agreement doubles as a runtime certificate (two maximally different contours through the same entire solutions): the relative mismatch is returned as certΔ and warns above certify_rtol. npc sets the profile points per mesh cell; extra keywords forward to both solve_ray calls (θ is fixed by the method — do not pass it).
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner_profile — Method
solve_inner_profile(params::GGJParameters, γ::Number; kwargs...)Diagnostic variant of solve_inner(GGJModel(:galerkin), ...) that also returns the reconstructed inner-layer field profiles. Returns (Δ, Q, profile) where profile = (x, Ψ, Ξ, Υ) (Ψ = inner ξ, columns odd/even parity). Same numerics/kwargs as solve_inner; for investigating resonant-layer structure vs resistivity, not the production matching path.
SLAYER
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.LayerWidths — Type
LayerWidthsResistive inner-layer length scales at one rational surface, in meters. The primary quantity is delta_s_m, the resistive layer thickness from the SLAYER del_s Riccati solve; d_beta is the β-weighted ion scale it is built from, retained as a drift-scale reference.
Fields
ising,m,n– surface index and mode numbers (traceability)dels_db– dimensionlessδ_s / d_βfromriccati_del_sdelta_s– complex layer thicknessδ_s = dels_db · d_β[m]delta_s_m–|δ_s|, the resistive layer thickness in meters (primary)d_beta– β-weighted ion scalec_β·d_iin meters (drift reference)
delta_s_m should sit within a few orders of magnitude of d_beta for a well-posed surface (dels_db is O(1)); a large gap flags a normalisation or input problem.
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.SLAYERModel — Type
SLAYERModel{S} <: InnerLayerModelSLAYER inner-layer model selector. The type parameter S selects the Riccati formulation:
:fitzpatrick– Pperp/Ptor Fitzpatrick formulation (default; Fitzpatrick 2023 two-fluid drift-MHD layer)
Future dispersion variants (e.g. :standard) may be added but are not currently implemented. The del_s formulation is exposed separately as the layer-thickness diagnostic slayer_layer_thickness, not as a dispersion solve_inner path.
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.SLAYERParameters — Type
SLAYERParametersDimensionless layer-physics parameters at one rational surface for the Fitzpatrick two-fluid drift-MHD SLAYER inner-layer model (Fitzpatrick 2023; Park et al. 2022), plus dimensional auxiliaries required for de-normalization. The parametrization uses P_perp, P_tor, and D_norm (not the older pr/pe/ds set).
| field | meaning |
|---|---|
ising | Singular-surface index (traceability only) |
m, n | Poloidal / toroidal mode numbers at this surface |
tau | Ti / Te |
lu | Lundquist number S = τR / τH |
c_beta | Compressibility √(βlocal / (1 + βlocal)) |
D_norm | (dβ/rs) · S^(1/3) · √(τ/(1+τ)) (Fitzpatrick normalized scale) |
P_perp | Perpendicular Prandtl number τR / τ⊥ |
P_tor | Toroidal-direction Prandtl number τR / τ‖tor |
Q_e | Normalized electron diamagnetic: −tauk · ω_*e |
Q_i | Normalized ion diamagnetic: −tauk · ω_*i |
iota_e | Qe / (Qe − Q_i) |
tauk | Q-conversion factor S^(1/3) · τ_H [s] — multiplies ω to get Q |
tau_r | Resistive diffusion time [s] |
delta_n | Δ-normalization factor S^(1/3) / r_s [m⁻¹] |
rs | Minor radius at this surface [m] |
R0 | Major radius [m] |
bt | Toroidal field [T] |
sval_r | r-based magnetic shear r_s · (dq/dr) / q (Fitzpatrick convention) |
dr_val | Resistive interchange D_R = E + F + H² (critical-Δ input; auto-derived from GGJ coefficients unless overridden) |
dgeo_val | Connor-Hastie-Helander 2015 Eq. 59 geometric factor (0 unless supplied) |
eta | Parallel resistivity entering τR = μ₀rs²/η [Ω·m] |
d_beta | Beta-weighted ion length scale cβ · di [m] |
dc_tmp | Critical-Δ offset from chi_parallel matching |
dc_type | Selector for dc_tmp formula |
The complex normalized growth rate Q = ω + iγ is not stored here; it is passed as a separate argument to solve_inner.
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.build_slayer_inputs — Method
build_slayer_inputs(equil, sings, profiles; …) -> Vector{SLAYERParameters}Build a SLAYERParameters for each rational surface in sings, pulling geometry (minor radius, r-based shear, q, dq/dψ, R₀) from the in-memory equil::PlasmaEquilibrium and kinetic data (ne, Te, Ti, ω, ω\*e, ω_*i) from profiles::KineticProfiles.
Layer inputs are assembled directly from the in-memory equilibrium and profiles, without an intermediate file round-trip.
Arguments
equil–PlasmaEquilibriumsings–Vector{SingType}(one per resonant surface)profiles–KineticProfilesvalid across allsingsψ values
Keyword arguments
bt– toroidal field [T]. Scalar, callable ofpsi, ornothing(default). Whennothing, the physicalB_T = F(ψ) / (2π·R₀)is computed per surface from the equilibrium's F-spline. Note:equil.config.b0expis a normalization (often just1.0), not the physical field, so passing it as a scalar is almost always wrong.mu_i– ion mass in proton-mass units (default2.0for D).zeff– effective charge (default1.0).chi_perp– perpendicular heat diffusivity [m²/s]. Scalar or a callable ofpsi(default1.0).chi_tor– toroidal heat diffusivity [m²/s]. Scalar or a callable ofpsi(default1.0).dr_val– resistive interchange indexD_R = E + F + H²(Glasser-Greene-Johnson 1975) feeding the critical-Δ formulas (:lar,:rfitzp,:toroidal). Whennothing(default), Julia derives it per-surface from the equilibrium asdr_val_k = D_R(ψ_k) = E_k + F_k + H_k², consistent with Connor-Hastie-Helander 2015 (PPCF 57 065001) Eq. 59 which uses(−D_R)in the χ_‖-matching critical-Δ. Pass a scalar / vector / callable to override.NOTE: the χ‖-matching critical-Δ requires the resistive interchange index `DR = E + F + H²
(Glasser-Greene-Johnson 1975), NOT the Mercier indexDI = E + F + H − 1/4. The two differ by(H − 1/2)², which is non-trivial on shaped equilibria (~factor 3 on DIII-D); this code uses the physically correctDR`.dgeo_val– Connor 2015 (PPCF 57 065001) Eq. 59 geometric factor used bydc_type=:toroidal. Whennothing(default), an error is raised ifdc_type=:toroidalis also requested — the auto-derived formula additionally needs ⟨|∇ψ|²⟩ FSA whichResistGeometrydoesn't currently expose. Pass a scalar / vector / callable to use a prescribed value. (Fordc_type=:rfitzpand:lar, dgeo_val is not consulted.)dc_type–:none(default),:lar,:rfitzp, or:toroidal.theta– poloidal angle at which to measure minor radius (default0.0, outboard midplane).resistivity_model–SauterNeoModel()(default),RedlNeoModel(),SpitzerModel(), orSpitzerHarmModel()(legacy Fitzpatrick σ∥). Sets the η entering τR = μ₀rs²/η. With a neoclassical model, `ftrapand ν*_e are taken from the surface'sResistGeometryif populated (viaForceFreeStates.resistevalall!), otherwise fall back to the ε-only Lin-Liu-Miller form andrs/R_0` aspect ratio.lnLambda_form– Coulomb-log form passed through toslayer_parameters(default:nrl;:wesson+SpitzerHarmModel()reproduces legacy SLAYER exactly).
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.r_based_shear — Method
r_based_shear(rs, q, dq_dpsi, da_dpsi) -> Float64Convert a ψ-based shear to the r-based (Fitzpatrick) convention used throughout SLAYER:
s_r = r_s · (dq/dr) / q = r_s · (dq/dψ) / (q · da/dψ)rs is the minor radius at the surface, q the safety factor, dq_dpsi the radial derivative of q with respect to ψ, and da_dpsi the derivative of the surface minor radius with respect to ψ. The two ψ derivatives must use the same ψ convention (i.e., both with respect to ψ_norm or both with respect to physical ψ — the conversion factor cancels in the ratio).
Equivalent to the conversion s_Fitz = s_psiN · r_s / (psi_N · da_dpsiN).
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.riccati_del_s — Method
riccati_del_s(p::SLAYERParameters;
q_start=5*p.D_norm, q_min=1e-5,
reltol=1e-10, abstol=1e-10, maxiters=50_000,
solver=Rodas5P(autodiff=false)) -> ComplexF64Solve the SLAYER del_s inner-layer Riccati ODE and return the dimensionless layer-thickness ratio δ_s / d_β at one rational surface (see file header for references).
Integrates dW/dq = W/q − W²/q + q·E/F inward from q_start = 5·D_norm to q_min, with asymptotic boundary value W = −α·q_start² − 0.5, α = √(P̂_⊥ / (1 + 1/τ)). The result is −(π / √(1 + 1/τ)) · W′(q_min), evaluated from a single RHS call at the inner endpoint.
Returns NaN + NaN·im if the stiff integration does not converge (the caller treats this as a missing diagnostic rather than a hard error).
Multiply the result by p.d_beta to obtain the resistive layer thickness in meters (see slayer_layer_thickness).
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.slayer_layer_thickness — Method
slayer_layer_thickness(p::SLAYERParameters; kwargs...) -> LayerWidthsCompute the resistive inner-layer thickness in meters at one rational surface.
Runs riccati_del_s for the dimensionless δ_s / d_β and scales by p.d_beta to obtain δ_s in meters. Keyword arguments are forwarded to riccati_del_s.
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.slayer_parameters — Method
slayer_parameters(; n_e, t_e, t_i, omega, omega_e, omega_i,
qval, sval_r, bt, rs, R0, mu_i, zeff,
chi_perp, chi_tor,
m, n,
dr_val=0.0, dgeo_val=0.0,
dc_type=:none, ising=0,
resistivity_model=SauterNeoModel(),
f_trap=nothing, nu_e_star=nothing,
R_major_eff=nothing,
lnLambda_form=:nrl)
-> SLAYERParametersBuild a SLAYERParameters for one rational surface from dimensional equilibrium and kinetic-profile inputs, in the Fitzpatrick two-fluid layer parametrization (Pperp/Ptor/Dnorm; the older magnetic/electron Prandtl pr/pe and ρs-based ds parameters are not used).
Arguments
n_e– electron density [m⁻³]t_e– electron temperature [eV]t_i– ion temperature [eV]omega– toroidal rotation frequency at the surface [rad/s]omega_e– electron diamagnetic frequency [rad/s]omega_i– ion diamagnetic frequency [rad/s]qval– safety factor q at the surfacesval_r– r-based magnetic shear r·(dq/dr)/q (Fitzpatrick). User_based_shearto convert from ψ-based shear.bt– toroidal field [T]rs– minor radius at the surface [m]R0– major radius [m]mu_i– ion mass in proton-mass units (e.g. 2.0 for D)zeff– effective chargechi_perp,chi_tor– perpendicular / toroidal heat diffusivity [m²/s]m,n– poloidal / toroidal mode numbers at the surfacedr_val,dgeo_val– inputs for the critical-Δ formuladc_type– one of:none,:lar,:rfitzp,:toroidalising– singular-surface index for traceability
Resistivity kwargs
The selected resistivity sets the resistive diffusion time τ_R = μ₀ r_s² / η and hence the Lundquist number and every normalized layer parameter derived from it.
resistivity_model–SauterNeoModel()(default: Sauter 1999 F33 trapped-particle correction, the physically-appropriate choice for H-mode tearing stability),RedlNeoModel()(improved high-ν* fit),SpitzerModel()(Sauter 18a fit, no trapped-particle correction), orSpitzerHarmModel()(Fitzpatrick Spitzer-Härm σ∥ — the legacy SLAYER closure; pair withlnLambda_form=:wessonto reproduce legacy τ_R exactly).f_trap– trapped-particle fraction at this surface. If not provided with a neoclassical model, falls back to Lin-Liu-Miller ε-only form withε = rs / (R_major_eff or R0).nu_e_star– electron collisionality. Ifnothingwith a neoclassical model, computed from Sauter 1999 Eq. 18b using the same ε.R_major_eff– ⟨R⟩ at the surface for the ν*_e formula (defaultR0).lnLambda_form–:nrl(default),:sauter, or:wesson(legacy form).
Sign convention for diamagnetic frequencies
The diamagnetic normalization uses
Q_e = -tauk · ω_*e
Q_i = -tauk · ω_*iFor the standard plasma-physics input where ω*e is tabulated negative and ω*i positive (electrons and ions drifting in opposite directions), this produces Q_e > 0, Q_i < 0, matching the opposite-drift expectation of the dispersion relation.
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.surface_da_dpsi — Method
surface_da_dpsi(equil, psi; theta=0.0, h=1e-5) -> Float64Central finite-difference approximation of d(minor radius)/dψ at psi. Falls back to one-sided differences near the flux-coordinate boundaries (0 or 1).
GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER.surface_minor_radius — Method
surface_minor_radius(equil, psi; theta=0.0) -> Float64Minor radius at normalized flux psi and poloidal angle theta, computed from equil.rzphi_rsquared as √((R − R₀)² + (Z − Z₀)²). theta = 0.0 (outboard midplane) is the default; pass θ = π to measure the inboard side if you want an average.
GeneralizedPerturbedEquilibrium.InnerLayer.solve_inner — Method
solve_inner(::SLAYERModel{:fitzpatrick},
p::SLAYERParameters, Q::Number;
pmin=1e-6, p_floor=6.0,
reltol=1e-10, abstol=1e-10,
maxiters=50_000,
solver=Rodas5P(autodiff=false)) -> InnerLayerResponseSolve the Fitzpatrick SLAYER inner-layer Riccati ODE for the complex normalized growth rate Q = ω + iγ. Returns an InnerLayerResponse with tearing = Δ and interchange = 0, so the result is interface-compatible with GGJModel.solve_inner (which populates both channels); SLAYER's pressureless layer produces only the tearing channel.
Algorithm
Implements the Fitzpatrick two-fluid drift-MHD layer formulation (Fitzpatrick 2023; Park et al. 2022) for the pressureless tearing channel (no parallel flow, pe = 0). Integrates dW/dp = -(fA'/p)·W − W²/p + (fB/(fA·fC))·p³ from a large p_start (selected by _riccati_f_initial according to whether D_norm² ≷ iota_e·P_perp/P_tor^(2/3)) inward to pmin, then computes Δ = π / W'(pmin) from a single RHS evaluation at the inner endpoint.
Solver
Default Rodas5P(autodiff=false) (Rosenbrock, stiff-friendly). The analytic Jacobian wired via the ODEFunction(jac=...) field accelerates the Newton solves. AD is disabled because complex Dual propagation through the chained denominators incurs allocations in this regime; finite-difference fallback is fast enough for the 1-equation system.
Note on solver swaps: sub-percent floating-point differences between ODE solvers cascade through the outer AMR's cell-flagging decisions (ContourSearchAMR.jl::_crosses_zero) and produce structurally different AMR cell trees. Swapping the ODE solver has been observed to change the classified root/pole inventory substantially even when the most-unstable γ agrees to within ~1e-5 relative. So solver choice is not just a per-call optimization — it affects the downstream root/pole inventory. Future solver swaps need to be validated against the topology fields (n_valid_roots, n_poles), not just γ.
Keyword arguments
pmin– inner-layer cutoff (default 1e-6)p_floor– floor onp_start(default 6.0)reltol,abstol,maxiters– stiff-solver tolerance/iteration limitssolver– any OrdinaryDiffEq algorithm; passTsit5()for the non-stiff path (rarely needed here)
Dispersion
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.AMRCell — Type
AMRCellA single axis-aligned-rectangle cell of an AMR scan. The four corner Q values (q_bl, q_br, q_tl, q_tr) and corresponding residual values (d_bl, d_br, d_tl, d_tr) are sufficient for marching-squares contour extraction.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.AMRResult — Type
AMRResultOutput of amr_scan.
| field | meaning |
|---|---|
cells | Final list of AMRCell after all refinement passes |
Q | Flat Vector{ComplexF64} of every unique residual eval |
Δ | Corresponding Vector{ComplexF64} of residual values |
truncated | true if max_cells was hit and remaining refinement |
passes were skipped (max_cells_action=:warn_truncate). | |
A true result is NOT fully converged — distinguish it | |
| from a converged scan in convergence studies. |
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.BoxActivity — Type
BoxActivityWhy a box was retained or skipped by multi_box_amr_scan. NoActivity means the pre-screen grid showed no zero-isoline crossings and no large-|Δ| corners; the box is excluded from refinement. The other variants record which criterion fired first.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.GrowthRateResult — Type
GrowthRateResultOutput of find_growth_rates.
| field | meaning |
|---|---|
Q_root | Best (highest-γ surviving) root, normalized |
omega_Hz | Re(Q_root) / tauk — physical rotation frequency |
gamma_Hz | Im(Q_root) / tauk — physical growth rate |
Q_root_secondary | Second-most-unstable root flagged for ambiguity, or |
NaN+NaNim if the primary root was unambiguous. | |
omega_Hz_secondary | physical ω of the secondary root, or 0 if none |
gamma_Hz_secondary | physical γ of the secondary root, or 0 if none |
warning_flags | Vector{Symbol} of warnings raised on Q_root: |
:geom, :gap (root accepted with caveat); | |
:spurious when ≥1 contour near-miss was dropped by | |
the validity gate (parked in filtered_roots); or | |
:no_root when NO usable root was found (Q_root is | |
NaN; omega_Hz/gamma_Hz fall back to 0 — check | |
| this flag to tell that apart from a true γ≈0 result). | |
| Empty if root is clean. | |
valid_roots | All non-pole intersections that survived pole filter |
poles | Intersections classified as poles |
filtered_roots | Intersections rejected by the above-pole/outside-Re |
| filter or the new geom+gap recursion | |
re_contours | Extracted Re(Δ)=re_target polylines |
im_contours | Extracted Im(Δ)=im_target polylines |
pole_threshold | Threshold used for pole classification |
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.MultiBoxAMRResult — Type
MultiBoxAMRResultOutput of multi_box_amr_scan. Per-box AMRResults plus the aggregated cells/Q/Δ across all active boxes. Pre-screen-inactive boxes have nothing for their AMRResult and contribute nothing to the aggregated arrays.
| field | meaning |
|---|---|
box_results | per-box AMRResult, or nothing if box was skipped |
box_activity | per-box BoxActivity enum |
cells | concatenated AMRCells from all active boxes |
Q | union of all unique Q evaluations (active + skipped) |
Δ | corresponding Δ values |
prescreen_evals | total f(Q) evaluations spent on pre-screening |
The aggregated (cells, Q, Δ) are suitable for direct consumption by find_growth_rates. Pre-screen evaluations are still included in Q/Δ even for skipped boxes, so any downstream pole-magnitude diagnostic that uses the flat residual list sees the full sample.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.MultiSurfaceCoupling — Type
MultiSurfaceCoupling{V<:AbstractVector{<:SurfaceCoupling}}Multi-surface dispersion data: a vector of SurfaceCoupling, the full Δ' matrix, the index of the reference surface (whose tauk defines the Q normalization), and the truncation msing_max (number of surfaces actually participating in the determinant). Calling mc(Q) returns det(M(Q)) where
M[k,k] = dp_matrix[k,k] - scale_k · Δ_inner_k(Q · tauk_ref / tauk_k) - dc_k
M[i,j] = dp_matrix[i,j] for i ≠ j (off-diagonal Δ' couplings)A root of mc in the complex Q plane is a coupled tearing eigenvalue.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.MultiSurfaceCouplingFull — Type
MultiSurfaceCouplingFull{V<:AbstractVector{<:SurfaceCoupling}}Full 4m × 4m tearing+interchange Pletzer-Dewar dispersion matrix (Wang et al. 2020, Phys. Plasmas 27, 122503).
Given the raw 2m × 2m outer-region matrix dp_raw (side-major ordering [L_s1, R_s1, L_s2, R_s2, …], from intr.delta_prime_raw) and a vector of SurfaceCoupling (each containing the inner-layer model and parameters), calling mc(Q) assembles the 4m × 4m Pletzer-Dewar matching matrix and returns det(mat).
This is the correct Pletzer-Dewar dispersion relation for tearing+interchange coupling. A naive 2m × 2m det(D' − D(γ)) form is not equivalent: it subtracts the inner Δ (parity ± basis) from the outer D' (side-major L/R basis), mixing two different bases. The 4m × 4m matching system introduced here keeps the bases separate via the explicit C^j_{L,R} / d^j_± unknowns. For pure-tearing (pressureless SLAYER) studies use the reduced m × m MultiSurfaceCoupling instead.
Fields
surfaces::V— per-surfaceSurfaceCoupling.dp_raw::Matrix{ComplexF64}— 2m × 2m outer-region matrix (side-major).ref_idx::Int— reference surface for Q rescaling (1-based).msing_max::Int— number of surfaces to include (truncates).rotation::Vector{Float64}— per-surface rotation frequencies (s⁻¹).ntor::Int— toroidal mode numbern(default 1).
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.ScanResult — Type
ScanResultOutput of a brute-force or AMR Q-plane scan.
| field | meaning |
|---|---|
Q | Complex Q values (Matrix for grid, Vector for AMR) |
Δ | Residual values, same shape as Q |
re_axis | Real-axis grid (only for regular-grid ScanResult) |
im_axis | Imaginary-axis grid (only for regular-grid ScanResult) |
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.SurfaceCoupling — Type
SurfaceCoupling{M<:InnerLayerModel, P}Per-surface dispersion data: (model, params, dp_diag, dc, scale, tauk). Calling sc(Q) returns the complex residual
r(Q) = dp_diag - scale * solve_inner(model, params, Q).tearing - dcA root of sc in the complex Q plane is a tearing eigenvalue at this surface in the uncoupled approximation (only the tearing channel of the inner-layer response appears — the interchange channel enters the full 2m×2m dispersion via MultiSurfaceCoupling, not this scalar form). Coupled multi-surface eigenvalues come from MultiSurfaceCoupling evaluating the determinant of the modified Δ' matrix.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.amr_scan — Method
amr_scan(f, Q_re_range, Q_im_range;
nre0, nim0, passes,
max_cells=10_000_000,
max_cells_action=:error,
snapshot_callback=nothing,
parallel=Threads.nthreads() > 1) -> AMRResultAdaptively refine a Q-plane scan of the residual f(Q). An initial nre0 × nim0 axis-aligned grid of cells is built over Q_re_range × Q_im_range and passes rounds of refinement are applied. Each pass:
- flags any cell whose 4 corner residuals straddle zero in
Re(Δ)orIm(Δ); - subdivides each flagged cell into 4 quadrant children, evaluating
fat 5 new midpoints; - unflagged cells are kept unchanged.
All evaluations of f are deduplicated through a Dict{ComplexF64, ComplexF64} hash cache so that adjacent cells share a single evaluation per corner. The returned AMRResult carries both the final cell list (for marching-squares contour extraction) and the flat list of all unique Q/Δ evaluations.
Keyword arguments
nre0,nim0– initial coarse-grid cell counts along each axispasses– number of refinement passesmax_cells– safety cap on total cells; behavior on hit is set bymax_cells_actionmax_cells_action–:error(raises) or:warn_truncate(logs a warning and returns the partial result). The latter is useful for convergence-vs-resolution studies where we deliberately push max_cells and want graceful degradation. Default:errorpreserves the prior safety-rail behaviour.snapshot_callback– if notnothing, a function called after each pass (and once for the initial grid, pass=0) with arguments(pass::Int, cells::Vector{AMRCell}, cache::Dict{ComplexF64,ComplexF64}). The callback receives live references — copy if you need persistence. Used by convergence studies to extract intermediate γ at each pass count.parallel– evaluatefin parallel viaThreads.@threadswithin each phase (initial grid + each refinement pass). Defaults totruewhen more than one Julia thread is available. Per-call evaluations offmust be thread-safe. The result is deterministic regardless of thread count: cache updates and cell-list construction stay serial, AND BLAS is pinned to one thread during the parallel region so thedet()-based residual does not pick up non-reproducible multithreaded-LAPACK reductions (which otherwise flip the extracted root between near-degenerate solutions).
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.as_amr_result — Method
as_amr_result(mbres::MultiBoxAMRResult) -> AMRResultWrap the aggregated cells/Q/Δ from a multi-box scan as a plain AMRResult so it can be passed directly to find_growth_rates(::AMRResult, tauk; ...).
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.brute_force_scan — Method
brute_force_scan(f, Q_re_range, Q_im_range; nre, nim,
threaded::Bool=true) -> ScanResultEvaluate the Q-callable residual f on a regular nre × nim grid spanning the rectangle Q_re_range × Q_im_range in the complex Q plane. f must accept a single Complex argument and return a Complex value (typically a SurfaceCoupling or MultiSurfaceCoupling, but any callable works).
Use find_growth_rates(scan, tauk; ...) to extract growth-rate eigenvalues from the result.
Arguments
f– Q-callable residual (e.g.SurfaceCoupling,MultiSurfaceCoupling)Q_re_range–(re_min, re_max)tupleQ_im_range–(im_min, im_max)tuple
Keyword arguments
nre,nim– grid resolution along each axisthreaded– distribute Q evaluations acrossThreads.@threads
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.find_growth_rates — Method
find_growth_rates(amr::AMRResult, tauk::Real;
re_target=0.0, im_target=0.0,
pole_threshold=10.0,
filter_above_poles=true,
filter_outside_re=true) -> GrowthRateResultExtract tearing growth-rate eigenvalues from an AMR AMRResult via Delaunay triangulation + marching triangles on the scattered evaluation points. The pipeline after contour extraction (segment intersection, pole classification, outside-Re filter, physical-Hz conversion) is identical to the brute-force grid path — only the contour extractor changes. Hanging-node issues from the quadtree's mixed refinement levels are resolved by the triangulation respecting every evaluated point uniformly.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.find_growth_rates — Method
find_growth_rates(scan::ScanResult, tauk::Real;
re_target=0.0, im_target=0.0,
pole_threshold=10.0,
filter_above_poles=true,
filter_outside_re=true,
gap_kHz_threshold=1.0) -> GrowthRateResultExtract tearing growth-rate eigenvalues from a brute-force ScanResult by contour-intersection analysis. tauk is the per-surface time normalization used to convert Q back to physical (Hz) units (SurfaceCoupling.tauk for single-surface scans; mc.surfaces[mc.ref_idx].tauk for coupled scans).
Keyword arguments
re_target,im_target– contour levels (zero for vanilla dispersion root-finding; nonzero values let the caller probe iso-residual contours)pole_threshold– intersection is classified as a pole whenmax(|Re(Δ)|)along the local arc of the nearest Im=0 contour exceeds this valuefilter_above_poles– discard roots whose γ exceeds the highest pole γfilter_outside_re– restrict the above-pole rejection to roots whose +γ step along the Im=0 contour exits the Re=0 contour loop. Whentrue, roots that are above a pole but geometrically inside the Re=0 contour survive (matches the Python default). Note this gate fails when the Re=0 contour is OPEN (e.g., exits the Q box edge), letting spurious upper-branch roots through; the:geomand:gapflags below cover that case.gap_kHz_threshold– if the highest-γ root is unstable (γ > 0) AND its γ exceeds the next root by more than this many kHz, it is flagged as a:gapwarning. Default 1.0 kHz.residual– optional dispersion-residual callablef(Q::Complex). When supplied, each contour-intersection root is POLISHED to the true zero offby a bounded, neighbour-aware local solve (see_polish_root), making the extracted root resolution- and thread-independent.nothing(default) reports the raw marching-squares estimate, which is sensitive to grid and floating-point rounding.polish_maxit– max polish iterations per root (default 20). Each costs a handful offevaluations; failures fall back to the unpolished point.validity_rtol– withresidualsupplied, a polished root is kept only if|residual| < validity_rtol · median(|Δ|)(default 1e-3). Intersections that don't polish to a true zero (contour near-misses, or surfaces whose Δ' BVP failed →|Δ'|huge and uncancellable) are dropped fromvalid_roots, parked infiltered_roots, and flagged:spurious. If every candidate is spurious, the result carries:no_root.
Spurious-root recursion
After the per-intersection pole / above-pole filters, the remaining roots are sorted by descending γ. The selection loop walks down this list and at each candidate evaluates two flags:
:geom— Re(Δ)=0 contour is locally a downward-concave "hill" at the candidate (clean polyline-following quadratic fit).:gap— candidate is unstable AND its γ exceeds the next root's by more thangap_kHz_thresholdkHz.
If BOTH fire, the candidate is discarded as spurious and the next-most- unstable root is tried. If exactly ONE fires, the candidate is accepted as primary with that warning recorded, and the next root is exposed as Q_root_secondary so downstream tools can plot or reanalyse it. If neither fires, the candidate is accepted cleanly.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.multi_box_amr_scan — Method
multi_box_amr_scan(f, boxes;
pole_magnitude_threshold,
prescreen_nre=25, prescreen_nim=25,
nre0=25, nim0=25, passes=4,
max_cells=10_000_000,
max_cells_action=:error,
parallel=Threads.nthreads() > 1) -> MultiBoxAMRResultRun amr_scan over multiple Q-plane boxes with a coarse pre-screen step that skips inactive boxes entirely. The typical use case is the three-stripe ω-axis scan for SLAYER coupled tearing dispersion:
ω ∈ [-75, -25], γ ∈ [-25, +25] (left stripe)
ω ∈ [-25, +25], γ ∈ [-25, +25] (centre stripe)
ω ∈ [+25, +75], γ ∈ [-25, +25] (right stripe)A single 150×50 box is wasteful when the dispersion is concentrated near a narrow ω band; splitting into stripes and pre-screening lets the AMR effort land on the active stripe.
Pre-screen logic
Each box is sampled on a prescreen_nre × prescreen_nim corner grid (default 25×25, matching the typical AMR initial-grid resolution). A box is ACTIVE if ANY pre-screen cell satisfies at least one criterion:
- sign change of
Re(Δ)across the cell's 4 corners (zero-isoline ofRe(Δ)crosses the cell — root candidate); - sign change of
Im(Δ)across the cell's 4 corners (zero-isoline ofIm(Δ)crosses the cell — root candidate); - any corner with
|Δ| ≥ pole_magnitude_threshold(likely pole — the sign-only criteria miss poles whose fringe doesn't straddle a corner).
Active boxes get the full amr_scan treatment. Inactive boxes are dropped (their AMRResult is nothing).
Arguments
f: residual functionQ::ComplexF64 → Δ::ComplexF64. Must be thread-safe ifparallel=true.boxes: vector of(Q_re_range, Q_im_range)tuples, one per box. Boxes may overlap or share boundaries; the aggregator deduplicates Q values.
Required keyword
pole_magnitude_threshold: activity threshold for|Δ|. A natural choice is≈ |mean(Δ)|from a baseline (or the same value used for adaptive polethreshold in `findgrowth_rates`).
Optional keywords
prescreen_nre,prescreen_nim(default 25 each): pre-screen grid resolution. Coarser misses small features; finer wastes evaluations on inactive boxes.nre0, nim0, passes, max_cells, max_cells_action, parallel: forwarded to each per-boxamr_scancall. Defaults matchamr_scan.
Returns
A MultiBoxAMRResult. The aggregated (cells, Q, Δ) can be wrapped in an AMRResult (helper as_amr_result below) for direct use with find_growth_rates.
Notes / TODO
- Each per-box
amr_scanrebuilds its own cache, so the 25×25 pre-screen corners get re-evaluated by the AMR initial pass on active boxes (≈ 676 wasted evals per active box). A future refactor could thread a shared cache throughamr_scan. For now the cost is small relative to the AMR refinement evals. - Boxes that share a boundary line (e.g. the three ω-stripe layout above) duplicate ≈
prescreen_nim+1corner evaluations per shared edge. Also small.
Example
boxes = [((-75.0, -25.0), (-25.0, 25.0)),
((-25.0, 25.0), (-25.0, 25.0)),
((25.0, 75.0), (-25.0, 25.0))]
result = multi_box_amr_scan(f_residual, boxes;
pole_magnitude_threshold=1e-3,
prescreen_nre=25, prescreen_nim=25,
nre0=25, nim0=25, passes=4)
amr = AMRResult(result.cells, result.Q, result.Δ)
roots = find_growth_rates(amr, tauk; pole_threshold=1e-3)GeneralizedPerturbedEquilibrium.Tearing.Dispersion.multi_surface_coupling — Method
multi_surface_coupling(surfaces, dp_matrix;
ref_idx=1,
msing_max=min(3, length(surfaces)))
-> MultiSurfaceCouplingConstruct a multi-surface coupling from a vector of SurfaceCoupling and the full outer-region Δ' matrix. dp_matrix must be square with side length length(surfaces) (it is the same matrix returned by PerturbedEquilibrium.SingularCoupling's Δ' boundary-value problem).
Keyword arguments
ref_idx– index of the reference surface whosetaukdefines the Q normalization. Defaults to1.msing_max– number of surfaces from the front ofsurfacesto include in the determinant. Defaults tomin(3, length(surfaces)): Δ' off-diagonal couplings beyond the third surface tend to be erratic in practice, so the determinant is conservatively truncated to the upper-leftmsing_max × msing_maxsubmatrix ofdp_matrix. Set explicitly (up tolength(surfaces)) to override.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.multi_surface_coupling_full — Method
multi_surface_coupling_full(surfaces, dp_raw;
ref_idx=1,
msing_max=length(surfaces),
rotation=zeros(length(surfaces)),
ntor=1) -> MultiSurfaceCouplingFullConstruct the 4m × 4m dispersion matrix driver. dp_raw must be the 2m × 2m matrix in side-major ordering (the intr.delta_prime_raw field populated by ForceFreeStates.compute_delta_prime_matrix! on the Riccati path). rotation[k] is the per-surface rotation frequency; it shifts the per-surface inner Q argument by i·ntor·rotation[k]. Default zero rotation matches the static-equilibrium case.
Keyword arguments
ref_idx— index of the reference surface whosetaukdefines the Q normalization (1 ≤ ref_idx ≤ m). Defaults to 1.msing_max— truncate to the leadingmsing_maxsurfaces; the matching matrix becomes 4·msingmax × 4·msingmax, built from the corresponding 2·msingmax × 2·msingmax submatrix ofdp_raw. Defaults tolength(surfaces).rotation— per-surface rotation frequencies in s⁻¹ (length m). Defaults to all zero.ntor— toroidal mode number n. Defaults to 1.inner_kwargs— NamedTuple of kwargs forwarded tosolve_innerat every Q evaluation, e.g.(pfac=0.1, xfac=10.0, nx=128, nq=5)for Galerkin grid tuning. Defaults toNamedTuple().
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.surface_coupling — Method
surface_coupling(model::GGJModel, params::GGJParameters,
dp_diag::Number) -> SurfaceCouplingGGJ convenience constructor. scale is 1.0 because GGJ's solve_inner applies its own rescale_delta (S^(2p₁/3)·v1^(2p₁)) internally, so the returned Δ is already in outer units. tauk defaults to 1.0 (GGJ has no direct analogue of SLAYER's per-surface time normalization, so multi-surface Q rescaling is a no-op for GGJ surfaces unless overridden).
No dc kwarg: GGJ's 4m×4m Pletzer-Dewar residual already includes the interchange channel, which provides Glasser (Mercier) stabilization natively. A Δcrit proxy (χparallel-matching offset on the diagonal) is meaningful only for tearing-only slab-layer approximations like SLAYER; for GGJ it would double-count the interchange physics. The SurfaceCoupling struct's dc field is hard-wired to 0 here.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.surface_coupling — Method
surface_coupling(model::InnerLayerModel, params, dp_diag::Number;
dc::Real=0.0, scale::Real=1.0, tauk::Real=1.0)
-> SurfaceCouplingGeneric fallback constructor. Use this when wiring a new inner-layer model into the dispersion solver — pass the appropriate inner→outer-units scale and per-surface tauk explicitly.
GeneralizedPerturbedEquilibrium.Tearing.Dispersion.surface_coupling — Method
surface_coupling(model::SLAYERModel, params::SLAYERParameters,
dp_diag::Number; dc::Real=0.0) -> SurfaceCouplingSLAYER convenience constructor. scale is set to params.lu^(1/3) so that the dimensionless Δ from riccati_f is mapped to outer ψ-units before subtraction from the Δ' diagonal. tauk is taken from params.tauk for use by MultiSurfaceCoupling Q rescaling.
Runner
GeneralizedPerturbedEquilibrium.Tearing.Runner.SLAYERControl — Type
SLAYERControlConfiguration for the SLAYER tearing-mode analysis. All fields are user-facing: read from the [SLAYER] TOML section of a gpec.toml via slayer_control_from_toml, or built directly via the @kwdef keyword constructor.
Core toggles
enabled– run the analysis at all (defaultfalse)inner_model–:slayer_fitzpatrick(default),:ggj_shooting, or:ggj_galerkinscan_mode–:amr(default) or:brute_forcecoupling_mode–:uncoupled(default, per-surface) or:coupled(multi-surface determinant)dc_type– critical-Δ offset selector, one of:none,:lar,:rfitzp,:toroidal(χ_‖-matching critical-Δ formulas, Connor-Hastie-Helander 2015)msing_max– number of surfaces to include in the coupled determinant (default 3; capped atlength(sings)at runtime)
Physics knobs
bt– toroidal field[T].nothing(default) resolves the physicalB_T = F(ψ)/(2π·R₀)per surface from the equilibrium's F-spline; a scalar or a callable ofpsioverrides itmu_i– ion mass in proton-mass units (default 2.0 for D)zeff– effective chargechi_perp,chi_tor– fallback perpendicular / toroidal heat diffusivity [m²/s], used only when the kinetic file carries no usablechi_e/chi_phiprofile (dataset absent or all-zero); otherwise the file's χ⊥(ψ)/χ_φ(ψ) take precedencedr_val,dgeo_val– critical-Δ formula inputs.nothing(default) auto-derives them from the equilibrium:dr_valfrom the resistive interchange indexD_R = E + F + H²at each surface,dgeo_valfrom the toroidal geometric factor (required only bydc_type=:toroidal). Supply a scalar only to override the auto-derivation; an explicit0.0disables the critical-Δ offset (Δ_crit ≡ 0)theta_sample– poloidal angle at which to sample minor radius (default 0.0, outboard midplane)resistivity_model– η closure setting τR = μ₀rs²/η::sauter(default, neoclassical F33),:redl,:spitzer(Sauter 18a, no trapped correction), or `:spitzerharm` (legacy Fitzpatrick σ_∥)lnLambda_form– Coulomb-log form::nrl(default),:sauter, or:wesson(legacy; pair with:spitzer_harmto reproduce old SLAYER)
Scan grid (used for both brute-force and AMR initial mesh)
Q_re_range,Q_im_range– box in the normalized Q planenre,nim– grid resolution along each axis
AMR refinement
amr_passes– max refinement levelsamr_max_cells– hard safety cap
Growth-rate-extraction filters
pole_threshold– threshold for pole classification (default 10)pole_threshold_adaptive– if true, pole_threshold is OVERRIDDEN per scan with10 × median(|Δ|)over the scan grid. The median is robust where|mean(Δ)|is not: it resists inflation from the near-pole samples that dominate a mean, and avoids the phase-cancellation that can shrink|mean|. Useful when |Δ| spans 8+ orders of magnitude (e.g. SLAYER scans where the hardcoded 10.0 default is too restrictive and classifies all intersections as poles).filter_above_poles– discard roots above the highest pole γfilter_outside_re– condition the above-pole filter on the +γ step exiting the Re(Δ)=0 contour loop
Kinetic-profile source
SLAYER reads kinetic profiles through the shared Equilibrium.read_kinetic_file reader (the same standardized object used by the kinetic/NTV physics), so there is one consistent interface for resistive and kinetic profiles.
profile_file– path to a kinetic-profile file (relative to the run dir), required when SLAYER is enabled. HDF5 (.h5) files use the GPEC kinetic schema and may carrychi_e(χ⊥) andchi_phi(χφ); ASCII tables are also accepted but carry no χ. When a χ dataset is absent or all-zero, the scalar `chiperp/chi_tor` fallbacks below are used (so a file can keep the χ keys set to 0 to defer to the scalars).profile_group– group within the HDF5 file (default"/")
Output control
store_scan– write the full Q/Δ scan grid to HDF5.falseby default to keep the output file small.
GeneralizedPerturbedEquilibrium.Tearing.Runner.SLAYERResult — Type
SLAYERResultOutput of run_slayer. Carries both summary eigenvalues (ωHz, γHz) and full diagnostic detail (valid roots, poles, filtered roots, contours) for downstream inspection and HDF5 output.
Fields
enabled–trueonly when the analysis actually rancontrol– theSLAYERControlused (frozen snapshot)params–Vector{SLAYERParameters}, one per surfacerational_psi,rational_q– normalized poloidal flux ψN and safety factor q of each analyzed surface, aligned withparams. Empty when the analysis was built from bare parameters (`runslayerfrominputs` without the surface list), in which case the HDF5 writer skips them.dp_matrix– outer-region Δ' matrix used in the analysisQ_root– tearing eigenvalue(s) in normalized Q- length
nsurfacesin:uncoupledmode - length
1in:coupledmode (global eigenvalue normalized byparams[1].tauk)
- length
omega_Hz,gamma_Hz– physical rotation frequency / growth rateper_surface_extraction–Vector{GrowthRateResult}of lengthnsurfacesin uncoupled mode (each includes polelines, pole list, valid roots, filtered roots). Empty in coupled mode.coupled_extraction– singleGrowthRateResultin coupled mode.nothingotherwise.layer_widths–Vector{LayerWidths}, one per surface: the resistive layer thickness (in meters) from thedel_sRiccati solve plus FKR / visco-resistive sanity scales. Empty when disabled.scan_data– scan results (per-surface in uncoupled, single entry in coupled). Empty unlesscontrol.store_scan == true.
GeneralizedPerturbedEquilibrium.Tearing.Runner.ggj_inner_deltas — Method
ggj_inner_deltas(params::AbstractVector{GGJParameters}, Q::Number;
solver=:galerkin) -> Vector{NamedTuple}Evaluate both parity channels of the GGJ inner-layer matching data at the complex normalized growth rate Q for each rational surface. Returns a vector of (ising, tearing, interchange) named tuples, where tearing (GWP Δ+) is the reconnecting channel and interchange (GWP Δ−) the non-reconnecting Glasser-stabilization channel (see InnerLayerResponse).
This is the supported GGJ diagnostic: both parity Δ's are physical and directly accessible here. Contour-matching γ extraction from these (the Re/Im scan intersection) is not yet validated — see the warning in run_slayer_from_inputs.
GeneralizedPerturbedEquilibrium.Tearing.Runner.run_slayer — Method
run_slayer(equil, surfaces, delta_prime_matrix, control; dir_path="./") -> SLAYERResultLoose-argument form of run_slayer, taking the equilibrium, the singular-surface vector and the outer-region Δ' matrix directly. Per-surface parameters are built via build_slayer_inputs; an empty or wrong-sized delta_prime_matrix falls back to a diagonal built from the sing.delta_prime stubs.
GeneralizedPerturbedEquilibrium.Tearing.Runner.run_slayer — Method
run_slayer(result, control; dir_path="./") -> SLAYERResultOrchestrate the full SLAYER analysis against a ForceFreeStates.ForceFreeStatesResult, reading its equilibrium, singular surfaces and Δ' matrix. Kinetic profiles are read from control.profile_file (relative to dir_path) through the shared Equilibrium.read_kinetic_file reader; when the file carries chi_e/chi_phi profiles they set χ⊥(ψ)/χφ(ψ), otherwise the scalar `control.chiperp/chi_tor` fallbacks are used.
The toroidal field comes from control.bt; leaving it unset (the default) makes build_slayer_inputs evaluate the physical B_T = F(ψ)/(2π·R₀) per surface.
Returns an enabled=false SLAYERResult when control.enabled is false.
GeneralizedPerturbedEquilibrium.Tearing.Runner.run_slayer_from_inputs — Method
run_slayer_from_inputs(params::Vector{SLAYERParameters},
dp_matrix::AbstractMatrix,
control::SLAYERControl) -> SLAYERResultRun the SLAYER tearing analysis given pre-built per-surface SLAYERParameters and the outer-region Δ' matrix. Bypasses the equilibrium-driven build_slayer_inputs step — use this when the parameters are already known (e.g. in unit tests or when rebuilding from cached HDF5 output).
GeneralizedPerturbedEquilibrium.Tearing.Runner.slayer_control_from_toml — Method
slayer_control_from_toml(section::AbstractDict) -> SLAYERControlParse a [SLAYER] TOML section into a SLAYERControl. Known nested subsections ([SLAYER.scan_grid], [SLAYER.amr], [SLAYER.growth_rate_filter]) are flattened into the top-level fields. Unknown keys raise an error so typos don't silently produce defaults.
GeneralizedPerturbedEquilibrium.Tearing.Runner.write_slayer_hdf5! — Method
write_slayer_hdf5!(parent::Union{HDF5.File,HDF5.Group},
result::SLAYERResult)Write result into a Tearing/ subgroup of parent. The subgroup is created if missing and overwritten if it already exists (keeps the output file reproducible across reruns).