Inner Layer Module

The InnerLayer module solves the resistive inner-region problem of matched-asymptotic resistive-MHD stability: the thin layer around a rational surface where resistivity, inertia and the ideal singularity balance. Its output is the pair of parity-projected matching data $(\Delta_\mathrm{odd}, \Delta_\mathrm{even})$ that the outer ideal region (DCON/GPEC) matches to in order to obtain resistive growth rates, tearing $\Delta'$, and resistive interchange stability.

The module provides an abstract InnerLayerModel interface and one concrete model so far, the GGJ (Glasser–Greene–Johnson) layer, with three interchangeable solver backends. Future inner-layer models (SLAYER, 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

Benchmark surface used in the figures

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:

backendmethodrobust regime
:shootingbackward stable shoot from $X_\mathrm{max}\to 0$$\lvert Q\rvert \ll 1$
:galerkinHermite-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:

Relative error of the :shooting and :galerkin backends against the :ray reference along the imaginary-Q axis, on the q=4 benchmark surface. Each curve's crossing of the 1% line marks that method's practical reach.

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.

The rotated integration ray in the complex layer-coordinate plane at Q = 500i. The real-axis contour runs through the pseudo-resonance x² = −Q²(G+KF); the ray at θ = arg(Q)/4 = 22.5° clears it.

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.

Inner-layer fields Ψ, Ξ, Υ on the rotated ray for the q=4 surface at Q = 2i. The collocation solution (solid) joins the asymptotic representation (dashed) seamlessly at the match point S = s_m.

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:

quantityvalue 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.

Relative change of Δ at Q = 500i under independent perturbations of each numerical knob (contour angle, spectral order, series order/radius, refinement depth, march tolerance, handoff radius, purification depth). The worst-case spread is the reported error bar.

Choosing a backend

:ray is the defaultGGJModel() 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_cond

API Reference

InnerLayer

GeneralizedPerturbedEquilibrium.InnerLayer.InnerLayerModelType
InnerLayerModel

Abstract 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.

source
GeneralizedPerturbedEquilibrium.InnerLayer.solve_innerFunction
solve_inner(model::InnerLayerModel, params, γ::ComplexF64; kwargs...) -> SVector{2,ComplexF64}

Compute the parity-projected matching data (Δ_odd, Δ_even) for the given inner-layer model, physical parameters params, and complex growth rate γ. Concrete models specialize this function.

The two returned components correspond to the homogeneous odd / even parity solutions of the half-domain inner-layer problem (parity boundary conditions imposed at the rational surface, X = 0). They are the Δ_{j,±}(γ) of Glasser, Wang & Park, Phys. Plasmas 23, 112506 (2016), Eqs. (34)–(35).

source

GGJ

GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.GGJModelType
GGJModel{S} <: InnerLayerModel

Glasser–Greene–Johnson resistive inner-layer model. The type parameter S selects the solver: :ray (default) for the rotated-contour collocation solver (robust at large |Q| on/near the imaginary axis; agrees with :galerkin within its error bar at moderate Q), :galerkin for the Hermite-cubic finite element solver (real-axis method; degrades for |Q| ≳ 1), and :shooting for the backward stable-shoot solver (|Q| ≪ 1 only). All implementations consume the same inps asymptotic-basis kernel and return the parity-projected matching data in the same convention. Note the backends take different numerical-knob keywords: pass GGJModel(solver=:galerkin) explicitly when using nx/xfac-style Galerkin knobs.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.GGJParametersType
GGJParameters

Dimensionless 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 are the same as the Fortran resist_type:

fieldmeaning
EGlasser interchange parameter (enters Mercier D_I = E+F+H−¼)
FGlasser interchange parameter
GCoupling coefficient (curvature × pressure gradient)
HPfirsch–Schlüter coefficient
KGlasser parameter
MMercier-related auxiliary parameter (held but not used here)
tauaLocal Alfvén time at the rational surface
taurLocal resistive time at the rational surface
v1Linear scale factor used in the V₁ rescaling
isingIndex of the singular surface (traceability only)

The complex growth rate γ is not stored here; it is passed as a separate argument to solve_inner.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.InnerAsymptoticsCacheType
InnerAsymptoticsCache

Frozen state of the inps Wasow asymptotic-basis construction for a single (GGJParameters, Q) pair. All matrices are stored as SMatrix/SVector so the evaluator can run allocation-free on a hot path.

Index convention: P[k+1] holds the k-th-order matrix P_k, B[k+1] holds B_k, etc., for k = 0, 1, …, the upper bound documented in each field.

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).
source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ._physical_ua_duaMethod

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 used by deltac/inpso. 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 (Ψ, Ξ, Υ).

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ._physical_uvMethod

Build the (I, U, V) coefficient matrices of the second-order system I·u'' − V·u' − U·u = 0 for u = (Ψ, Ξ, Υ) at coordinate x. Port of inpsogetuv. 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))

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ._solve_paritiesMethod
_solve_parities(Ic, Jc, Vc, rhs, ndof, Ng, 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 (odd: unit entries at components (4,2,3); even: at (1,5,6)), i.e. Aeven = Aodd + Σₖ e{rₖ}(e{c2ₖ} − e_{c1ₖ})ᵀ — a rank-3 update. One sparse LU (of the row-equilibrated odd system) plus a Woodbury correction (3 extra triangular solves + a 3×3 solve) replaces the second factorization, which dominates the linear-algebra cost.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.asymptotic_profileMethod
asymptotic_profile(params, res::RaySolveResult, srange) -> (; s, x, Ψ, Ξ, Υ)

Evaluate the ANALYTIC representation u_small + Δ·u_big on the ray for s ≥ res.S (where the inps series is trusted; decaying-exponential content is machine-dead there). Concatenating with solution_profile demonstrates the seamless numeric ↔ asymptotic match — the same overlap the outer-region matching relies on.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.asymptotic_residualMethod
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). Mirrors inps_delta: 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).

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.boundary_basisMethod
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.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.build_asymptoticsMethod
build_asymptotics(params::GGJParameters, Q::ComplexF64; kmax::Int=8) -> InnerAsymptoticsCache

Construct the inps Wasow asymptotic basis for the given GGJ parameters and dimensionless growth rate Q. Truncates each power series at order kmax (default 8). The returned cache can be evaluated at any x > 0 via evaluate_asymptotics and queried for an adaptive X_max via pick_xmax.

Reference: Glasser & Wang, Phys. Plasmas 27, 012506 (2020), Eqs. 7–53.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.decaying_pairMethod
decaying_pair(params, Q, θ, S; growth=30.0, seed=20260707) -> E::Matrix{ComplexF64} (6×2)

Orthonormal basis of the forward-decaying pair at s = S on the ray, via a SHORT backward RK4 integration over [S, S+ΔS], with ΔS chosen so the pair is amplified by ≈ e^growth relative to everything else (backward-attracting purification of random seeds). The integration is fully resolved (h·rate ≈ 0.05), so plain RK4 is stable over this short stretch.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.delta_convergenceMethod
delta_convergence(params, Q; verbose=true, refine_tol=1e-9,
                  max_rounds=16, max_cells=12000, kwargs...)
    -> (; Δ, spread, base, table)

Convergence battery for the matching data: solve at a trusted baseline, then re-solve with every numerical knob perturbed on an INDEPENDENT axis, and report the relative change of (Δ₁, Δ₂) per knob. The knobs probe orthogonal error sources, so the worst-case spread is an honest error bar on Δ:

  • θ → 1.2θ : contour angle — Δ is an analytic invariant of the ray, so any drift is pure numerical error (sharpest test). OUTWARD: the natural angle maximizes clearance from the x² ≈ −Q²(G+KF) pseudo-resonance, so inward checks measure their own degraded solve, not the baseline;
  • p → p+4 : spectral-element order (interior discretization);
  • kmax+4, smax_tol/10 : series order and matching radius S (far-field BC);
  • refine_tol/10 : mesh refinement depth;
  • march_rtol/100 : outer-region quotient-march tolerance;
  • sm_fac 1→1.4 : BVP/march handoff radius;
  • growth 30→45 : decaying-pair purification e-folds.

Returns the baseline Δ, the per-parity worst-case relative spread, the baseline result struct, and the per-knob table (name, δΔ₁, δΔ₂).

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.evaluate_asymptoticsMethod
evaluate_asymptotics(cache, x::Complex; derivative=true, apply_T=true) -> (U, dU)

Evaluate the inps asymptotic basis at complex x (GW2020 Eq. 53; principal branch, |arg x| < π). Returns the 6×2 matrix U whose columns are the two power-like asymptotic solutions in the GW2020 Eq. (2) state convention u = (xΨ, Ξ, Υ, (xΨ)'/x, Ξ'/x, Υ'/x), and their x-derivatives dU if requested. apply_T=false returns the pre-Jordan-basis representation used by the residual measure.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.evaluate_asymptoticsMethod
evaluate_asymptotics(cache::InnerAsymptoticsCache, x::Real;
                    derivative::Bool=true, apply_T::Bool=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 inps_delta for residual checks). The default apply_T=true returns the solutions in the original 6-component first-order-system basis used by inpso_get_uv and the shooting / Galerkin solvers.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.glasser_wang_2020_eq55Method
glasser_wang_2020_eq55() -> GGJParameters

D-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 a Fortran rmatch/INPS run, not this paper alone.

Timescale parameters (taua, taur, v1) are set to canonical normalization; callers should override them for physical cases.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.initial_breaksMethod
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.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.inner_QMethod
inner_Q(p::GGJParameters, γ::Number) -> ComplexF64

Dimensionless scaled inner-layer growth rate Q = γ / Q₀ (GWP2016 Eq. A15), the eigenvalue appearing in the inner-region equations (Eq. 11) and the inps Wasow basis. The argument γ may be real or complex; the result is always complex.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.march_boundaryMethod
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 the matching data Δ is defined). Adaptive 2-stage Radau IIA (L-stable, stiffly accurate) on the raw linear system, with Φ-budgeted purges against locally-extracted decaying frames; see the file header for why explicit (including projected-QR) marchers cannot do this job at large S.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.ode_matrixMethod
ode_matrix([CT,] p::GGJParameters, Q, x) -> SMatrix{6,6,CT}

Coefficient matrix M(x) of dv/dx = M v, v = (Ψ, Ξ, Υ, Ψ', Ξ', Υ'), valid for real or complex x. The optional leading type argument selects the element type (e.g. Complex{Double64} for the extended-precision damped-zone march); default ComplexF64.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.p1Method
p1(p::GGJParameters) -> Float64

p₁ = √(−D_I), the Mercier power that sets the large-x Frobenius exponents r± = 3/2 ± √(−D_I) (GW2020 Eq. 49; μ = −1/2 ± √(−DI) in GWP2016 Eq. 26). The Mercier-stable branch requires `DI < 0`; this function asserts it.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.parity_rowsMethod
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)
source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.physical_ua_duaMethod
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, column 2 = small). Same map as the deltac inpso_get_ua/dua convention and the Galerkin backend's _physical_ua_dua, generalized to complex x.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.pick_smaxMethod
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: sweep the ray parameter s log-uniformly and return the smallest s at which the series residual along x = e^{iθ}s drops below eps. If the target is never reached, returns the residual- minimizing s with achieved = false (caller should warn). Also returns the asymptotics cache for reuse.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.pick_xmaxMethod
pick_xmax(params::GGJParameters, Q::ComplexF64;
          eps::Float64=1e-7, kmax::Int=8,
          xlogmin::Float64=-1.0, xlogmax::Float64=4.0,
          dxlog::Float64=0.01) -> (Float64, InnerAsymptoticsCache)

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. Mirrors inps_xmax.

Throws an ErrorException if no x in the sweep range achieves the target tolerance.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.q4_surface_benchmarkMethod
q4_surface_benchmark() -> GGJParameters

Physical 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).

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.rescale_deltaMethod
rescale_delta(Δ, p::GGJParameters) -> SVector{2,ComplexF64}

Map the scaled inner-layer matching data back to physical Δ at the rational surface by the X₀^(−2√(−D_I)) = S^(2√(−D_I)/3) rescaling, together with the v₁^(2√(−D_I)) linear-scale factor, implied by the power-like matching (GWP2016 Sec. IV; the inner solution ~ X^{r±} converts to physical x = X₀ X, so the large/small amplitude ratio Δ scales by X₀^{−(r₊−r₋)} = X₀^(−2√(−DI))). Operates element-wise on a 2-vector of `(Δodd, Δ_even)`.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.solution_profileMethod
solution_profile(res::RaySolveResult; npc=8) -> (; s, x, Ψ, Ξ, Υ)

Reconstruct the physical inner-layer fields (Ψ, Ξ, Υ) on the solution contour, npc points per cell over the collocation domain [0, sm]. Output layout mirrors the legacy `solveinner_profile: each field is annpts × 2matrix with columns (isol=1 "odd", isol=2 "even");x = e^{iθ}s` is the (complex) layer coordinate. For real Q (θ = 0) this is the real-axis profile directly; for rotated solves the fields live on the ray — analytic continuation back to real x is a separate (unstable) problem, by design.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.solve_inner_convergedMethod
solve_inner_converged(::GGJModel{:galerkin}, params::GGJParameters, γ::Number;
                      rtol=1e-2, kmax0=12, xfac0=1.5, cells_per_unit=3.0,
                      nx_min=1024, nx_max=8192, kmax_step=2, kmax_max=28,
                      xfac_growth=1.5, max_levels=6, nq=6, pfac=1.0, cutoff=8, tol_res=1e-4)
    -> (; delta, converged, err, kmax, xfac, nx, nlevels)

Convergence-guarded GGJ inner-layer solve: only returns a Δ once it is stable under joint refinement of the three coupled accuracy knobs — series order kmax, asymptotic reach xfac, and grid resolution nx. Successively refines all three (raising kmax clears the high-|Q| series floor; raising xfac clears the reach floor; nx is scaled with xmax to hold cells-per-unit-x ≈ cells_per_unit, which prevents the grid-starvation breakup that otherwise corrupts Δ at large xfac/high |Q|) until the per-component relative change of (Δ₁, Δ₂) drops below rtol, or max_levels is hit (then converged=false).

The metric is per real/imag component with a significance floor (5% of |Δᵢ|), so a converged norm cannot mask a wrong reactive part Re(Δ₁) — the failure mode under-reach produces (Im dominates |Δ₁|).

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.solve_inner_profileMethod
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.

source
GeneralizedPerturbedEquilibrium.InnerLayer.GGJ.solve_rayMethod
solve_ray(params::GGJParameters, Q; θ=angle(Q)/4, kmax=12, p=12,
          S=nothing, smax_tol=1e-9, growth=30.0,
          refine_tol=1e-8, max_rounds=8, max_cells=6000,
          verbose=false) -> RaySolveResult

Solve the GGJ inner-layer matching problem on the rotated ray x = e^{iθ}s and return the parity matching data. Δ follows the deltac output convention (swap + rescale_delta) so it is directly comparable to the GPEC_julia Galerkin solver.

Keyword highlights:

  • θ : contour angle; default arg(Q)/4 makes the parabolic exponent exactly real. θ = 0 reproduces a real-axis solve.
  • S : matching radius; default from the inps series residual criterion along the ray (pick_smax).
  • p : polynomial order per spectral element.
  • refine_tol/max_rounds: residual-driven bisection refinement control.
source
GeneralizedPerturbedEquilibrium.InnerLayer.solve_innerMethod
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)
            -> SVector{2,ComplexF64}

Solve the GGJ inner-layer matching problem using the Hermite-cubic finite element (Galerkin) method (GWP2016 Sec. III). Direct port of rmatch/deltac.f 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. The ordering matches deltac.f's output convention (swapped relative to deltar.f).

source
GeneralizedPerturbedEquilibrium.InnerLayer.solve_innerMethod
solve_inner(::GGJModel{:ray}, params::GGJParameters, γ::Number; kwargs...)
    -> SVector{2,ComplexF64}

Solve the GGJ inner-layer matching problem with the rotated-ray collocation backend and return the parity-projected matching data (Δ₁, Δ₂) in the same convention as the :shooting and :galerkin backends (deltac output swap + X₀^{2√(−D_I)} physical rescale applied), directly interchangeable with them. Converts γ via inner_Q and forwards all keywords to solve_ray; use solve_ray directly when the full RaySolveResult (raw Δ, contour, mesh, nodal solution, residuals) is wanted.

Preferred backend for |Q| ≳ 1 and for Q near the imaginary axis (rotation / resistivity scans): validated to |Q| = 500 on the imaginary axis, where the other backends fail. At |Q| ≪ 1 all three backends agree; :ray remains accurate but :galerkin may be faster for very small |Q| real Q.

source
GeneralizedPerturbedEquilibrium.InnerLayer.solve_innerMethod
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()) -> SVector{2,ComplexF64}

Solve the GGJ inner-layer matching problem by stable backward shooting in the origin-diagonalized 4×4 basis. Direct port of the rmatch deltar.f algorithm.

Returns the parity-projected matching data (Δ₁, Δ₂) (already rescaled back to physical units via rescale_delta). Index ordering matches the Fortran deltar output.

Tolerances reltol/abstol are the integrator tolerances; rtol_origin controls the truncation error of the origin Frobenius series and the choice of tmin.

source