Skip to content

MAC-based poloidal-m estimator with even/odd confidence#66

Draft
d-burg wants to merge 4 commits into
developfrom
feature/mac-poloidal-m
Draft

MAC-based poloidal-m estimator with even/odd confidence#66
d-burg wants to merge 4 commits into
developfrom
feature/mac-poloidal-m

Conversation

@d-burg

@d-burg d-burg commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What

Wires the Olofsson MAC (eigspec eq 9) into a poloidal mode-number estimator that answers even/odd — 1/1 vs 2/1 — with a probability, an independent shape-based cross-check on poloidal_phase_fit's slope fit.

The GP mode-shape machinery + mac()/mac_n_spectrum() already existed but were wired into nothing; θ* elongation landed in #38. This connects them for the poloidal side and adds the uncertainty layer.

How

  • core.mode_shape
    • mac_m_probability() — Monte-Carlos the MAC over the per-probe phase σ → a posterior P(m) (even/odd split), deterministic under a fixed seed.
    • poloidal_m_spectrum() — the poloidal-specific prep: match the phase pattern (unit phasors), because a mode number is a phase winding and the per-probe amplitude on a poloidal array spans 3–4 decades of probe sensitivity/flux-surface geometry (an amplitude-weighted MAC gets captured by one big probe). SNR-gates dead/saturated channels.
  • core.contracts / contract.ts — a bar kind for the P(m) spectrum (+ NodeView renderer).
  • service.nodespoloidal_m_spectrum node; the verdict is gated on the absolute MAC, so a poor match reads as "no clean poloidal mode" rather than a spuriously precise m/n.
  • web (RotatingTab) — a MAC + confidence tile: P(m) bars, nominal-MAC overlay, and a verdict banner coloured by fit quality.

Why phase-pattern + SNR gate

On real DIII-D 174446 the raw amplitude-weighted MAC was ~0.07 everywhere (dominated by 1–2 near-saturated probes; amplitude spread up to 29000×). Phase-pattern + a 5% SNR gate (typically 8–28 of 48 MPID probes carry signal) recovers clean identifications:

t (ms) verdict MAC quality
2500 m/n = 3/2 (P=1.00 odd-m) 0.77 clean
3000 m/n = 1/3 (P=0.94 odd-m) 0.61 clean
3500 m/n = 3/2 (P=1.00 odd-m) 0.61 clean
4000 no clean poloidal mode 0.19 weak

Tests

  • tests/test_mode_shape.py::TestMacMProbability, ::TestPoloidalMSpectrum — peak/parity on clean modes, posterior normalization, noise spreading the posterior, SNR-gate drops dead probes (+ aliasing-aware fixture), seed reproducibility.
  • tests/test_nodes.py::test_poloidal_m_spectrum_node — end-to-end against the synthetic-shot fixture.
  • Full suite: 223 passed. ruff format/check clean; GUI tsc/eslint clean; production build OK.

Notes / follow-ups

  • Per-probe σ on the rotating path is real (cross-spectral), so the confidence is genuine — no constant-σ placeholder here.
  • Resolvable |m| is still bounded by the sparse, uneven poloidal array (aliasing); the MC surfaces this as a spread/bimodal P(m) rather than hiding it.
  • The SNR gate (5%) and MAC quality thresholds (0.6/0.3) are heuristics — easy to tune.

🤖 Generated with Claude Code

Wire the Olofsson MAC (eigspec eq 9) into a poloidal mode-number
estimator that answers even/odd (1/1 vs 2/1) with a probability, an
independent cross-check on poloidal_phase_fit's slope fit.

- core.mode_shape: mac_m_probability() Monte-Carlos the MAC over the
  per-probe phase σ to yield P(m); poloidal_m_spectrum() does the
  poloidal-specific prep — match the *phase pattern* (unit phasors, since
  a mode number is a phase winding and the per-probe amplitude spans 3-4
  decades of probe sensitivity), SNR-gating out dead/saturated probes.
- contracts: a `bar` kind (both sides) for the P(m) spectrum.
- service: poloidal_m_spectrum node; verdict gated on the absolute MAC so
  a low match reads as "no clean poloidal mode", not a spurious m/n.
- web: MAC+confidence tile (P(m) bars, nominal MAC overlay, verdict
  banner coloured by quality) in the rotating tab.

On real DIII-D 174446 this resolves clean 3/2 and 1/3 modes (MAC
0.6-0.77) and honestly flags mode-free slices.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Copilot AI review requested due to automatic review settings July 1, 2026 19:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR wires an existing MAC (Modal Assurance Criterion) mode-shape implementation into a new poloidal mode-number (m) probability spectrum node, exposing an even/odd confidence (e.g., 1/1 vs 2/1) as a GUI tile and providing an independent cross-check to the existing poloidal_phase_fit slope-based estimator.

Changes:

  • Add core logic for a Monte-Carlo MAC-based P(m) estimator (mac_m_probability) plus a poloidal-specific phase-pattern + SNR-gated wrapper (poloidal_m_spectrum).
  • Introduce a new contract node kind bar and render it in the frontend (NodeView) and the Rotating tab (new “MAC + Confidence” card).
  • Add service wiring for a new node id poloidal_m_spectrum and extend tests to cover the new node and core behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/test_service_api.py Expands valid node-kind set to include the new bar kind.
tests/test_nodes.py Adds end-to-end node-builder test for poloidal_m_spectrum.
tests/test_mode_shape.py Adds unit tests for mac_m_probability and poloidal_m_spectrum behavior (parity, normalization, gating, reproducibility).
src/magnetics/service/nodes.py Adds _poloidal_m_spectrum builder and registers it in _BUILDERS.
src/magnetics/core/mode_shape.py Implements MacMResult, mac_m_probability, and poloidal_m_spectrum (SNR gate + phase-pattern MAC).
src/magnetics/core/contracts.py Adds contracts.bar() node constructor.
gui/web/src/lib/NodeView.tsx Adds Plotly rendering for bar nodes (with optional secondary overlay).
gui/web/src/lib/contract.ts Extends TypeScript contract with BarNode and updates the Node union.
gui/web/src/components/tabs/RotatingTab.tsx Fetches and displays poloidal_m_spectrum as a MAC+confidence UI tile.
Comments suppressed due to low confidence (1)

src/magnetics/service/nodes.py:940

  • except ValueError, KeyError: is invalid Python syntax (it triggers a SyntaxError at import time). Use a tuple for multiple exceptions so the service can start and the node builder can be imported.
    return geometry.elongation_theta_star(thetas_deg, kappa), True

@d-burg d-burg marked this pull request as draft July 1, 2026 20:02
d-burg and others added 2 commits July 1, 2026 16:54
Add EFIT 2-D profile support so the q-profile (q vs ψ_N, in time) can be
pulled and read back, for the rotating mode-number anchor.

- signals: TREE_PROFILES / tree_profiles_for — q_profile from EFIT02 only
  (MSE-constrained; no efit01 fallback, so a non-MSE q is never silently
  substituted — the profile is simply omitted if efit02 lacks it).
- toksearch: a Profile record + _mdsthin_tree_profiles (2-D fetch,
  orientation-robust, ψ_N = uniform gEQDSK grid), wired additively into the
  mdsthin backend and written as its own h5 group; the existing scalar/
  pointname path is untouched.
- h5source: load_q_profile() reader; channel_names() skips profile groups so
  they never masquerade as (data, time) channels.
- synthetic_shot: a physical synthetic q-profile (q0≈1.05→q_edge≈4.5) so the
  read/anchor chain is exercised in CI without any tokamak data.

The live mdsthin fetch needs a re-pull to validate against real EFIT02;
toksearch/remote backends don't yet fetch profiles (follow-up).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The poloidal MAC winner is easily an alias on the sparse, uneven poloidal
array (a physical high-m mode folds down to a spurious small one), so the
bare fit could report an unphysical mode like m/n = 1/3 (q ≈ 0.33, inside the
q ≈ 1 axis) with false confidence. Fix it three ways:

- core.equilibrium (new): resonant_surfaces() + anchor_poloidal_m() — for the
  reliable toroidal n, keep only m whose q = m/n has a real rational surface
  (q_min ≤ m/n ≤ q_max) and, if the raw MAC winner is unphysical, swap in the
  best-MAC physical alias. Returns the q surface + its ψ_N.
- core.mode_shape: mac_m_probability now also reports the MAC margin
  (best − next-best) and the alias set within a margin of the best.
- service.nodes: poloidal_m_spectrum reads the q-profile (_q_at) and anchors m
  to it; a margin-aware quality ladder — weak < ambiguous (alias near-tie, no q
  to break it) < marginal < clean < q-anchored — so a near-tie reads as
  "ambiguous: |m| ∈ {…}" instead of a false-confident integer.
- web: verdict banner shows the q surface / ψ_N when anchored, the alias set +
  margin otherwise, coloured by trust.

On the synthetic shot the ground-truth 2/1 is recovered from an aliased MAC
(q=2.00 @ ψ_N=0.52); real shots without a q-profile now honestly report the
ambiguity (e.g. 174446 @ 3000 ms → "|m| ∈ {1,2,6}") instead of "1/3".

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants