Skip to content

Commit 85aaf58

Browse files
authored
Merge pull request #238 from OpenFUSIONToolkit/feature/tearing-growthrates
Feature: add SLAYER and GGJ tearing growth rates
2 parents 167ada7 + f46b202 commit 85aaf58

65 files changed

Lines changed: 8437 additions & 238 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ pestotv
2828
regression-harness/*.sqlite
2929
regression-harness/*.sqlite-shm
3030
regression-harness/*.sqlite-wal
31+
scratch/
32+
.claude/worktrees/
33+
.claude/agent-memory/
34+
35+
# Local profiling scratch (one-off study scripts, not part of the package)
36+
profiling/

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ version = "0.1.0"
77
[deps]
88
AdaptiveArrayPools = "4f381ef7-9af0-4cbe-99d4-cf36d7b0f233"
99
Contour = "d38c429a-6771-53c6-b99e-75d170b6e991"
10+
DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df"
1011
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
1112
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
1213
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
@@ -36,6 +37,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3637
[compat]
3738
AdaptiveArrayPools = "0.3.5"
3839
Contour = "0.6.3"
40+
DelaunayTriangulation = "1.6.6"
3941
DelimitedFiles = "1.9.1"
4042
DiffEqCallbacks = "4.9.0"
4143
Documenter = "1.14.1"

docs/make.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ makedocs(;
3636
"KineticForces" => "kinetic_forces.md",
3737
"Forcing Terms" => "forcing_terms.md",
3838
"Perturbed Equilibrium" => "perturbed_equilibrium.md",
39-
"Inner Layer" => "inner_layer.md",
39+
"Tearing" => "inner_layer.md",
4040
"Analysis" => "analysis.md",
4141
"Utilities" => "utilities.md"
4242
],

docs/src/developer_notes.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,98 @@ Where CODE is the module name (EQUIL, ForceFreeStates, VAC, PERTURBED EQUILIBRIU
2020

2121
The regression harness **must be run on every pull request before merging into `develop`**. It is the project's primary safeguard for tracking how numerical results evolve across changes, so it is only useful if every PR exercises it. When you open a PR, paste the regression report into the PR thread so reviewers can see what moved (and what did not). If your change touches a quantity that is not yet tracked, add a new regression case — or extend an existing one — in the same PR.
2222

23+
### Open problem: pinning grid-sensitive Δ′ robustly
24+
25+
The ideal-MHD Δ′ values pinned in `test/runtests_parallel_integration.jl`
26+
(`delta_prime_matrix` diagonal) and tracked by the harness are **single-point
27+
snapshots**: one value at one `psi_accuracy` on one grid. They exist to catch
28+
unintended changes, and are explicitly *not* converged Δ′. The extraction is
29+
intrinsically grid-sensitive — a `psi_accuracy` scan from 2e-3 to 2.5e-4 swings
30+
the DIII-D-like `dpm[1,1]` by roughly 50% (about 6.2 to 9.9), and switching the
31+
grid generator moves it again. Any such pin therefore encodes an arbitrary point
32+
on a varying curve, and re-pinning is required whenever the grid changes, which
33+
weakens it as a regression signal.
34+
35+
A more defensible criterion is to pin the **plateau**: scan Δ′ across
36+
`psi_accuracy` and the integration-truncation controls, and take the value where
37+
the result is stationary — the mode of the resulting distribution rather than any
38+
single sample. Where a plateau exists it is a property of the physics rather than
39+
of the discretization, so it would survive grid changes and would be a genuine
40+
convergence statement.
41+
42+
A `psi_accuracy` scan on the DIII-D-like SLAYER deck (2e-3 down to 3.125e-5, a
43+
64x range) shows what a plateau search is up against:
44+
45+
| psi_accuracy | knots | implied | dpm[1,1] | dpm[2,2] | dpm[3,3] | gamma 2/1 (Hz) |
46+
|---|---|---|---|---|---|---|
47+
| 2.0e-3 | 172 || 6.850 | -5.783 | -16.308 | 165.3 |
48+
| 1.0e-3 | 205 || 6.387 | -5.873 | -16.867 | 154.3 |
49+
| 5.0e-4 | 252 || 8.003 | -6.105 | -16.413 | 192.9 |
50+
| 2.5e-4 | 307 | 522 | 8.567 | -6.235 | -16.529 | 206.2 |
51+
| 1.25e-4 | 372 | 666 | 8.146 | -6.285 | -16.649 | 196.2 |
52+
| 6.25e-5 | 457 | 916 | 8.911 | -6.477 | -16.510 | 214.4 |
53+
| 3.125e-5 | 559 | 1226 | 9.514 | -6.260 | -16.485 | 228.7 |
54+
55+
Three things follow. First, convergence is per-surface: the outer surface
56+
`dpm[3,3]` (q=4) *does* plateau under the auto grid — the last four points agree
57+
to about 1% — while `dpm[2,2]` is marginal and `dpm[1,1]` (q=2) never settles,
58+
still moving ~7% between the two tightest grids. A plateau detector must
59+
therefore report per-surface rather than pass/fail for the whole diagonal.
60+
(As the `ldp` scan below shows, q=2 is not inherently unconvergeable — it is the
61+
auto grid that prevents it from settling.)
62+
63+
Second, the growth rate is linear in Δ′: `gamma/dpm[1,1]` is 24.1 to within 0.5%
64+
across the whole scan, so a converged Δ′ is both necessary and sufficient for a
65+
converged SLAYER growth rate on this surface.
66+
67+
Third — and this is the blocker — the `implied` column (what
68+
`implied_knot_count` requests after the refined solve) grows monotonically
69+
relative to the grid actually used, from 1.7x to 2.2x. The two-pass scheme stops
70+
after pass 2, so tightening `psi_accuracy` moves it *further* from
71+
self-consistency rather than closer, and the warning's advice to "consider
72+
tightening psi_accuracy" is counterproductive in this regime.
73+
74+
The same deck on a deterministic `ldp` grid, which skips the measure-and-re-form
75+
step entirely, converges:
76+
77+
| mpsi | dpm[1,1] | dpm[2,2] | dpm[3,3] | gamma 2/1 (Hz) |
78+
|---|---|---|---|---|
79+
| 128 | 5.208 | -8.666 | -19.157 | 126.2 |
80+
| 256 | 7.097 | -5.187 | -15.729 | 171.2 |
81+
| 512 | 8.489 | -6.217 | -16.517 | 204.4 |
82+
| 1024 | 8.932 | -6.298 | -16.529 | 214.9 |
83+
| 2048 | 8.914 | -6.385 | -16.468 | 214.5 |
84+
85+
Successive `dpm[1,1]` increments are +1.889, +1.392, +0.443, -0.018: the last
86+
doubling moves both Δ′ and the growth rate by 0.2%. So the q=2 Δ′ is **not**
87+
intrinsically ill-conditioned — it converges to ≈8.91, with gamma ≈214.5 Hz, and
88+
`dpm[3,3]` converges to ≈-16.5 on *both* grid families, which is what a physical
89+
value should do. The non-convergence under the auto grid is an artifact of the
90+
generator, not of the Δ′ extraction.
91+
92+
Two consequences. A plateau criterion is implementable today against a fixed
93+
`ldp` grid, without waiting on the auto-grid work. And the auto grid's answers
94+
are biased in both directions relative to the converged value: at its default
95+
`psi_accuracy` it gave 6.39 (28% low), at its tightest 9.51 (7% high). Anything
96+
pinned on the auto grid should be read with that in mind.
97+
98+
This is not implemented. Doing it properly needs:
99+
100+
- either a fixed `ldp` grid (which already converges, see above) or knot
101+
refinement iterated to a fixed point (repeat the measure-and-re-form
102+
step until `implied_knot_count` stops exceeding the grid in use), since
103+
without it the scan target keeps moving;
104+
- a scan driver over `psi_accuracy` × truncation (`psihigh`, `dmlim`, `qhigh`)
105+
that records the Δ′ diagonal per configuration;
106+
- a plateau/mode detector with an explicit stationarity tolerance, plus a
107+
defined failure mode for surfaces where no plateau exists (the
108+
near-separatrix surfaces are expected to fall in this class);
109+
- a harness case that pins plateau values and their scan width, replacing the
110+
single-point pins.
111+
112+
Until then, treat the pinned diagonal as an order-of-magnitude and sign
113+
diagnostic only, and expect to re-pin it whenever the equilibrium grid changes.
114+
23115
### Regression Harness: Quick usage guide
24116

25117
Set up an alias for convenience (optional):

docs/src/inner_layer.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
# Inner Layer Module
1+
# Tearing Module
22

3-
The InnerLayer module provides abstract scaffolding for resistive inner-layer
4-
models used in matched asymptotic expansions for resistive MHD stability
5-
analysis. It currently includes the GGJ (Glasser–Greene–Johnson) shooting
6-
method for computing the inner-layer response.
3+
The `Tearing` module groups the resistive tearing-mode analysis stack:
4+
`InnerLayer` (per-surface inner-layer matching data Δ(Q) for the GGJ and
5+
SLAYER models), `Dispersion` (physics-agnostic complex-plane scan and
6+
contour-intersection root extraction), and `Runner` (user-facing TOML
7+
configuration, profile loading, and HDF5 output).
8+
9+
## Layer Inputs
10+
11+
Equilibrium/ForceFreeStates glue that assembles per-surface inner-layer inputs.
12+
13+
```@autodocs
14+
Modules = [GeneralizedPerturbedEquilibrium.Tearing]
15+
```
716

817
## InnerLayer
918

@@ -16,3 +25,21 @@ Modules = [GeneralizedPerturbedEquilibrium.InnerLayer]
1625
```@autodocs
1726
Modules = [GeneralizedPerturbedEquilibrium.InnerLayer.GGJ]
1827
```
28+
29+
## SLAYER
30+
31+
```@autodocs
32+
Modules = [GeneralizedPerturbedEquilibrium.InnerLayer.SLAYER]
33+
```
34+
35+
## Dispersion
36+
37+
```@autodocs
38+
Modules = [GeneralizedPerturbedEquilibrium.Dispersion]
39+
```
40+
41+
## Runner
42+
43+
```@autodocs
44+
Modules = [GeneralizedPerturbedEquilibrium.Runner]
45+
```

docs/src/utilities.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ The Utilities module currently provides:
1414
Modules = [GeneralizedPerturbedEquilibrium.Utilities, GeneralizedPerturbedEquilibrium.Utilities.FourierTransforms]
1515
```
1616

17+
## Physical Constants
18+
19+
```@autodocs
20+
Modules = [GeneralizedPerturbedEquilibrium.Utilities.PhysicalConstants]
21+
```
22+
23+
## Neoclassical Resistivity
24+
25+
Parallel-resistivity closures (Spitzer, Spitzer-Härm, and the Sauter and Redl
26+
neoclassical models) used to set the Lundquist number in the tearing stack.
27+
28+
```@autodocs
29+
Modules = [GeneralizedPerturbedEquilibrium.Utilities.NeoclassicalResistivity]
30+
```
31+
1732
## IMAS Output
1833

1934
```@docs
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# DIII-D-like SLAYER tearing-mode growth-rate example.
2+
# Reuses the equilibrium and H-mode kinetic profiles of the sibling
3+
# DIIID-like_ideal_example (geqdsk referenced by relative path; not
4+
# duplicated). Runs equilibrium + ForceFreeStates + SLAYER and skips
5+
# PerturbedEquilibrium (ForceFreeStates.force_termination = true).
6+
7+
[Equilibrium]
8+
eq_filename = "../DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode.geqdsk" # Path to equilibrium file
9+
eq_type = "efit" # Type of the input 2D equilibrium file
10+
jac_type = "hamada" # Coordinate system (hamada, pest, boozer, equal_arc)
11+
power_bp = 0 # Poloidal field power exponent for Jacobian
12+
power_b = 0 # Toroidal field power exponent for Jacobian
13+
power_r = 0 # Major radius power exponent for Jacobian
14+
grid_type = "log_asymptotic" # Radial grid packing type
15+
psilow = 1e-4 # Lower limit of normalized flux coordinate
16+
psihigh = 0.9995 # Upper limit of normalized flux coordinate
17+
mpsi = 0 # Number of radial grid points (0 = auto-compute from psi_accuracy)
18+
psi_accuracy = 0.001 # Target absolute error in q for auto-mpsi
19+
mtheta = 256 # Number of poloidal grid points
20+
newq0 = 0 # Override for on-axis safety factor (0 = use input value)
21+
etol = 1e-10 # Error tolerance for equilibrium solver
22+
force_termination = false # Terminate after equilibrium setup (skip stability calculations)
23+
24+
[Wall]
25+
shape = "nowall" # Wall shape (nowall, conformal, elliptical, dee, mod_dee, filepath)
26+
a = 0.2415 # Distance from plasma (conformal) or shape parameter
27+
aw = 0.05 # Half-thickness parameter for Dee-shaped walls
28+
bw = 1.5 # Elongation parameter for wall shapes
29+
cw = 0 # Offset of wall center from major radius
30+
dw = 0.5 # Triangularity parameter for wall shapes
31+
tw = 0.05 # Sharpness of wall corners (try 0.05 as initial value)
32+
equal_arc_wall = true # Equal arc length distribution of nodes on wall
33+
34+
[ForceFreeStates]
35+
force_termination = true # Run FFS + SLAYER, skip PerturbedEquilibrium
36+
local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile
37+
mat_flag = true # Construct coefficient matrices for diagnostic purposes
38+
ode_flag = true # Integrate ODEs for stability of the internal long-wavelength mode (must be true for GPEC)
39+
vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes
40+
41+
psiedge = 0.99 # Edge dW scan band: dW(ψ) computed for ψ ∈ [psiedge, psilim], integration truncated at peak
42+
qlow = 1.02 # Integration initiated at q determined by min(q0, qlow)...
43+
qhigh = 1e3 # Integration terminated at q limit determined by min(qa, qhigh)...
44+
sing_start = 0 # Start integration at the sing_start'th rational from the axis (psilow)
45+
46+
nn_low = 1 # Smallest toroidal mode number to include
47+
nn_high = 1 # Largest toroidal mode number to include
48+
delta_mlow = 8 # Expands lower bound of Fourier harmonics
49+
delta_mhigh = 8 # Expands upper bound of Fourier harmonics
50+
mthvac = 512 # Number of points used in splines over poloidal angle at the plasma-vacuum interface
51+
52+
kinetic_source = "fixed" # Kinetic matrix source: "fixed" test matrices, or "calculated" from the kinetic NTV model
53+
kinetic_factor = 0.0 # Scaling of kinetic matrices (0 = ideal path; >0 enables kinetic mode)
54+
eulerlagrange_tolerance = 1e-10# Relative tolerance for ODE integration of Euler-Lagrange equations
55+
save_interval = 3 # Save every Nth ODE step (1=all, 10=every 10th). Always saves near rational surfaces.
56+
singfac_min = 1e-4 # Fractional distance from rational q at which ideal jump enforced
57+
ucrit = 1e4 # Maximum fraction of solutions allowed before re-normalized
58+
59+
# Δ' BVP + parallel integration (see ForceFreeStatesControl docstring for details)
60+
use_parallel = true # Run parallel FM-propagator BVP path (unlocks singular/delta_prime_matrix)
61+
parallel_threads = 1 # serial/bit-deterministic BVP — keeps the regression Δ' (and hence γ) reproducible
62+
populate_dense_xi = false # No PerturbedEquilibrium here; the dense EL pass has no consumer (auto-disabled under force_termination anyway). SLAYER needs only delta_prime_matrix from the parallel BVP.
63+
truncate_at_dW_peak = false # Edge-dW scan stays diagnostic; integration domain set by qhigh / psihigh / dmlim
64+
set_psilim_via_dmlim = true # TRUE for diverted geqdsks — q → ∞ at separatrix, so dmlim truncation avoids the δW kink instability at negligible domain cost
65+
dmlim = 0.2 # Truncate integration at (last_rational_q + dmlim) / n
66+
67+
[SLAYER]
68+
# SLAYER tearing-mode growth-rate analysis on the DIII-D-like H-mode equilibrium.
69+
# Runs in the force_termination path (PE skipped). Kinetic profiles (incl. the
70+
# χ⊥/χ_φ heat/momentum diffusivities) are read from the standardized HDF5
71+
# kinetic file shared with the kinetic/NTV physics. Per-surface (uncoupled)
72+
# analysis: the headline is the unstable 2/1; the validity gate drops surfaces
73+
# with no real root (e.g. the 5/1, whose Δ' BVP yields a huge uncancellable Δ').
74+
enabled = true
75+
inner_model = "slayer_fitzpatrick"
76+
scan_mode = "amr"
77+
coupling_mode = "uncoupled"
78+
dc_type = "none"
79+
mu_i = 2.0
80+
zeff = 1.0
81+
chi_perp = 1.0 # fallback only; the kinetic file supplies χ⊥(ψ)
82+
chi_tor = 1.0 # fallback only; the kinetic file supplies χ_φ(ψ)
83+
pole_threshold_adaptive = true # SLAYER |Δ| spans many orders; adapt to 10·median(|Δ|)
84+
filter_above_poles = true
85+
filter_outside_re = true
86+
polish_roots = true # refine each root to the true zero; enables the validity gate
87+
store_scan = false # per-surface scans omitted from HDF5 (uncoupled has one per surface)
88+
# Standardized kinetic-profile file (GPEC HDF5 kinetic schema), shared with the
89+
# sibling ideal example; carries n_e, T_e, T_i, omega_E, chi_e (χ⊥), chi_phi (χ_φ).
90+
profile_file = "../DIIID-like_ideal_example/TkMkr_D3Dlike_Hmode_kinetic.h5"
91+
92+
[SLAYER.scan_grid]
93+
Q_re_range = [-2.0, 2.0]
94+
Q_im_range = [-0.5, 3.0]
95+
nre = 41
96+
nim = 31

examples/Solovev_ideal_example/gpec.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ verbose = true # Enable verbose logging
4444
write_outputs_to_HDF5 = true # Write perturbed equilibrium outputs to HDF5
4545
reg_spot = 0.05 # Regularization width for singular surfaces (0 = disabled)
4646

47+
# Note: SLAYER tearing analysis is not run on the Solovev analytic equilibrium
48+
# — its Δ' / inner-layer dispersion does not yield meaningful tearing roots.
49+
# The SLAYER example and regression case use the DIII-D-like geqdsk instead
50+
# (see examples/DIIID-like_SLAYER_example).
51+
4752
[ForceFreeStates]
4853
local_stability_flag = true # Perform local stability analysis (Mercier and ballooning) across the ψ profile
4954
vac_flag = true # Compute plasma, vacuum, and total energies for free-boundary modes

0 commit comments

Comments
 (0)