Skip to content

InnerLayer.SLAYER - BUGFIX! - Use the physical toroidal field, not the b0exp normalization - #399

Merged
d-burg merged 4 commits into
developfrom
bugfix/slayer-bt-normalization
Aug 18, 2026
Merged

InnerLayer.SLAYER - BUGFIX! - Use the physical toroidal field, not the b0exp normalization#399
d-burg merged 4 commits into
developfrom
bugfix/slayer-bt-normalization

Conversation

@logan-nc

@logan-nc logan-nc commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: SLAYER layer parameters move on every deck that leaves [SLAYER] bt unset — Lundquist S +94.5%, D_norm 34.8%, tauk/Q_root 36%, γ 4.4% on the DIII-D-like deck (harness @ a00ecad)
  • Migration: re-run any saved SLAYER results — growth rates and layer parameters from earlier runs used the wrong field. To keep the previous behaviour, set [SLAYER] bt to the value equil.config.b0exp had for that deck (1.0 on the shipped ones).

SLAYER was running its layer physics at the b0exp normalization (exactly 1.0 on the shipped decks)
instead of the physical toroidal field B_T = F(ψ)/(2π·R₀) ≈ 1.95 T, so the Lundquist number and
every quantity derived from it were wrong by roughly a factor of two.

Regression report

regress --cases diiid_slayer_n1 --refs develop,local, baseline develop @ faf6d03, manifest pinned.

Regression Report: diiid_slayer_n1
=========================================================================================
Ref 1: develop  @ faf6d031 (2026-08-18)
       env: julia 1.11.6, arm64-apple-darwin24.0.0, manifest 7e5c34ad (pinned), 2 threads/2 BLAS
Ref 2: local  @ local (2026-08-18)
       env: julia 1.11.6, arm64-apple-darwin24.0.0, manifest 7e5c34ad (pinned), 2 threads/2 BLAS
-----------------------------------------------------------------------------------------
Quantity                            develop   local     Diff                Status       
-----------------------------------------------------------------------------------------
SLAYER surface indices              [6 elem]  [6 elem]  0.0e+00             OK           
SLAYER poloidal m                   [6 elem]  [6 elem]  0.0e+00             OK           
SLAYER toroidal n                   [6 elem]  [6 elem]  0.0e+00             OK           
SLAYER minor radius rs              [6 elem]  [6 elem]  0.0e+00             OK           
SLAYER r-based shear                [6 elem]  [6 elem]  0.0e+00             OK           
SLAYER Lundquist S                  [6 elem]  [6 elem]  5.261e+07 (94.51%)  ** CHANGED **
SLAYER D_norm                       [6 elem]  [6 elem]  1.458e+00 (34.79%)  ** CHANGED **
SLAYER P_perp                       [6 elem]  [6 elem]  0.0e+00             OK           
SLAYER tauk                         [6 elem]  [6 elem]  3.726e-05 (35.97%)  ** CHANGED **
SLAYER iota_e                       [6 elem]  [6 elem]  1.1e-16             OK           
SLAYER Q_root [2/1,3/1,4/1]         [3 elem]  [3 elem]  3.919e-01 (35.95%)  ** CHANGED **
SLAYER ω_Hz [2/1,3/1,4/1]           [3 elem]  [3 elem]  7.247e+00 (0.07%)   ** CHANGED **
SLAYER γ_Hz [2/1,3/1,4/1]           [3 elem]  [3 elem]  3.337e+01 (4.43%)   ** CHANGED **
SLAYER no_root flags [2/1,3/1,4/1]  [3 elem]  [3 elem]  0.0e+00             OK           
SLAYER enabled flag                 1         1         0.0e+00             OK           
Runtime (s)                         142.5s    140.7s                        --           

The unchanged rows are the control: rs, the r-based shear, P_perp and iota_e carry no B_T
dependence and do not move.

Notes for reviewers

The bug

run_slayer substituted equil.config.b0exp for the toroidal field whenever [SLAYER] bt was unset, which is the default:

bt = control.bt === nothing ? equil.config.b0exp : control.bt

b0exp is a normalization, not a field. On the DIII-D-like deck it is exactly 1.0, while the equilibrium's own F-spline gives B_T = F(ψ)/(2π·R₀) ≈ 1.95 T. SLAYER has therefore been running its layer physics at roughly half the true toroidal field.

build_slayer_inputs already documents this exact trap in its own docstring:

bt — toroidal field [T]. Scalar, callable of psi, or nothing (default). When nothing, the physical B_T = F(ψ) / (2π·R₀) is computed per surface from the equilibrium's F-spline. Note: equil.config.b0exp is a normalization (often just 1.0), not the physical field, so passing it as a scalar is almost always wrong.

The fix

Pass control.bt straight through. Unset then means the physical per-surface field, exactly as documented; an explicitly configured bt still wins. One line, plus a regression test that pins the behaviour.

Impact — this changes results

B_T enters tau_h ∝ 1/B_T, hence the Lundquist number S = tau_R/tau_H, and separately c_beta (via the local β) and so d_beta and the layer thickness delta_s.

regress --cases diiid_slayer_n1 --refs develop,local:

quantity change note
Lundquist S +94.5% tau_h ∝ 1/B_T, so S nearly doubles
D_norm 34.8% via lu^(1/3) and d_beta
tauk 36.0% ∝ B_T^(-2/3)
Q_root 36.0% normalized frequency
γ_Hz 4.4% growth rates
ω_Hz 0.07%
r_s, r-based shear, P_perp, iota_e unchanged none depend on B_T

The unchanged rows are the check: geometry and P_perp = tau_R/tau_perp carry no B_T dependence, and they do not move. diiid_slayer_n1 needs re-pinning.

How it was found

Cross-checking two independent computations of the same layer width that disagreed by 3–15%. With this fix they agree to ~1e-6, the residual being that one runs on the pass-1 equilibrium and the other on the re-formed one.

Scope

Only the SLAYER branch of run_slayer. The GGJ branch does not take bt. No other caller passes b0exp as a field.


⚠️ This is a physics correction, not a refactor — please sanity-check the direction against expectation for this equilibrium before approving.

…alization

run_slayer substituted `equil.config.b0exp` for the toroidal field whenever
`[SLAYER] bt` was unset (the default). b0exp is a NORMALIZATION -- commonly
exactly 1.0 -- so the layer physics ran at B_T = 1 T while the equilibrium's own
F-spline gives ~1.95 T on the DIII-D-like deck.

build_slayer_inputs already documents this exact trap:

    `bt` -- toroidal field [T]. Scalar, callable of `psi`, or `nothing`
    (default). When `nothing`, the physical `B_T = F(psi)/(2*pi*R_0)` is computed
    per surface from the equilibrium's F-spline. Note: `equil.config.b0exp` is a
    *normalization* (often just `1.0`), not the physical field, so passing it as
    a scalar is almost always wrong.

Passing `control.bt` straight through restores that documented default: unset
means the physical per-surface field, and an explicitly configured bt still wins.

B_T enters c_beta (via the local beta), hence d_beta, hence the layer thickness
delta_s, and separately tau_h and so the Lundquist number and D_norm. Layer widths
on the DIII-D-like deck move by 3-15%, and the growth rates with them, so this
changes SLAYER results -- it is a correction, not a refactor.

Found by cross-checking two independent computations of the same layer width that
disagreed; with this fix they agree to ~1e-6 (the residual being that one runs on
the pass-1 equilibrium and the other on the re-formed one).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSrf6JCViFfVzqzkQ66o6b
@d-burg

d-burg commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Verified the three load-bearing claims independently: run_slayer.jl:385 is the only place b0exp
is consumed as a field; the deck never sets b0exp so it takes the EquilibriumTypes.jl:49 default
of exactly 1.0; and LayerInputs.jl:151-153 does resolve bt === nothing to F(ψ)/(2π·R₀).

The reported moves are also self-consistent with B_T going 1.0 → 1.95 T, which is independent
evidence the direction is right:

expected at ratio 1.95 measured
S (τ_H ∝ 1/B_T) +95% +94.5%
tauk ∝ B_T^(-2/3) −36.1% −36.0%

Two requests before merge:

1. Control.jl:32 is left stale — it still says `bt` ... `nothing` → use `equil.config.b0exp`,
now exactly backwards, and it's the docstring someone configuring [SLAYER] bt reads.

2. The test asserts on source text, not behaviour. It greps run_slayer.jl for
bt = control.bt. That's coupled to the literal formatting of a file #393 is actively rewriting
(signature, docstring, result construction), so a pure refactor can fail it; it can't catch the bug
returning elsewhere (defaulting control.bt to b0exp in the constructor would leave both
assertions passing); and the third assertion is tautological — the || fallback
occursin("F_spline", li) is true for any file mentioning F_spline. Asserting the resolved field
is ≈1.95 rather than 1.0, and that an explicit control.bt still wins, would pin the actual physics.

Harness note: this re-baselines diiid_slayer_n1 and invalidates the provenance of the new
slayer_delta_probe (its layer parameters are quoted verbatim from a pre-fix run — the Δ(Q) physics
stays valid, the parameters need re-quoting). I'll regenerate once this and #398 land.

⚠️ Needs third-party human review before merge — not approving here.

@logan-nc logan-nc added the bugfix Something was wrong and now is not label Aug 18, 2026
@logan-nc

Copy link
Copy Markdown
Collaborator Author

@d-burg please address your own review requests as you are the lead developer on this section of the code and know it best. Merge when you are happy - after you submit a formal accepting review, of course ;)

…epping source

Addresses the two review points on this branch.

Control.jl still documented `bt` as resolving to equil.config.b0exp, which is
exactly the behaviour the fix removes and the docstring a user configuring
[SLAYER] bt reads.

The regression test asserted on the literal text of run_slayer.jl, which couples
it to the formatting of a file under active refactoring, cannot catch the bug
returning by another route (defaulting control.bt to b0exp in the constructor
would leave it passing), and its third assertion was tautological -- the
occursin("F_spline", li) fallback holds for any file mentioning F_spline.

Replaced with a behavioural check where the equilibrium fixture already lives.
Since tau_h = R0*sqrt(mu0*rho)/(n*sval_r*bt), lu is linear in the field actually
used, so the resolved bt is recoverable from the returned parameters: the test
pins that the default equals F(psi)/(2*pi*R0), that an explicit bt overrides it,
and that resolving to b0exp gives a different answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@d-burg

d-burg commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Pushed both fixes to this branch (1fd7974).

  1. Control.jl now documents bt as resolving to the physical B_T = F(ψ)/(2π·R₀) per surface, with a scalar or callable overriding it.

  2. Replaced the source-text assertions with a behavioural test, moved to runtests_slayer_inputs.jl where the Solovev equilibrium fixture already exists. Since tau_h = R0·√(μ₀ρ)/(n·sval_r·bt), lu is linear in the field actually used, so the resolved bt is recoverable from the returned parameters. It pins that the default equals F(ψ)/(2π·R₀), that an explicit bt overrides it, and that resolving to b0exp gives a materially different answer — which fails if the bug returns by any route, including via the constructor.

@d-burg d-burg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable to me

@github-actions

Copy link
Copy Markdown
Contributor

This pull request is missing a reviewer.

If you are not ready to name them, mark this pull request as a draft.
docs/development/contributors.md suggests lead developers to ask.
Merging is not blocked here, but no pull request may be merged without human review.

The b0exp comparison assumed the normalization differs from the physical field.
On the Solovev fixture it does not: b0exp = 1.0 and F(psi)/(2*pi*R0) = 1.0 to
roundoff, which is precisely why the original defect survived there. CI caught
the bad assertion.

Pins the resolution rule instead, which holds on any deck: leaving bt unset is
identical to passing F(psi)/(2*pi*R0) explicitly, and lu tracks an explicit bt
linearly. Where the bug is actually visible -- the DIII-D-like EFIT deck, b0exp
= 1.0 against a physical ~1.95 T -- is recorded in a comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@d-burg d-burg changed the title SLAYER - BUGFIX - Use the physical toroidal field, not the b0exp normalization InnerLayer.SLAYER - BUGFIX! - Use the physical toroidal field, not the b0exp normalization Aug 18, 2026
@github-actions github-actions Bot added the changed-results Results move or an interface breaks - read before upgrading label Aug 18, 2026
@d-burg
d-burg merged commit 14a3c9d into develop Aug 18, 2026
9 of 10 checks passed
@d-burg
d-burg deleted the bugfix/slayer-bt-normalization branch August 18, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Something was wrong and now is not changed-results Results move or an interface breaks - read before upgrading

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants