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 @ 28a0425)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
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 moveiota_eassertion still passes — the predicted signatureruntests_slayer_inputs38/38julia -t 4 --project=. test/runtests.jlat53fffc0d0: 62 testsets, 2348 assertions, 0 failuresRegression report
diiid_slayer_n1,develop(e09795119) vs this head (53fffc0d0), both sides--force, identical pinned environments.Inert on the shipped deck, as predicted. Every layer quantity is bit-identical at
0.0e+00— includingD_norm,iota_eandtauk, the quantities the restored factor would have moved had it done anything here. That is the expected result: this deck isnn = 1, where the factor is exactly1.0.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.127e-01 Hz (0.01%), inside that band. With every layer input bit-identical, a 0.11 Hz shift in the extracted root can only come from the root search.
The n = 2 case that now guards this
The gap this PR originally had to state — no n > 1 SLAYER case anywhere in the harness — is closed on this branch.
diiid_slayer_n2reuses the n = 1 deck via[overrides]atnn = 2(the deck's qmin = 1.204 puts the 3/2 surface in the plasma; 13 rationals total) and tracksQ_e/Q_idirectly, which the n = 1 case never pinned. Enabling[overrides]for this deck needed one harness fix: override rundirs now materialize beside their example so relative file references (SLAYER.profile_file) resolve; the rundir pattern is gitignored so it cannot spuriously dirty golden provenance.Validation run,
develop(e09795119) vs21fd7240e, both--force:This table is the direct demonstration the n = 1 run could not provide: 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 only 0.13%. The root inventory (no_rootflags) is identical across the fix. Ondevelop, in other words, an n = 2 run underestimates the diamagnetic drive by exactly half; any 3/2 analysis needs this PR first.The two commits enabling this (
8ae06cd23harness fix,21fd7240ecase,4c70d4232gitignore + cleanup note) touchregression-harness/and.gitignoreonly — nosrc/change, so the harness stamp above remains valid.Note
Harness re-run at
28a04256cafter a comment-only commit touchedsrc/, which staled the earlier53fffc0d0stamp. The numbers reproduce that run exactly — the commit changed comments only — but the stamp must name a commit the harness actually ran at, so it was re-run rather than re-pointed.Notes for reviewers
The factor is applied at the call site rather than inside
_omega_star_at, becausenis a per-surface property of theSingTypeand the helper is a pure profile evaluation. Values supplied throughprofiles.omega_e/omega_i(thecompute_omega_star=falsepath) are left alone — those are taken to be physical already. In practicerun_slayer.jlpasseszeros(npsi)there, so that path supplies no real values today.Worth a second opinion: whether file-supplied ω_* should also be treated as per-n. I have assumed not.
Note
Found alongside a second, independent discrepancy in
D_norm— see the companion PR. The two are in different files and do not conflict.