InnerLayer.SLAYER - BUGFIX - Restore the toroidal mode number in the diamagnetic frequencies - #433
InnerLayer.SLAYER - BUGFIX - Restore the toroidal mode number in the diamagnetic frequencies#433d-burg wants to merge 6 commits into
Conversation
…diamagnetic frequencies _omega_star_at returns (dp/dpsi)/(e n_e), the n = 1 diamagnetic frequency. The physical omega_* = k_theta v_* carries the poloidal mode number; written in flux coordinates dpsi/dr = r B_phi / q absorbs the q in m = n q and leaves an explicit factor n. Nothing downstream restored it, so Q_e and Q_i were low by n at every rational surface and drift stabilization was underestimated for n >= 2. Confirmed against Fitzpatrick's TJ, which carries the mode number explicitly in both the layer-parameter notes and Rational.cpp; the ratio to the GPEC form is m/(q g) = n. The neighbouring quantities check out unchanged: TJ's tau_H reduces to R0/(n s V_A), matching tau_h exactly. iota_e = Q_e/(Q_e - Q_i) is a ratio, so the factor cancels there and it was already correct. Every shipped deck is n = 1, where the restored factor is exactly 1.0 and the arithmetic is bit-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o relative paths resolve An [overrides] case copied its deck to a system temp dir, so a deck whose gpec.toml references files by relative path (the SLAYER deck's profile_file points into a sibling example) lost those references and the run failed. Copy the deck to a throwaway sibling of the example dir instead, and remove that dir itself on cleanup rather than its parent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…and the Q_e n-scaling No SLAYER case ran above n = 1, so the n-dependence of the layer build — the diamagnetic frequencies omega_* proportional to n feeding Q_e/Q_i — was invisible to the harness; the omega-star factor restored on this branch could be silently reverted without any case moving. Reuse the n=1 deck via [overrides] at nn = 2, where qmin = 1.204 puts the 3/2 surface in the plasma, and track Q_e/Q_i directly alongside the n=1 case's quantity set with roots pinned on 3/2, 4/2, 5/2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-bearing cleanup path The sibling rundirs live inside the checkout, so without an ignore entry they show as untracked in git status --porcelain and can spuriously mark golden provenance dirty. Also state at both cleanup sites why rm targets rundir and not its parent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jhalpern30
left a comment
There was a problem hiding this comment.
I am fully trusting you on the physics here - all of my comments are more general suggestions. Leaving this as approved for simplicity whenever you're done looking at the comments since this definitely doesn't need a second runthrough, but if you disagree/decide to ignore any of the below please comment why for posterity
| # Override ω_*e, ω_*i with spline-derivative values when requested. In flux coordinates | ||
| # ω_* = n·(dp/dψ)/(e·n_e); `_omega_star_at` returns the n = 1 value, so restore the | ||
| # factor n here. Values supplied through `profiles` are taken to be physical already. | ||
| ω_e_use, ω_i_use = if compute_omega_star |
There was a problem hiding this comment.
Personally, I think the something = an entire if-else block is kinda confusing code, but it seems like that's in a lot of this file so its probably ok.
But if I am understanding this PR correctly - the actual omega* has an included n, so its a little bit deceptive to have _omega_star_at return something that isn't actually omega* and needs to be multiplied by n. I think its more clear to build it into the helper even though Claude argues against it _omega_star_at(psi, n). Then you can remove this 3 line comment up here and just leave it as is
| n_res = sing.n[1] | ||
|
|
||
| prof = profiles(psi) | ||
| # Override ω_*e, ω_*i with spline-derivative values when requested. In flux coordinates |
There was a problem hiding this comment.
When I see override, I think of something that is already set; in this case its just another branch of how to set this value.
I think more true to form would be
ω_e_use, ω_i_use = (prof.omega_e, prof.omega_i)
# Override ω_*e, ω_*i with spline-derivative values when requested
compute_omega_star && (ω_e_use, ω_i_use = _omega_star_at(psi, n))or something like that. The PR comment does suggest that prof.omega_e/i are never actually used, but I didn't factor that in here since I am unfamiliar with the rest of the relevant code
| # Each [quantities.*] block names an HDF5 path in the run output, how to extract it, | ||
| # and the noise floor below which a difference is treated as zero. | ||
| [case] | ||
| name = "diiid_slayer_n2" |
There was a problem hiding this comment.
So the entire purpose of this regression case is to guard the n-dependence of the layer module... that seems like overkill for a regression test that takes 5 minutes to run. I speak selfishly because my (and I'm assuming other's) regress running is automatically chosen by Claude. So if I change anything related to this module's code, Claude will queue up both of these cases to test something relatively small.
So personally, I am against regression case bloat and don't want them to take the place of unit tests. So my question here for your consideration - if the n1 and n2 case here actually needed and worth the additional regression harness runtime, which can already get quite long? Or is this something where we could keep a single regression case (perhaps n2 to try to catch n-dependent things like this in the future) and supplement with unit tests that check for specifics to slayer more cheaply?
Release note
nn = 1, where the restored factor is exactly1.0and the arithmetic is bit-identical. Results move for any run at n ≥ 2. (harness @ 47feada50)build_slayer_inputscomputed the electron and ion diamagnetic frequencies without the toroidal mode number, soQ_eandQ_iwere low by a factor ofnat every rational surface. Drift stabilization was therefore underestimated for any n ≥ 2 layer calculation.The defect
_omega_star_atreturnswhich is
(dp_e/dψ)/(e·n_e)— the n = 1 diamagnetic frequency. The physical quantity isω_*e = k_θ·v_*e = (m/r_s)·(dp_e/dr)/(e·n_e·B). Writing that in flux coordinates,dψ/dr = r·B_φ/qabsorbs theqinm = n·qand leaves an explicitNothing downstream restored the
n:Q_e = -tauk * omega_einLayerParameters.jl, with no mode-number factor anywhere in the chain.How it was found
Auditing whether the shear sign from #431 was load-bearing elsewhere in the layer physics. It is not — but the ExB rotation does pick up its mode number downstream (
1im * mc.ntor * mc.rotation[k],Tearing/Dispersion/CoupledFullMatch.jl) while ω_* never did, and theomegakwarg toslayer_parametersis accepted but unused in the body. That asymmetry was the tell.Confirmed against TJ
Fitzpatrick's TJ carries the mode number explicitly.
Documentation/LayerParameters.tex:and
TJ/Rational.cpp:180:Converting TJ's r-form to the flux form gives ratio
TJ/GPEC = m/(q·g) = n.The same cross-check validates the neighbouring quantities, which are not changed here: TJ's
tHk = Lsk/m/VAkreduces toR₀/(n·s·V_A), matching GPEC'stau_hexactly; theW_diteration and the:rfitzpcritical-Δ match term for term.Blast radius
Q_e,Q_in— drift stabilization underestimated at n ≥ 2iota_e = Q_e/(Q_e − Q_i)lu,tauk,D_norm,P_perp,delta_n,tau_rEntirely latent today: every SLAYER deck is
nn = 1, and the only n > 1 example (Solovev_ideal_example_multi_n,nn_high = 2) is an ideal run that never builds a layer. The regression harness cannot catch this — there is no n > 1 SLAYER case.Validation
New test
build_slayer_inputs: omega_star carries the toroidal mode numberresolves the same surface at n = 1 and n = 2 and assertsQ/taukdoubles whileiota_edoes not moveVerified non-vacuous: with the fix reverted, exactly the two scaling assertions fail and the
iota_eassertion still passes — the predicted signatureruntests_slayer_inputs38/38Full suite
julia -t 4 --project=. test/runtests.jlat53fffc0d0: 62 testsets, 2348 assertions, 0 failuresPost-review refactor (
_omega_star_atnow takes the mode number) re-verified:runtests_slayer_inputs38/38,runtests_slayer_params68/68,runtests_slayer_runner73/73Regression report
diiid_slayer_n1,develop(349a0c262) vs this head (47feada50), both sides--force, identical pinned environments.Inert on the shipped deck, as predicted. Every layer quantity is bit-identical at
0.0e+00— includingQ_eandQ_i, now tracked by this case, which is the direct statement that the restored factor does nothing at n = 1.The lone γ line is the threaded root search's own reproducibility, not this change. Its measured same-source floor is 0.076/0.122/0.145 Hz per surface (#418), and #431 saw 1.276e-01 on this same case; here it is 1.276e-01 Hz (0.02%). With every layer input bit-identical, a 0.13 Hz shift in the extracted root can only come from the root search. An earlier run of this same pair at
25d285eb8returned 1.9e-05 on this line — the spread between those two runs is itself the floor being described.The n = 2 demonstration
There is no n > 1 SLAYER case in the harness, so this defect was invisible to it. While preparing this PR I built one —
diiid_slayer_n2, the n = 1 deck re-run via[overrides]atnn = 2— to produce the measurement below. It is not part of this PR: on review (@jhalpern30) it was dropped as regression bloat, since it cost 273s/ref to guard something a unit test covers for free. Its two novel quantities,Q_eandQ_i, were folded intodiiid_slayer_n1instead, and the n-scaling is guarded bytest/runtests_slayer_inputs.jl.The measurement stands as validation even though the case is gone.
develop(e09795119) vs21fd7240e:With every geometric and resistive input bit-identical,
Q_eandQ_imove by exactly 100% — the restored factor n = 2 — and the mode rotationω_Hzdoubles with them (drift rotation ∝ ω_*), whileγ_Hzmoves 0.13%. The root inventory is unchanged. Ondevelop, an n = 2 run underestimates the diamagnetic drive by exactly half; any 3/2 analysis needs this PR first.The harness fix that made
[overrides]usable here (8ae06cd23, plus the4c70d4232gitignore) is kept — it is a general capability for any deck with relative file references, not specific to the removed case.Notes for reviewers
The mode number is applied inside
_omega_star_at(ψ, n_tor), which returns the physical ω_. (It was originally applied at the call site; @jhalpern30 correctly pushed back that a helper named for ω_ should return ω_*.) The parameter isn_torrather thannbecausen_eandn_iare densities in the same scope.The
compute_omega_star=falsebranch readsprofiles.omega_e/omega_i, which are literal zeros in production:run_slayer.jl:44hardcodeszeros(npsi),compute_omega_stardefaults totrue, and no caller insrc/passes it. The kinetic file schema has no ω_* column either, so nothing can supply one. That branch is a library-only path.Note
Found alongside a second, independent discrepancy in
D_norm— see the companion PR. The two are in different files and do not conflict.