Repository navigation
InnerLayer.SLAYER - BUGFIX - Restore the toroidal mode number in the diamagnetic frequencies - #433
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
There was a problem hiding this comment.
I agree, a helper named _omega_star_at that doesn't return ω_* is an easy thing to misuse. In 47feada, _omega_star_at(ψ, n_tor) returns the physical ω_*, and the call site is now just one expression with no patch comment
| 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
There was a problem hiding this comment.
Took your wording point, "override" was the wrong word for what is just a branch. I kept one assignment rather than your two-line version to match the rest of the file, but I don't feel strongly about that.
On the part you weren't sure about: prof.omega_e and prof.omega_i are always zero in production – they're now cruft and I'll need to clean them up in an upcoming focused PR (won't burden this one further).
| # 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?
There was a problem hiding this comment.
Agreed – diiid_slayer_n2 is dropped in 25d285e. The n-scaling is covered by the unit test at test/runtests_slayer_inputs.jl:214, which runs the same surface at n = 1 and n = 2 and checks that Q/tauk doubles while iota_e doesn't move. It was confirmed to fail without the fix.
… n=2 case The n=2 case existed only to guard the n-dependence of the diamagnetic frequencies, which the unit test in runtests_slayer_inputs.jl already covers at a fraction of the cost (273 s/ref for the case). Its quantity set was a strict superset of the n=1 case, so fold the two additions it contributed -- Q_e and Q_i -- into the n=1 case and drop it. iota_e is their ratio and is blind to a common factor, so Q_e/Q_i are what pin the drive itself. The [overrides] rundir fix that this case needed stays: it is a general harness capability, not specific to the removed deck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… omega-star helper `_omega_star_at` returned the n = 1 diamagnetic frequency and left the call site to restore the factor n, so a helper named for omega-star did not return omega-star. Take the mode number as an argument instead and return the physical value, which drops the three-line fix-up comment at the call site and collapses the branch to a single expression. The parameter is `n_tor` rather than `n` because `n_e` and `n_i` are densities in the same scope. Numerically identical; the existing test asserts through the returned SLAYERParameters, so it covers the new arrangement unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The override directory handling has a critical concurrency race that can mix or delete files between simultaneous harness runs.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Restores toroidal mode-number scaling in SLAYER diamagnetic frequencies and adds regression coverage.
Changes:
- Applies
nscaling to electron and ion diamagnetic frequencies. - Adds unit tests and regression tracking for
Q_e/Q_i. - Updates override-directory handling and ignores temporary directories.
File summaries
| File | Summary |
|---|---|
test/runtests_slayer_inputs.jl |
Verifies mode-number scaling and invariant iota_e. |
src/InnerLayer/SLAYER/LayerInputs.jl |
Restores toroidal-mode scaling. |
regression-harness/src/runner.jl |
Materializes override decks beside examples. |
regression-harness/cases/diiid_slayer_n1.toml |
Tracks diamagnetic drive quantities. |
.gitignore |
Ignores temporary override directories. |
Review details
- Files reviewed: 4/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| rundir = joinpath(dirname(example_path), ".regress-override-" * basename(example_path)) | ||
| rm(rundir; recursive=true, force=true) # stale leftover from a crashed run |
…undir Placing the override rundir beside its example fixed relative file references, but the name became fixed per example, so two harness runs from the same checkout on an override case would delete and rewrite each other's deck and output. Create it with mktempdir under the example's parent instead: still a sibling, so relative references resolve, but unique per invocation, as the override rundir, worktrees and subprocess files already were. Only concurrent local-ref runs could collide; git-ref comparisons each run in their own temporary worktree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 @ 0d5275e)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(df8f940d5) vs this head (0d5275e31), both sides--force, identical pinned environments. Re-run after mergingdevelopin, so both sides carry #403.Inert on the shipped deck, as predicted. Every tracked quantity is unchanged — including
Q_eandQ_i, now pinned by this case, which is the direct statement that the restored factor does nothing at n = 1. The root lines sit at 1e-5 Hz and below, inside the threaded root search's own floor (0.076/0.122/0.145 Hz per surface, #418).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.