Skip to content

Tearing - BUGFIX - Doppler-shift each GGJ surface into its own E×B frame in the coupled growth rates - #612

Merged
logan-nc merged 3 commits into
developfrom
bugfix/ggj-per-surface-doppler
Oct 30, 2026
Merged

logan-nc merged 3 commits into
developfrom
bugfix/ggj-per-surface-doppler

Conversation

@d-burg

@d-burg d-burg commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: none by default; coupled GGJ growth rates change only when a surface is given an E×B rotation, which was not possible before (harness @ 7056464)
  • Migration: none

Coupled GGJ growth rates can now account for differential E×B rotation between rational surfaces. Each GGJ surface is evaluated in its own rotating frame, surface_coupling(GGJModel(), params, dp_diag; omega_E=Ω_E, n=n). Before this, every coupled surface saw the same frequency, which is correct only when the surfaces rotate together.

Closes #611

Regression report

regress --refs 10ec452da,705646449 --force (all cases, after develop with #581 was merged in) on feynman (SLURM, 8 threads, Julia 1.11.9, manifest pinned): every case is unchanged, including diiid_slayer_n1, diiid_slayer_n1_coupled and diiid_slayer_n1_winding; gal_resistive_pe is N/A on both refs, as on develop. No harness case runs the coupled GGJ path, which is reachable only through the API. The five test files below also pass on feynman (the runner file now has 164 tests).

On the Mac (Julia 1.11.6) at 4f7f6657e:

  • runtests_dispersion_ggj.jl passes 33/33, including the 5 new rotation tests.
  • runtests_dispersion_coupled.jl (43), runtests_dispersion_coupled_full.jl (26), runtests_dispersion_residual.jl (20) and runtests_slayer_runner.jl (156) pass unchanged.

Follow-up at c62678e11 (review fixes, pushed by @logan-nc): deletes the unreachable _q_shift(::InnerLayerParameters, ::Real) = 0.0 fallback in the runner, notes in the ggj_growth_rates docstring that omega_range must cover the lab-frame ω when rotation is set, and trims the rotation tests (one shared secant helper; the == wiring check and the second frame-invariance check are dropped as covered by the common-rotation root test). Its src/ changes are a dead-method deletion and docstring/comment text, so the harness result at 705646449 carries over; no case reaches the coupled GGJ path. On alfven at c62678e11: runtests_dispersion_ggj (31), _dispersion_rotation (20), _dispersion_coupled_full (26), _dispersion_coupled (43), _slayer_runner (164) and _critical_resonant_field (71) pass.

Notes for reviewers

Why. GGJ is derived in the plasma frame. Rotation enters its equations only beyond the order the derivation keeps (Johnson & Greene 1967, Plasma Phys. 9, 611, Appendix B), so its rotation physics is a Doppler shift. There is no differential rotation inside one layer, but there is between layers. With the determinant scanned in the lab frame, surface k must see s_k = s + i·n·Ω_E,k. This is SLAYER's existing frame change, Q_layer = τ_k·ω_lab − τ_k·n·Ω_E, written in GGJ's variable s = γ + iω (s = −i·Q·Q₀). Thanks to @matt-pharr for pointing this out.

Change.

  • surface_coupling(::GGJModel, ...) takes omega_E (rad/s, per unit n, as in SLAYER) and n, and stores q_shift = n·omega_E. A nonzero omega_E without n ≥ 1 throws.
  • One helper, layer_argument(sc, x), applies each model's shift in its own frequency variable: added to SLAYER's normalized Q, and as i·q_shift to GGJ's s. It is used at all three evaluation sites (the scalar SurfaceCoupling, the reduced MultiSurfaceCoupling and the full MultiSurfaceCouplingFull), so there is no second Doppler path.
  • For SLAYER the helper is the old expression, Q·ratio + q_shift.
  • The shift is along ω, so Re s = γ is unchanged and the γ > 0 domain restriction is unaffected.

Tests, on the existing two-surface coupled fixture:

  • A common rotation leaves γ unchanged and moves the root by Ω/Q₀ in Re Q. The test bound is 1e-8 of |Q|, the same as the existing coupled test; measured 4e-12.
  • Rotating only surface 2 by 0.5 Q₀ moves the coupled γ by −6.0e-4 relative. The test requires more than 1e-4, against a root precision near 1e-12.
  • omega_E without n throws.

Not in this PR.

  • The runner still refuses GGJ.
  • Wiring the profiles' Ω_E into a GGJ TearingProblem belongs to the planned solve(TearingProblem(...), GGJModel(...)) port, which can pass the same omega_E per surface that the SLAYER runner uses.
  • How ω is reported with rotation set (lab frame) is stated in the ggj_growth_rates docstring.

🤖 Generated with Claude Code

…ame in the coupled growth rates

GGJ is a plasma-frame layer, so rotation enters only as a Doppler shift: surface k sees s + i·n·Ω_E,k. surface_coupling(::GGJModel, ...) takes omega_E and n, and one layer_argument helper applies each model's shift (real on SLAYER's Q, imaginary on GGJ's s) in the scalar, reduced and full determinants. Tests: a common rotation only shifts ω; a differential one moves the coupled γ, and only the relative rotation matters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@d-burg d-burg added this to the GPEC v2.0.0 milestone Oct 29, 2026
@d-burg d-burg self-assigned this Oct 29, 2026
@d-burg
d-burg requested a review from matt-pharr October 29, 2026 14:42
@github-actions github-actions Bot added the bugfix Something was wrong and now is not label Oct 29, 2026
@d-burg
d-burg marked this pull request as ready for review October 29, 2026 18:10
@logan-nc

Copy link
Copy Markdown
Collaborator

Merged develop into this branch at 7056464 so it sits on top of the kinetic-profiles move (#581); the merge was clean and the diff against develop is still exactly your commit. At that head, on alfven with 3 threads: runtests_dispersion_ggj 33/33, dispersion_rotation 20/20, dispersion_winding 56/56, dispersion_amr 50/50, slayer_profile_transport 20/20, dispersion_coupled 43/43, dispersion_coupled_full 26/26, slayer_runner 164/164. Nothing else touched.

…te the GGJ rotation tests

Remove the unreachable _q_shift fallback, which would silently drop E×B rotation for a
non-SLAYER surface; note in the ggj_growth_rates docstring that omega_range must cover the
lab-frame ω when rotation is set; share one secant helper in the GGJ coupled tests and drop
the assertions that restate the implementation or repeat the frame-change check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
@logan-nc

Copy link
Copy Markdown
Collaborator

Reviewed #612 and pushed the fixes as c62678e11, so nobody needs to redo them.

Physics: correct. Mapping SLAYER's existing shift Q_layer = Q_lab − τ·n·Ω_E through GGJ's plane convention s = −i·Q/τ gives s_layer = s_lab + i·n·Ω_E, exactly what layer_argument applies. GGJ and SLAYER therefore share one frame convention by construction. Re s is unchanged, so the γ > 0 cut restriction still holds, and n·omega_E in rad/s matches GGJ's s in s⁻¹.

Changed in c62678e11:

  • Deleted _q_shift(::InnerLayerParameters, ::Real) = 0.0 in run_slayer.jl. It is unreachable (the runner refuses GGJ first), and if it were ever reached it would silently drop rotation, the bug class this PR fixes. A future misuse is now a MethodError.
  • ggj_growth_rates docstring: with rotation set, omega_range must cover the lab-frame ω, shifted by about n·Ω_E; a box centred on zero can miss the root.
  • layer_argument comment shortened to one line.
  • Tests: the existing two-surface secant loop and the new secant_root were the same code, so both now use one helper. I dropped mc_rot(Ω, Ω)(s) == mc2(s + im*Ω), which restates the implementation, and the b_frame check, which repeats the frame-invariance that the common-rotation root test already pins. The throw, common-rotation and differential-rotation tests remain (33 → 31, one fewer root solve).

Verification: the dispersion GGJ, rotation, coupled, coupled-full, SLAYER-runner and critical-resonant-field test files pass on alfven at c62678e11. No harness rerun: the src/ changes are a dead-method deletion and docstring text, and no case reaches the coupled GGJ path, so the 705646449 result carries over.

Deferred to the GGJ runner port: the q_shift field now holds normalized Q for SLAYER and rad/s for GGJ, and the two constructors take different rotation arguments (q_shift vs omega_E, n). When solve(TearingProblem, GGJModel) lands, giving both constructors omega_E, n and folding _q_shift into the SLAYER one would unify them.

Still needs a human approval before merge; matt-pharr is requested.

@logan-nc
logan-nc added this pull request to the merge queue Oct 30, 2026
Merged via the queue into develop with commit 4f33fcf Oct 30, 2026
15 of 17 checks passed
@logan-nc
logan-nc deleted the bugfix/ggj-per-surface-doppler branch October 30, 2026 02:37
@d-burg

d-burg commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Physics check on a GGJ-unstable case. On LAR_resistive_match_test (2/1 and 3/1, η and ρ from the deck, Δ′ from its Galerkin solve), I rotated the 3/1 surface relative to the 2/1 and followed the coupled root:

γ [s⁻¹]
2/1 alone 958.6
2/1 + 3/1, no differential rotation (develop before this PR) 973.4
2/1 + 3/1, n·ΔΩ_E ≥ 0.25 Q₀ 958.0–959.4
  • Decoupling: differential rotation removes the coupling's contribution to γ, half of it by n·ΔΩ_E ≈ 0.07 Q₀ (about 1,100 rad/s, with Q₀ = 1.6e4 s⁻¹).
  • Realistic range: the spread of n·ΔΩ_E between surfaces in the DIII-D-like kinetic profiles is 1.5 to 10 Q₀, inside the decoupled regime.
  • Checks: independent full scans at λ = 1 and 10 agree with the tracked root to 1e-7.
  • Caveat: the rotation is prescribed, so the horizontal axis is a sweep, not a prediction.
  • Why not the DIII-D-like example itself: GGJ finds it stable at γ > 0 (its interchange channel stabilizes the 2/1), so it has no unstable root to show this on.
image

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tearing - BUGFIX - Doppler-shift each GGJ surface into its own frame in the coupled growth-rate determinant

2 participants