Repository navigation
ForceFreeStates - FEATURE! - Cap the integration domain where resistive layers overlap - #298
amlakbekaluachule wants to merge 30 commits into
Conversation
|
@logan-nc this PR does two things:
Let me know if anything looks off. |
|
I synced this branch with Worth knowing why the retarget was needed: The merge hit two conflicts, both caused by Two things worth a look before review: 1.
|
|
@d-burg whats the status of this? Should it be reviewed now? Or should it still be marked as draft? |
|
Fixed both flagged issues — SetResistiveWidthPsihigh.jl is now included/exported, and δ_FKR/δ_visco are documented in the LayerWidths docstring. |
|
@claude review this |
ReviewReminder (per repo policy): this PR must not be merged without third-party human review, regardless of any automated approval. Also — please run the regression harness ( Correctness / design concerns
Physics traceability
Style / consistency
Documentation / tests
None of the above touch the pre-existing |
StatusRewritten from the original 172-line contribution, which had zero call sites and compared layer widths in metres against positions in normalized flux. That file is gone. Feature — Default is off because enabling it crosses a rational surface on a diverted plasma: on Nine defects fixed along the way, all found by trying to make the feature trustworthy:
Verification — 122 + 67 + 59 + 32 + 14 tests; docs clean; the metres→flux conversion triple-checked three independent ways (the binding pair is 1.821 mm apart with half-widths summing to 2.338 mm, argued in pure metres); the back-off gate validated against ForceFreeStates energies as ground truth, and it flips one step before they degrade. Still open
|
|
@d-burg — ready for review, but please take #399 first. #399 is the one-line That ordering also separates the regression movement. Worth your attention specifically:
Everything except the opt-in flag is a bug fix that stands on its own — the field-line ODE |
|
@d-burg, as claude says this has hit a point where it needs your expert review. Workflows are in place to decide the truncation from the resonant layer widths. The issue is that it is reporting big widths and wanting to truncate way in (~0.9892 for DIII-D). I find this suspect, since Fitzpatrick had been finding things like to be very far out past 0.9999. In the process of adding the truncation logic, claude did convince me the units of the layer widths had been wrong in the original overlap-checking-loop (comparing layer widths of m to rational spacing in psin) and a conversion from m to psin was added... so maybe double check that? Do you think we need to try and reproduce the exact case from Fitzpatricks paper on this as a sanity check? |
…ational surfaces are found The no-surface early return built LayerOverlapScan with 13 arguments for a 15-field struct (width_dr and first_overlap missing), a MethodError the pipeline's catch downgraded to a spurious scan-failure warning. Also make the JET reproduction fail loudly when its alpha fixed point or Psi=1.05 bracket degenerates instead of proceeding on a silent fallback, document the rs_method and max_layer_solves kwargs, and compress the investigation narratives to the reasoning they support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-failure diagnostics The applied flag records that the bound entered sing_lim! as an active cap, not that it set the final psilim (dmlim/qhigh may truncate deeper) -- say so in the schema. Carry the backtrace in the scan-failure warning so a programming error is not indistinguishable from a physics refusal, and compress the edge-fit floor comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Please ping me again when this is ready for review - it seems to still be being developed. Turning off auto-notifications on this for now to avoid my inbox being blown up |
|
Same @d-burg. It says
above but I see no comment explaining how/why it got promoted from draft. The SLAYER stack is big rn so I am a bit lost - hold our hands through this please 🙏 |
|
I put together a review package for this PR — the overlap criterion and cut conventions as one diagram, the always-recorded / opt-in-applied wiring, the ten-equilibrium convention evidence, and the bugs found en route. https://claude.ai/code/artifact/71b5b47a-7b76-45e8-9714-bd43041bcde4 |
|
@d-burg lets merge this one way or another this week. Responses to the review package artifact questions:
I am fine with this for now. The result is perhaps a very small fraction of "ideal-ish" plasma getting truncated out - it is still much better than what we had before and pushing further out into the known-stochastic region feels unjustified as well as numerically harder.
I believe @d-burg declared Eq. (100) to be the more consistent one to the paper and thus agree this is where we should start. Besides, 0.98978 is pretty deep and starts to cut into the pedestal... so more thought would be needed about what that means → future task to be considered if/when this has been used a bit more so we have more experience.
No. All DIII-D like examples should get
Nah. Too much of a change. Lets just add a super quick test that does solovev with auto-grid and confirms it is correctly classified as limited - no full example needed.
This is fast and it's always valuable to have the basic outputs - so when users come to developers with "my run is funny" we can quickly look it up and see if the truncation they used was "reasonable". This is the number one gotcha for new users. |
logan-nc
left a comment
There was a problem hiding this comment.
Approved once the task list associated with the response to the review package questions is addressed.
jhalpern30
left a comment
There was a problem hiding this comment.
I gave a very cursory review and did not read the entire chain. These comments are all optional and I'll leave it up to you if you decide any of them are worth it, its just things I saw when skimming the code; sorry if anything was repeated from the above. Don't want to add too much work on a PR that's been sitting for a while
The implementation specifics (i.e. dmlim logic, opting in to get truncation, warning the user if they requested a psihigh past truncation, no-ops on most shipped decks) all seem ok with me
| intr.q1lim = profiles.q_deriv(profiles.xs[end]; hint=Ref(profiles.npts_minus_1)) | ||
| intr.psilim = equil.params.psihigh_resolved | ||
|
|
||
| # Resistive-layer overlap imposes an UPPER BOUND on the domain: past the first pair of |
There was a problem hiding this comment.
This should probably just be in the docstring (which needs to be updated with the extra argument anyway)
There was a problem hiding this comment.
Moved into the docstring, which now documents psilim_cap and the dmlim bound; the inline comment is down to one line (14d60b6).
Doing this turned up a real bug. dmlim stepped back only against qmax, so it could land past the cap, and past qhigh too: qhigh = 4.0 gave qlim = 4.2. And because the cap sits exactly on a rational, whether it was exceeded came down to how q(psilim_cap) rounded. dmlim now steps back until it is inside min(qmax, qhigh, cap), so qhigh = 4.0 and 4.1 both give 3.2; the tests put a cap on the exact rational and 1000 ulps either side. No shipped deck moves.
| function _layer_overlap_scan(path::Union{Nothing,AbstractString}, | ||
| intr::ForceFreeStatesInternal, equil::Equilibrium.PlasmaEquilibrium) | ||
| (path === nothing || !isfile(path)) && return nothing | ||
| if intr.nlow != intr.nhigh |
There was a problem hiding this comment.
Why not just take the most conservative truncation limit? Like min(layer_overlap(n) for n in nlow:nhigh)?
There was a problem hiding this comment.
Done in 8cfbbee. The scan now runs for each n in nn_low:nn_high and the innermost overlap point binds. On the DIII-D-like equilibrium at psihigh = 0.9999, n=1 overlaps at 0.99975 and n=2 at 0.99935, so nn = 1:2 binds at n=2. The recorded LayerOverlap group is the binding scan, and its n dataset says which.
| # Eq. (100) is not covariant -- it is anchored to the toroidal-flux label of the paper's | ||
| # Eq. (30), so the scan is driven in that label regardless of the SLAYER default. | ||
| return Tearing.resistive_layer_overlap(equil, profiles; n_tor=intr.nlow, rs_method=:flux) | ||
| catch err |
There was a problem hiding this comment.
Is there an actual reason for the try catch here or is this an AI artifact?
There was a problem hiding this comment.
skimming these comments - but I think the resistive_layer_overlap itself might throw errors? I agree this setup is opaque and could/should be cleaner
There was a problem hiding this comment.
Fair question: it was hiding real errors (the empty-scan MethodError was downgraded to a "scan failed" warning). resistive_layer_overlap already records expected per-surface failures in its notes and rethrows anything else, so the outer catch-all is gone (8cfbbee). The only guard left is on reading the kinetic file, since that is external input to an optional diagnostic and a bad file should skip the scan rather than abort someone's stability run.
| numunorms_init::Int = 100 | ||
| singfac_min::Float64 = 1e-4 # Matches Fortran STRIDE; required nonzero for the Riccati path. | ||
| set_psilim_via_dmlim::Bool = true # Safe default for diverted equilibria (most production use); set false for limited/analytical (LAR, Solovev). Auto-skipped for multi-n. See docstring. | ||
| psilim_from_layer_overlap::Bool = false # Cap psilim where adjacent resistive layers overlap. Opt-in; the scan is recorded either way. See docstring. |
There was a problem hiding this comment.
Is false the right default here? If this is a true physical limit, do we want people to have to know that they need to turn this on or they might accidentally set truncation out too far?
There was a problem hiding this comment.
Keeping it false, now with numbers. On the shipped DIII-D-like decks the overlap point is at ψ ≈ 0.9997, beyond every deck's psihigh, so the cap cannot bind there and turning it on changes nothing (measured: identical results flag on vs off, even at psihigh = 0.9995).
On setting the truncation out too far: the scan always runs and is recorded under ForceFreeStates/LayerOverlap/, and GPEC now warns whenever psilim lands past the overlap point, flag on or off (14d60b6). DIIID-like_truncation_example (psihigh = 0.9999) is the case where the cap binds, with a harness case tracking it.
| # when it cannot be run (no kinetic file, multi-n, or the scan itself refuses). Reads the kinetic | ||
| # file directly rather than reusing `kinetic_profiles`, which is a `KineticProfileSplines` built | ||
| # for the NTV path and carries a different field set than the layer builders take. | ||
| function _layer_overlap_scan(path::Union{Nothing,AbstractString}, |
There was a problem hiding this comment.
If this is a property of the equilibrium, should this be done during setup_equilibrium? And along the same lines, would it make sense for the layer_overlap to be a member of the PlasmaEquilibrium struct? Completely up to you, just a thought. I guess its a balance of scope/struct bloat vs. carrying along layer_overlap everywhere
There was a problem hiding this comment.
I'd keep it out of the equilibrium. The overlap point depends on n (now on the whole nn range) and on the kinetic profiles, neither of which PlasmaEquilibrium carries, so it belongs to the run rather than the equilibrium. It is computed once in prepare_force_free_states! and only goes to sing_lim! and the HDF5 writer, so it isn't carried around anywhere else.
…e layer-overlap cap sing_lim! moved qlim to (last rational + dmlim)/n and then stepped back only while it exceeded qmax, so the dmlim offset could push the domain past a user-set qhigh or the resistive-layer overlap cap: qhigh = 4.0 gave qlim = 4.2. Step back against the effective bound min(qmax, qhigh, cap) instead, so a bound on, or less than dmlim/n above, a rational excludes that rational (qhigh = 4.0 and 4.1 both give 3.2). Because the overlap cap lands exactly on a rational surface by construction, the old behaviour also made the capped domain depend on how q(psilim_cap) rounded: a spline value just below the integer excluded the surface, one just above kept it. The bounded step-back gives the same qlim on both sides. Also warn whenever psilim lands past the overlap point, whether or not the cap is enabled, since the matched-asymptotic treatment is undefined there; document the psilim_cap argument and the dmlim bound in the sing_lim! docstring; and record in the psilim_from_layer_overlap docstring why it stays off by default: on the shipped DIII-D-like decks the overlap point (psi ~ 0.9997) lies beyond every deck's psihigh, so the cap is inert there. Adds the first dmlim = true coverage to the cap-wiring tests: qhigh on and just above a rational, and a cap on the exact rational and 1000 ulps either side. 8 of the 12 new assertions fail against the previous step-back. No shipped deck moves: every deck with dmlim on has qhigh = 1e3, and every deck with a finite qhigh has dmlim off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erlap cap binds Every shipped deck leaves psilim_from_layer_overlap off, and at their psihigh the overlap point (psi ~ 0.9997) lies outside the domain anyway, so no deck or harness case exercised the cap end to end. DIIID-like_truncation_example reuses the ideal example's equilibrium and kinetic profiles at psihigh = 0.9999 with the cap on: the cap pulls qlim from 8.35 to 8.0, dmlim steps back to 7.2, and the q=8 surface is excluded. The diiid_truncation_n1 case tracks the ForceFreeStates/LayerOverlap outputs (applied, psilim_overlap, first_overlap_index, width_dr), which no case tracked before, plus the final domain, the retained surfaces and the plasma energy. The deck runs ideal fixed-boundary stability only, in about two minutes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed and DIII-D as diverted edge_q_law had no direct tests, and no harness case runs a limited equilibrium on the auto grid, so its limited-plasma branch was argued from measured r^2 values only. Assert that the Solovev auto-grid equilibrium is rejected on both passes, including with the absolute r^2 floor disabled so the log-vs-linear comparison is what rejects it, and that the DIII-D-like equilibrium is accepted with a diverging fit, so the rejection cannot pass vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…verlap point The overlap scan skipped multi-n runs because the overlap point depends on n. The domain must stop before the first overlap of any n, so run the scan for each n in nn_low:nn_high and let the one with the innermost overlap point bind; scans that find no overlap never bind. On the DIII-D-like equilibrium at psihigh = 0.9999, n=1 overlaps at 0.99975 and n=2 at 0.99935, and nn = 1:2 binds at n=2. The recorded LayerOverlap group is the binding scan, whose n dataset says which. Also stop wrapping the whole scan in a catch-all. resistive_layer_overlap already records expected per-surface failures in its notes and rethrows anything else, and the outer catch downgraded those code errors to a warning. Guard only the kinetic-file read, which is external input to an optional diagnostic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erlap cap Add an integration-domain truncation section to the stability page: how qhigh, psihigh and dmlim set psilim (including that dmlim never steps past qhigh or the cap), the resistive-layer overlap criterion and how the scan locates it, why the cap is off by default, the truncation example that exercises it, and the LayerOverlap datasets. Add Fitzpatrick (2025) to the citations page, cross-linked with the new section. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The truncation deck runs with vac_flag = false, which leaves every FreeBoundaryStability dataset empty and writes no fixed-boundary energy, so extracting ep_real failed the case. Track the domain selection and the scan's layer widths, which is what the deck exists to exercise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@d-burg I was excited to see the commits roll in! Whats the status now? Still working through any todos? |
…cati solve The scan called slayer_layer_thickness, which always runs the stiff riccati_del_s solve, and dropped any surface whose delta_s came back non-finite. The criterion uses only delta_dr, which is closed-form algebra, so a Riccati failure deleted a surface the criterion could score: _first_overlap_limit then compared its neighbours across a gap twice the physical one, hiding an overlap and reporting too wide a domain. The m_max and max_layer_solves backstops both flag an inconclusive verdict; this deletion was silent. Split the algebraic scales out as slayer_algebraic_widths and score the criterion from those. delta_s is now best-effort: a failure leaves it NaN with a note and its own channel reports no overlap, while the criterion is untouched. A surface that genuinely cannot be scored on the DR channel is skipped with an explicit "not conclusive" note. Also guards the delta_s solve against throwing, which the per-surface guard did not cover once the scan's outer catch-all was removed. Measured unchanged on the DIII-D-like equilibrium at psihigh = 0.9999: the scan returns psihigh = 0.9997535788 over 14 surfaces with no notes, in 3.9 s per n. Gives LayerOverlapScan a keyword constructor -- the positional form takes 15 arguments in a fixed order, which is how an earlier empty-scan return came to pass 13 -- and uses it for the empty return and in the tests. The JET width check now takes the no-ODE path, with a new assertion that the two agree exactly, so it no longer pays a discarded stiff solve per evaluation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
refined_psi_grid's psihigh keyword and its _truncate_density helper are left from the design that re-formed the equilibrium on the capped domain, which the qlim cap replaced. No caller passes psihigh and no test covers either, so they shipped as untested API. _truncate_density also duplicated the outer node when psihigh landed exactly on an interior knot, giving a zero-width interval. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The truncation deck ran vac_flag = false, which writes no energies, so the case that exercises the cap tracked only domain bookkeeping: a change in what the capped domain does to the physics would have passed unnoticed. Run the deck free-boundary and pin ep_real and et_real alongside the domain quantities. That also moves the case off the fixed-boundary path, which reports a non-Hermitian W-inverse on this equilibrium at any psihigh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n prose The scan resolves its kinetic file from the run's TOML, so the programmatic solve(prob, alg) entry point cannot run it and psilim_from_layer_overlap is inert there. Say so in the control docstring and the stability page. Trim the psilim_from_layer_overlap docstring from a nine-sentence paragraph to the contract plus a pointer to the docs section, which is the canonical home. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s from the SLAYER inputs The scan resolved its kinetic file from either [KineticForces] kinetic_file or [SLAYER] profile_file and re-read it with its own loader, which duplicated SLAYER's _load_profiles, ignored its profile_group, and never passed any diffusivity -- so every layer width was computed with the chi_perp = chi_tor = 1.0 defaults while the SLAYER analysis of the same surfaces used the file's chi(psi). On the shipped DIII-D-like profiles chi_perp is 1.25-3.29 m^2/s, and since delta_dr goes as chi_perp^(1/4) the scan's widths were 6-35% narrower than SLAYER's own. Build the SLAYER control from [SLAYER] and call Runner._load_profiles, so the scan sees the same profiles, HDF5 group and chi(psi), with the control's scalar fallback when the file carries no chi -- the precedence run_slayer uses. A deck without a [SLAYER] profile_file no longer runs the scan. At the unscaled profiles this leaves the overlap point unchanged at psihigh 0.9995 and 0.9999 (widths grow ~27% nearly uniformly) but moves it one surface inward, 0.99969 -> 0.99857, on a psihigh = 0.995 grid where the edge surfaces are extrapolated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead of a tuned r² floor EDGE_FIT_MIN_R2 = 0.90 sat just under the worst real reconstruction then measured (0.929), so it was fitted to that sample rather than set by a principle, and the docstring already said it was not the discriminator: r2_log > r2_linear is, and the limited Solovev edge is rejected with the floor disabled entirely. Replace it as the pathology guard with a physical check. The fitted A in q = q_bar + A*(ln(1-psi) - u_bar) is the local logarithmic shear, so it must match -(1-psi)*q'(psi) over the fitted knots; accept agreement within a factor of EDGE_FIT_SHEAR_RATIO = 2. Across 14 real DIII-D reconstructions from IDA runs the ratio is 0.884-1.160, the synthetic DIII-D-like deck 0.948, and the limited Solovev edge 0.666. Keep EDGE_FIT_MIN_R2 = 0.5 as an explicit backstop against a fit that describes nothing, documented as such. Adds a test that the shear check is live: a tolerance tighter than the DIII-D-like fit's own disagreement must reject it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… so the overlap cut binds Scale chi_e (the chi_perp profile) in the shared DIII-D-like kinetic file by 2, from 1.25-3.29 to 2.50-6.59 m^2/s; chi_phi is unchanged, and the file's provenance attribute records the scaling. The profile is an electron power-balance chi used as a proxy for the one-fluid energy diffusivity the layer width wants, and that proxy is uncertain by a factor of this order. The doubled profile centres on the 4 m^2/s TJ uses as its default. With it the truncation deck's cut moves one surface inward: the cap pulls qlim from 8.35 to 7.0, dmlim steps back to 6.2, and q=7 and q=8 are excluded. The resulting domain, psilim = 0.99498 with q=6 the last surface and ep_real = -1.3485, reproduces the hand-chosen psihigh = 0.995 of the ideal deck (ep_real -1.34833). The factor sits well clear of both thresholds found on this equilibrium (the cut first moves at chi x1.26 and next at x~30). Moves results on decks that read chi from this file: SLAYER growth rates on the SLAYER deck, and the truncation case. The ideal deck's NTV does not read chi. The truncation deck now takes its profiles from a [SLAYER] section (enabled = false) rather than [KineticForces], since the scan reads only [SLAYER]. Also corrects 6de1b8f, whose message said that running this deck with vac_flag = true moved it off the path that reports a non-Hermitian W-inverse. That warning comes from the Newcomb zero-crossing check, which runs after every forward and Riccati integration regardless of vac_flag, so it still appears here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sivity each formula needs The critical-Δ source was cited as "Connor-Hastie-Helander 2015" in eight places. It is Connor, Ham, Hastie & Liu, PPCF 57, 065001 (2015), Eqs. (59) and (61), after Lütjens, Luciani & Garbet, Phys. Plasmas 8, 4267 (2001); the W_d loop and its short/long-mean-free-path chi_parallel blend follow Fitzpatrick, Phys. Plasmas 32, 062509 (2025). Both added to the citations page. Record that the two formulas fed by chi_perp want different quantities: the layer width goes through tau_E = r^2/chi_E, the one-fluid perpendicular energy diffusivity, while W_d describes electron-temperature flattening against an electron chi_parallel and so wants the electron channel. One profile, the kinetic file's electron power-balance chi_e, feeds both for now, matching TJ's single-chi convention; separating them is deferred because it needs a chi_i in the kinetic schema, and the upstream power-balance ion chi is not yet usable. Also brings the truncation docs in line with the scaled profiles and the SLAYER-only profile path: the overlap point is now psi ~ 0.9985, outside the 0.995 decks' domain but inside the SLAYER deck's 0.9995. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release note
diiid_slayer_n1—P_perp×2 exactly, the 2/1 growth rate 194.6 → 173.6 Hz (−11%), the 3/1 and 4/1 rates by −9% and −8%,Q_root≤ 0.3%, ω ≤ 0.1%. The other 216 tracked quantities across the 15 cases shared with develop are unchanged. Results also change, on no shipped deck, for (1) runs withset_psilim_via_dmlim = trueand a finiteqhigh, wheredmlimcould previously step pastqhigh(qhigh = 4.0gaveqlim = 4.2, now3.2), and (2) real reconstructions ongrid_type = "auto"whose edge the old q-law gate wrongly rejected.dmlimon and a finiteqhighnow truncates one rational lower wheneverqhighsits on a rational or less thandmlim/nabove one; raiseqhighabove(rational + dmlim)/nto keep the old domain.LayerWidthsgainsdelta_norm,delta_viscoanddelta_dr, so code constructing it positionally must supply them.sing_lim!gains an optionalpsilim_capkeyword and is otherwise unchanged. The layer-overlap scan reads its profiles only from[SLAYER] profile_file, so a deck without one no longer writesForceFreeStates/LayerOverlap/. The shipped DIII-D-like kinetic file'schi_eis doubled, so SLAYER results computed from it differ from earlier versions.GPEC can now locate the point past which adjacent rational surfaces' resistive layers overlap — the outer limit of the matched-asymptotic treatment, after Fitzpatrick, Nucl. Fusion 2025 Sect. 5.9 — and, when
psilim_from_layer_overlap = true, use it as an upper bound on the stability integration domainpsilim. Its layer widths use the same profiles and χ⊥(ψ) as the SLAYER analysis. The overlap point is recorded whenever the scan runs, and GPEC warns wheneverpsilimlands past it. The truncation logic is documented in the new "Integration domain truncation" section of the stability docs.What is actually wired
[ForceFreeStates] psilim_from_layer_overlap(defaultfalse)Tearing.resistive_layer_overlap, run inprepare_force_free_states!whenever the run's[SLAYER]section names a readableprofile_file, once perninnn_low:nn_high; the innermost overlap point bindssing_lim!(...; psilim_cap=...)— an upper bound onqlim, applied beforedmlim, which can no longer step past itForceFreeStates/LayerOverlap/— always written when the scan ranpsilimlands past the overlap point, flag on or offThe scan runs regardless of the flag; only applying the cap is gated, so a run always shows where layer physics would have cut the domain. Profiles, and χ⊥(ψ)/χ_φ(ψ), come from the
[SLAYER]profile_filethrough SLAYER's own loader, so the scan and the SLAYER analysis see identical inputs.Why it stays off by default, and why the DIII-D decks keep their
psihighWith the shipped DIII-D-like profiles the Eq. (100) overlap point is ψ ≈ 0.9985, at the q = 7 surface. That is outside the ideal and Riccati decks'
psihigh = 0.995, so the cap could not bind there even if enabled, and inside the SLAYER deck's 0.9995, where enabling it would exclude q = 7. The flag is off on every shipped deck, so none of them changes; the SLAYER deck does now log the "past the overlap point" warning, because itsdmlimdomain ends near ψ = 0.9989.Raising the ideal and Riccati decks to
psihigh = 0.9995, as suggested, would move results substantially, because the domain gains the q = 7 surface:diiid_n1, 0.995 → 0.9995msing/mpertpe_toroidal_torqueet_imagep_real,ev_realq0,q95It would also now put the overlap point inside their domain, so with the flag on the cap would pull them straight back to q ≤ 6 — the domain they already have. The change is therefore not made here; where the cap binds is covered by a dedicated deck (below).
Verified end to end
examples/DIIID-like_truncation_exampleruns the DIII-D-like equilibrium atpsihigh = 0.9999with the cap on — ideal free-boundary stability only, about two minutes:The q = 7 and q = 8 layers are the first adjacent pair to overlap, so the cap sits at the q = 7 surface itself, and
dmlimthen stops 0.2 of a rational spacing past the last surface inside it, q = 6. The resulting domain,psilim = 0.99498, reproduces the ideal deck's hand-chosenpsihigh = 0.995:ep_real−1.348513 vs −1.34833,et_real0.8010979 vs 0.801353.The new harness case
diiid_truncation_n1tracks that run, including theLayerOverlapoutputs (applied,psilim_overlap,first_overlap_index,width_dr) that no case tracked before.Correction to an earlier version of this description, which said
dmlim'strunc"backs off to the rational below" the cap. It did here, but only becauseq(psilim_cap)happened to evaluate to 7.99999…: the cap lands exactly on a rational by construction, and a value rounding the other way gaveqlim = 8.2, i.e. past the cap. Fixed in14d60b6b9(below); the capped domain no longer depends on that rounding.The domain-limit convention
The scan cuts at ψ[k−1], the position of the last surface with a well-separated inner region. This is the paper's own
1 − ε_c, and it is whatruntests_layer_overlap_fitzpatrick.jlreports in order to reproduce the paper's Ψ = 0.9985 / 0.9952.Settled against ten equilibria (the shipped deck plus nine IDA DIII-D reconstructions) rather than by preference — the two half-width alternatives both fail:
Full per-case table in the thread.
Bugs found while making the feature trustworthy
1. The edge q-law gate rejected 8 of 9 real DIII-D reconstructions (
d4ce058ff, carries!).EDGE_FIT_MIN_R2 = 0.99was calibrated against the synthetic TokaMaker deck (r²_log 0.9957, 0.9989). Real EFIT/CAKE reconstructions carry noise in the edge q and fit at 0.929–0.977, so all were refused: no surfaces placed past the grid, and the scan reported "no overlap" — a silent fail-open on exactly the case the criterion exists for. No absolute threshold can separate the classes (the limited a10 deck sits at 0.972, inside the diverted band); ther²_log > r²_linearcomparison does, and is unchanged. Lowered to 0.90 at the time; since replaced by a physical shear-consistency check with a 0.5 backstop (see "Edge q-law gate" below). The!is because the same gate controls the auto-grid edge density floor.2. Reverse shear crashed the layer build — split out as #431, being pre-existing on develop. #431 has since merged; the cherry-picked copy resolved away in the develop sync merge (
7e9e66508).3. A single failing surface aborted the whole scan (
6781b639f). The per-surface guard caught onlyArgumentError.4. The no-surfaces path threw instead of returning an empty scan (
c02a3c43d, found in adversarial review). The early return builtLayerOverlapScanwith 13 arguments for a 15-field struct — aMethodErrorthe pipeline's blanket catch downgraded to a spurious "scan failed" warning. Fixed, with a regression test forcing the path (m_max=0, extrapolation off), and the catch now logs the backtrace so a programming error is distinguishable from a physics refusal.5.
dmlimcould step pastqhighand the cap (14d60b6b9, carries!).sing_lim!stepped the(last rational + dmlim)/noffset back only while it exceededqmax, soqhigh = 4.0gaveqlim = 4.2, and a cap on a rational could be exceeded depending on float rounding. It now steps back againstmin(qmax, qhigh, cap):qhigh = 4.0and4.1both give3.2, and a cap on the exact rational and 1000 ulps either side all give the sameqlim. 8 of the 12 new assertions fail against the old step-back. The existing cap-wiring tests all ran withset_psilim_via_dmlim = false, which is why this was not caught.6. The scan's catch-all hid code errors (
8cfbbee7e).resistive_layer_overlapalready records expected per-surface failures in its notes and rethrows anything else; the outer catch downgraded those to a warning (bug 4 was found through exactly that). Only the kinetic-file read is guarded now.Perpendicular diffusivity
The scan used the wrong χ (
f5c60e779, carries!). It re-read the kinetic file with its own loader and passed no diffusivity, so every layer width was computed withchi_perp = chi_tor = 1.0, while the SLAYER analysis of the same surfaces used the file's χ(ψ). On the shipped profiles χ⊥ is 1.25–3.29 m²/s, and sincedelta_dr ∝ χ⊥^(1/4)the scan's widths were 6–35% narrower than SLAYER's own. The scan now takes its profiles, HDF5 group and χ(ψ) from[SLAYER]through SLAYER's own loader.The shipped χ⊥ profile is doubled (
ce29f7d99, carries!), from 1.25–3.29 to 2.50–6.59 m²/s;chi_phiis unchanged and the file's provenance attribute records the scaling. On this equilibrium the overlap cut is marginal — it first moves one surface inward at χ⊥ ×1.26 and next at ×~30 — so ×2 sits clear of both, centres the profile on TJ's default of 4 m²/s, and makes the truncation deck exercise a deeper cut. It is a deliberate change to input data, justified by how uncertain this proxy is (next paragraph), not by new physics.Which χ each formula wants — recorded, and deferred. The layer width goes through
τ_E = r²/χ_E, the one-fluid perpendicular energy diffusivity (every Fitzpatrick paper defines it that way). The critical-Δ island widthW_d ∝ (χ_⊥/χ_∥)^(1/4)describes electron-temperature flattening against an electron χ_∥, so its χ_⊥ is the electron channel. GPEC feeds both from one profile, the kinetic file'schi_e, which is autoONETWO's electron power-balanceCHI_E: the right channel for the critical Δ and a proxy for χ_E in the layer width. This matches TJ, which uses oneChipfor both and assumesT_e = T_i. Separating the two needs an ionchi_iin the kinetic schema, and the power-balance ion χ available upstream is not yet usable (negative across 0.3 ≤ ρ < 0.95 on the case checked), so it is deferred and recorded in the stability docs as a to-do.Critical-Δ citations corrected (
9c1937bda). Cited in eight places as "Connor-Hastie-Helander 2015"; it is Connor, Ham, Hastie & Liu, PPCF 57, 065001 (2015), Eqs. (59) and (61), after Lütjens, Luciani & Garbet, Phys. Plasmas 8, 4267 (2001). TheW_dloop and its short/long-mean-free-path χ_∥ blend follow Fitzpatrick, Phys. Plasmas 32, 062509 (2025).Edge q-law gate
EDGE_FIT_MIN_R2 = 0.90sat just under the worst real reconstruction then measured (0.929), so it was fitted to that sample, and the relativer2_log > r2_lineartest was already doing the discriminating. It is replaced as the pathology guard by a physical check (cf85db8c5, carries!): the fittedAinq = q̄ + A(ln(1−ψ) − ū)is the local logarithmic shear, so it must match the profile's own−(1−ψ)q′over the fitted knots, within a factor of 2. Across 14 real DIII-D reconstructions from IDA runs the ratio is 0.884–1.160 (all accepted), the synthetic DIII-D-like deck 0.948, and the limited Solovev edge 0.666.EDGE_FIT_MIN_R2 = 0.5stays as an explicit backstop against a fit that describes nothing, documented as such.Jacobian independence
@logan-nc asked for confirmation that
delta_sgives a Jacobian-independent ψ width. Measured underpest/hamada/equal_arc:r_sinvariant to 1e-16,da/dψto 3e-14,|δ_s|and both Δψ widths to ~7e-6 (the equilibrium solve's own noise — the inputs are invariant to 1e-14). Same under:flux. Confirmed clean.The check did find that
:fsaand:halfwidthare coordinate-dependent (up to 7% inr_s); those labels live in #403, are not defaults, and are not used here.Response to the review-package questions (@logan-nc, 2026-09-15)
dmlimdmlimnow provably stays inside the cap (bug 5)psihigh = 0.9995harmonisation is declined for the reasons aboveruntests_grid_refinement.jl(9f2010cfd): Solovev is rejected as limited on both passes, including with the absolute r² floor disabled, and the DIII-D-like equilibrium is accepted as a positive controlnNotes for reviewers
rs_method = :flux, not the SLAYER default. Eq. (100) is not covariant — δ ∝(q/|dq/dr|)^(1/2)— so it is only valid in the coordinate it was derived in, the toroidal-flux label of the paper's Eq. (30).delta_s_absappears in a third HDF5 group. The one-name-per-quantity rule says this is right, but @logan-nc asked to be told if it stayed ambiguous.evaluate_stability_criterion!) reports a non-Hermitian W-inverse at ~40 integration steps on the DIII-D-like equilibrium at anypsihigh. It runs after every forward and Riccati integration whatevervac_flagis. Correction: an earlier version of this description, and the message of6de1b8f03, said running the truncation deck withvac_flag = trueavoided it; it does not.Regression report
regress --cases <all 17 cases> --refs cb18e386e,af9a8badd --force— develop as merged into the branch vs this head (the merge commit); julia 1.11.6, pinned manifest:220 quantities compared, 216 unchanged. The four changes are the doubled χ⊥ on
diiid_slayer_n1:P_perpis exactly ×2 on every surface, and the growth rates move 2/1 194.6 → 173.6 Hz, 3/1 −249.2 → −226.8 Hz, 4/1 −1008.8 → −925.3 Hz — far above the root-search jitter. Every SLAYER input that does not depend on χ⊥ (S,D_norm,tauk,iota_e,r_s) is unchanged, and so is every other case, includingdiiid_n1, whose NTV does not read χ. So the SLAYER-only profile path and the edge-gate change move nothing on any other deck.gal_resistive_peextracts nothing on either ref, develop included, so it predates this PR; it is being looked at separately.Runtimes are not quoted: the run was not made under controlled load.
diiid_truncation_n1is new, so it runs at this head only:Changes from adversarial review
cd6dce306, carries!). The scancalled
slayer_layer_thickness, which always runs the stiffriccati_del_ssolve, and droppedany surface whose
delta_scame back non-finite -- even though the criterion uses only theclosed-form
delta_dr. A deleted surface left_first_overlap_limitcomparing its neighboursacross a gap twice the physical one, hiding an overlap and reporting too wide a domain, silently.
delta_sis now best-effort (NaNplus a note, its own channel only); a surface that cannot bescored on the DR channel is skipped with an explicit "not conclusive" note, matching the
m_maxand
max_layer_solvesbackstops. Measured unchanged:psihigh = 0.9997535788over 14 surfaces,no notes, 3.9 s per
n.6de1b8f03). It ranvac_flag = false, which writesno energies, so only bookkeeping was tracked on the one deck where the cap binds. The deck now
runs free-boundary and the case pins
ep_realandet_real.refined_psi_grid'spsihighkeyword and_truncate_densityare removed (9848fa332) --left from the design that re-formed the equilibrium, unreachable and untested since the
qlimcap replaced it. (The Migration note below no longer mentions them.)
solve(prob, alg)path, which resolves no kineticfile; documented in the control docstring and the stability page (
a3e7c51fa).LayerOverlapScangains a keyword constructor -- the positional form takes 15 arguments in afixed order, which is how the earlier empty-scan return came to pass 13.
Validation
620e4c531, no conflictsruntests_layer_overlap_fitzpatrick.jlat this head: JET reproduction 9/9 (Ψ = 0.9985111 vs paper 0.9985 for n = 1, 0.9949738 vs 0.9952 for n = 4, plus the no-ODE width path asserted equal to the full one), cap wiring 28/28runtests_grid_refinement.jlat this head: 66/66, including a test that the shear check is liveruntests_slayer_params.jl71/71,runtests_kinetic.jl277/277,runtests_slayer_inputs.jl62/62psihigh = 0.9999: n = 1 overlaps at 0.99975, n = 2 at 0.99935,nn = 1:2binds at n = 2Important
Do not merge without human review.
🤖 Generated with Claude Code