Repository navigation
Tearing - FEATURE - Add critical resonant field calculations to the tearing workflow - #415
Conversation
Sounds like this should be marked draft until this is done
"Should" is scary - benchmark it! There is a benchmark script for comparing to fortran - modify that for your needs (using scratch scripts - no need to commit run scripts)
Mark this as draft until the IO and benchmarks are done then ping us for reviews. You can make a "Task" type issue describing future plans (and assign yourself) so we have it on record as work in progress. |
Pull request was converted to draft
Review (combining my findings with Claude's)Structure is sound and Cole Eq. 62 is the right target. Physics claims below were checked numerically against a DIII-D-like 2/1 Checked, no action needed
Blocking
Should fix
Example is internally inconsistent
Separately, Merge readiness
Cleanup
Hygiene
SummaryCore calculation holds up better than expected: conventions consistent, branch lands where it should, α is a 4% effect. Before sign-off I'd want the item-1 test, κ̂, the example's viscous_input_type mismatch plus a re-run figure, and conflicts resolved with a fresh harness run. Cleanup can ride along. Drafted with assistance from Claude Code; physics verified numerically against origin/feature/b_crit_calculation. |
|
Chiming in without having read all of the more thorough review 😝
|
Take develop's version of every file outside the critical resonant field code: drops the formatter and whitespace churn, the edits to ForceFreeStates files develop has reorganized, the notebook, the Cole PDF, the test_ang_mom_diff kinetic dataset and the .gitignore entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e on Cole's Q axis solve_inner mirrors the real-Q axis, so the torque balance paired a mirrored Δ with an unmirrored Q0 and put the diamagnetic poles at −Q_e, −Q_i. Map back with conj(Δ(−Q)) so the branch lies between Q_e and Q0 as in Cole and Fitzpatrick 2006 and Fortran gslayer.f. Replace the two-largest-samples search with true local maxima, reject maxima within one grid step of a pole, derive the scan window from Q0, Q_e, Q_i as gslayer.f does, and carry κ̂ explicitly (held at 2/s² for Fortran parity). Fix the header equation and attach the docstrings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ithout the removed pole exclusion Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…omparison with the total resonant field Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…assumes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@d-burg @ebursch — I investigated the threshold selection using actual SLAYER responses. There is a reproducible case where the torque curves contain a saddle-node bifurcation of the higher-rotation equilibrium, but the current selector returns ReproducerThis uses the SLAYER layer parameters extracted from the DIII-D-like example's 3/1 surface, with Q₀ = 2 and α = 0.48. These are controlled changes to the rotation and outer stability input, not a self-consistent new equilibrium. The unmodified example does not exhibit this failure. The code tested was The net-torque curves show a saddle-node bifurcation at approximately:
In this plot, each curve holds the vacuum resonant driving field fixed. The rotation-dependent plasma response is included through Δ̂(Q); the penetrated field is not held fixed. The orange curve touches zero at the saddle-node bifurcation, and increasing the vacuum field removes the higher-rotation equilibrium. A remaining lower-rotation equilibrium in the linear extrapolation does not rule out calling this the penetration threshold: loss of the shielded equilibrium can lead into nonlinear penetration physics. We should not require the linear calculation to predict exactly zero rotation. Why the selector misses it
For this case, the required vacuum field reaches the saddle-node value above, decreases, then turns upward again at lower rotation. The last retained sample, at Q ≈ 0.55345, requires b_r^vac/B_φ ≈ 2.55037 × 10⁻³—slightly more than the earlier local maximum. Consequently:
The earlier local maximum is followed by a 5.77% drop in the field-squared balance, comfortably above the existing 0.1% criterion. It also survives the mask: |Δ̂|/α ≈ 2.50 there. The warning that the required field “rises monotonically” is therefore incorrect for this curve. Validation and suggested correctionThe failure occurs at the default 2,000 samples and persists at 32,000 samples, with solver tolerances tightened to 10⁻¹³ and with integration-boundary checks. These calculations ran on SDCC through Slurm. A separate case keeps the example's original rotation and changes only α to 0.3; it exhibits the same rejection mechanism. The selector needs to identify the first resolved saddle-node bifurcation on the stable branch followed from Q₀, rather than requiring the global maximum over every retained equilibrium to be interior. Candidate extrema still need a significance/convergence check so solver noise does not become a threshold; simply taking the first raw sample-to-sample maximum would not be sufficient. The warning should also distinguish failure to identify a threshold from a verified absence of a saddle-node bifurcation. A diagnostic selection on the saved data recovers both counterexamples and preserves the unmodified example's 3/1 threshold to sampling accuracy. No production source changes have been made. This diagnoses the selection failure; it does not independently establish that the |Δ̂| > α cutoff is physically wrong. |
…ation threshold Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@matt-pharr thanks, confirmed and fixed in
Values are b_r/B_φ. The warning now says no fold was resolved on the sampled path instead of claiming the curve rises monotonically. A stub test reproduces your shape (a fold, a dip, then a rise past the fold's height toward Q_e); the old rule returns NaN on it. Would you be able to run the harness on #415, #441 and #489 against develop |
|
On it @d-burg |
|
@d-burg — completed the requested full SDCC fanout comparison against develop Result: no changes in comparable quantities; harness exit code 0.
Environment: Julia 1.12.6, x86_64 Linux, CI manifest pin ./regress_fanout.sh --refs 057c4af0d,da6926142 --harness-ref da6926142 --name pr415_da6926142_vs_057c4af0d_20261028SDCC report: Full regression report (21 cases)Third-party human review and approval are required before merge. |
…om Q0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@logan-nc, the port is on the branch now, and this answers your question 2; @matt-pharr is reviewing it. It went in at
The description is rewritten for the port. Tests pass (49 in the critical-resonant-field file) and CI is green. @matt-pharr's harness against develop On question 2, thanks for checking the Fortran. I've added |
|
@d-burg — the original counterexamples are addressed, but I found one additional selector edge case that I’d like corrected before approval. I checked this against the actual Julia function at da69261 on SDCC, using the same discharge-derived 3/1 layer with Q0 = 2, alpha = 0.48, and: torque_balance_scan(tb; Qmin=0.9, Qmax=0.75, n=401) The sampled balance is strictly decreasing throughout this interval, yet the routine returns: Q_peak = 0.892910447761194 The balance values immediately around the selected point are: 36.73998060, 36.73751588, 36.73533196 There is no interior maximum or saddle-node bifurcation in this window. The cause appears to be the running-maximum reset: after a sufficiently large fall from the first sample, peak is reset to a descending point. A subsequent fall then accepts that point because it is no longer path[1], even though the curve never rose to it. I am going to implement a change for this and then approve this PR. |
|
@matt-pharr good catch, I was still worried that the patch I put in wasn't exhaustive, looks like the restart after a fall from the first sample let a purely descending curve report a peak. Requiring the balance to have risen to the maximum before a drop counts closes it. Thanks for the fix. Do you think the selection logic is in a place that makes sense re: the physics, or have we possibly built ourselves into a corner here? |
Reject monotonic clipped windows and subthreshold bumps by requiring a significant rise and fall around an interior maximum. Reset the search across excluded samples and retain the first resolved saddle-node bifurcation. Add regression coverage and document the selection criterion.
|
@d-burg — completed the requested full SDCC fanout against develop Result: no changes in comparable quantities; harness exit code 0. This tests the actual PR branch after the selector correction and develop merge, at
Environment: Julia 1.12.6, x86_64 Linux, CI manifest ./regress_fanout.sh --refs 7817f0e7a,3a6435d60 --harness-ref 3a6435d60 --name pr415_3a6435d60_vs_7817f0e7a_20261028SDCC report: Full regression report (21 cases)Third-party human review and approval are required before merge. |

Release note
Tearing can now compute the critical resonant field b_r/B_φ for error-field penetration at each SLAYER surface, from the torque balance of Cole and Fitzpatrick, Phys. Plasmas 13, 032503 (2006), Eqs. 61–62. Turn it on with
[SLAYER.CriticalResonantField] enabled = true. Each surface uses its own SLAYER layer: P is itsP_tor, α its outer stability, and Q0 = τ_k·n·Ω_E its E×B rotation. The threshold is the penetration fold (Fitzpatrick, Nucl. Fusion 33, 1049 (1993), Sec. 7.4). A surface with no fold, which slows smoothly instead, reports NaN. Results go toTearing/CriticalResonantField/ingpec.h5. Resolves #371.Original implementation by @ebursch; ported onto develop's
TearingProblemand reworked by @d-burg.Changes
Tearing.CriticalResonantField(TorqueBalance.jl) evaluates Cole's balance 2P(Q0 − Q)/(Sκ̂ Im[−1/(α + Δ̂)]) on the real Q axis, with Δ̂ from the surface's SLAYER layer.D_c_typeis set. A tearing-unstable surface (α ≤ 0) is skipped with a warning.omega_Eor theomega_E_kHzoverride. The run stops if no surface has a known Ω_E and skips a surface without one. An explicit zero is valid and gives Cole's diamagnetic-only threshold.run_slayer_from_inputs, so script callers get it too. The[SLAYER.CriticalResonantField]section takesenabled,n,store_scanand optionalQmin/Qmax. HDF5 outputs:br_crit(b_r/B_φ),br_crit_tesla,Q_peak,Q0andrational_index, and withstore_scanthe per-surfaceScan/Surface_<k>/{Q, balance, Delta}.runtests_critical_resonant_field.jl, 49):diiid_slayer_n1pinsbr_crit,br_crit_tesla,Q_peakandQ0on the inner three surfaces.inner_layer.md, with the Fortran benchmark, and Fitzpatrick 1993 inreferences.md.On the DIII-D-like example
examples/DIIID-like_SLAYER_examplewith the analysis on, at5d1079d52; the fold-selection fix in753986fd1leaves every value unchanged:br_crit(b_r/B_φ)br_crit_tesla[T]Q_peakD_c_type = "none"it is tearing-unstable (α = −0.040), so there is no shielded state to penetrate.6d0c7dd7c, 1.61e-3). That comes from develop0b4b9fbfb: InnerLayer.SLAYER - FEATURE! - Build P_perp from the perpendicular particle diffusivity by default and add an ion heat diffusivity profile #501'sD_perpdefault lowers P_perp 3–4× on every surface, which changes Δ̂(Q). Every other layer input is unchanged, andQ_peakmoves by 0.4 %.Regression report
Latest SDCC fanout rerun: develop
7817f0e7avs3a6435d60— 21 cases; no changed comparable quantities; four new CRF quantities in each of two SLAYER cases have no develop baseline; harness exit 0. Full report and limitations.gal_resistive_pehas all eight quantities missing on both refs and provides no numerical validation. Earlier reports are retained below.Full harness by @matt-pharr on SDCC (report), develop
057c4af0dvsda6926142, Julia 1.12.6, manifest pinned: 21 cases, harness exit code 0. Every existing quantity in every case is unchanged. The four new pins (br_crit,br_crit_tesla,Q_peak,Q0on the inner three surfaces ofdiiid_slayer_n1anddiiid_slayer_n1_chi_perp_e) are N/A on develop, which has no such output.gal_resistive_peis N/A on both refs, as on develop.runtests_critical_resonant_field.jl(49) passes at753986fd1, and CI passes atda6926142.Notes for reviewers
Saddle-node counterexample (@matt-pharr, above). On the 3/1 layer with Q0 = 2 and α = 0.48 the threshold is now 2.54987e-3 at Q = 0.974, matching his 2.54975e-3 at Q ≈ 0.972; the old global-maximum rule returned NaN there and on his α = 0.3 case.
Fortran benchmark at Fortran's settings. Given Fortran's normalized inputs and α = 10⁻², the Julia balance reproduces Fortran's from Fortran's own Δ(Q) table to all printed digits. The thresholds differ only through the selection rule. Fortran takes the global maximum on its window, which is the balance spike within 0.003 of the Q_e pole on 2/1, 3/1, 4/1 and 6/1. The two layer models (Park's drift-MHD in Fortran, Fitzpatrick's in Julia) give Δ(Q) a median 24–78 % apart between Q_i and Q_e. Figure and details are in the docs section.
Units.
br_critis the dimensionless b_r/B_φ used by Cole and Fitzpatrick (2006, Eqs. 61–68) and by Fitzpatrick, Phys. Plasmas 30, 092512 (2023), Eq. 120. Both normalize the resonant field by the toroidal field.br_crit_tesla=br_crit·|B0|, with the layer's own B0, is there for comparison withresonant_area_weighted_field. That is GPEC's total resonant field with the ideal plasma response, and Park et al., PRL 99, 195003 (2007) identify it, rather than the external vacuum field, as the field that opens islands. Fitzpatrick's drive likewise includes the outer plasma's amplification. The comparison is approximate: the theory's b_r is a cylindrical field at the layer, and no reference maps it onto GPEC's area-weighted toroidal field. Do not compare it withpenetrated_area_weighted_field, which already includes the layer response. Fortran'sgpout.flabels the ratio "Tesla" but leaves out the B_φ factor; @logan-nc confirmed that reading above.Stability test. A surface is used only when α > 0, i.e. r_s Δ′ < Δ_crit. Two limits:
Skipping any surface where SLAYER finds a growing root would close both; it is left for a follow-up.
InnerLayer.SLAYER - API! - Default the layer radial label to the toroidal-flux label #599 (flux radial label). No conflict, and the tests here pass on the combined tree, but the label moves these pins:
br_critby 5.8 %,Q_peakby 40.8 %,Q0by 29.6 % (through τ_k). Whichever of the two merges second needs a new stamp.Open questions (from the thread):
🤖 Generated with Claude Code