Skip to content

Tearing - FEATURE - Classify tearing roots by winding number - #597

Merged
matt-pharr merged 5 commits into
bugfix/slayer-polish-antianalyticfrom
feature/tearing-winding-classification
Oct 29, 2026
Merged

matt-pharr merged 5 commits into
bugfix/slayer-polish-antianalyticfrom
feature/tearing-winding-classification

Conversation

@d-burg

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

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: none by default; root_classification = "winding" is opt-in (harness @ 36d54d2)
  • Migration: none

Tearing roots can now be classified by the argument principle. With root_classification = "winding" in [SLAYER] (or classification = :winding in find_growth_rates), each contour intersection is a root, a pole or a near-miss according to the winding number of the dispersion residual on a small circle around it, instead of the pole threshold and the above-pole/geometry filters. The reported root is the least stable zero, and the winding numbers are written to Tearing/Diagnostics/*Winding.

Changes since the review

Regression report

Latest full SDCC comparison (2026-10-29): aac35ab68 (parent) versus 36d54d2fa (this PR plus the documentation-only correction), all 22 cases / 44 tasks on sirius, Julia 1.12.6, pinned manifest 4c9d54fa, 8 Julia threads / 4 BLAS threads. All 296 comparable quantities across 20 shared cases are unchanged, including the uncoupled, alternate-transport, and coupled SLAYER cases. The new diiid_slayer_n1_winding case succeeds on 36d54d2fa (203.5 s) and fails on the parent, which does not support root_classification; this is not a successful two-ref comparison for that new case. gal_resistive_pe still has eight missing/N/A quantities on both refs and provides no numerical validation. No other task failed. Report job 1300276; SDCC report: /home/ITER/pharrm/Projects/GPEC_dev/regress-fanout/runs/pr597_36d54d2fa_full/report.txt.

The tested documentation-only correction 36d54d2fa is now the PR head. Numerical behavior is unchanged by that commit.

Earlier comparison:

regress --refs 5828895a0,fbdd6eb07 --force (all cases, against #577's head) on feynman (SLURM, 8 threads, Julia 1.11.9, manifest pinned): every case is unchanged, including diiid_slayer_n1 (17) and diiid_slayer_n1_coupled (7), so threshold mode is untouched. The new diiid_slayer_n1_winding case fails on the base, which has no root_classification key, so the harness exits 1; gal_resistive_pe is N/A on both refs. At fbdd6eb07 the winding case finds the same three roots as threshold mode, with γ (291.1, −339.4, −1395.3 s⁻¹) agreeing to within 6e-8 relative and ω to within 2e-8, in 277 s. runtests_dispersion_winding.jl (56), runtests_slayer_runner.jl (156), runtests_dispersion_scan.jl (33), runtests_dispersion_amr.jl (50) and runtests_dispersion_polish.jl (65) pass.

Supplemental SDCC validation after rebasing

At rebased head 18d88c91c, four DIII-D-like original/proposed comparisons (uncoupled, alternate transport, coupled, coarse coupled) produced identical roots, frequencies, growth rates, poles and flags. Both classifiers agreed with all 41 audited production circles. Of 32 radius-stress checks, the proposed hardening returned unresolved once where the original and dense reference agreed on zero; all finite accepted counts agreed with the reference. The prototype passed 369 focused checks but cost 4–5 times the production-circle evaluations. It is deferred as a possible future extension, not included in this PR's numerical behavior.

The documentation-only correction 36d54d2fa has executable syntax identical to 18d88c91c and was subsequently tested by the full SDCC harness reported above. The hardening prototype remains deferred.

Notes for reviewers

Why. The threshold classifier decides root vs pole from the size of Re Δ along the Im Δ = 0 contour, then prunes with the above-pole and :geom/:gap heuristics. None of these is a proof, and the threshold is scale-dependent. In coupled SLAYER scans of a current ramp-up, the threshold path listed two intersections as poles and reported no root; both wind −1 on circles of three radii, i.e. they are isolated zeros (that deck used the absolute pole_threshold = 10 against |det| ~ 2e8). Near a root–pole crossing the labels are equally unreliable. The argument principle counts zeros minus poles inside the circle; the implementation estimates this from finite samples and can miss rapid phase turns between samples. Classification does not depend on an amplitude threshold. We have used this acceptance rule out of tree (a driver around run_slayer_from_inputs) on every point of the coupling-paper scans since 2026-10-07; this PR moves it into the package so other analyses, and GGJ (stacked PR), can share it.

Orientation. The SLAYER residual is a function of conj(Q) (solve_inner evaluates conj(Δ̂(ĝ)), the same fact #577 fixes the polish for), so its zeros wind −1 and its poles +1. A holomorphic residual, e.g. the GGJ full-matching determinant, has the opposite sign. find_growth_rates takes zero_winding = ±1; the runner sets −1 for SLAYERModel and refuses other models in winding mode for now. The tests show that the wrong sign swaps roots and poles.

Method. The winding classifier (_winding_classify in GrowthRateExtraction.jl) is one step inside the shared _run_analysis, in place of the pole threshold; everything after it (polish, the +γ-step flag, root selection, :geom/:gap warnings, the secondary root, flags) is the same code in both modes.

  • contour intersections exactly as now, duplicates merged;
  • circle radius winding_rfrac (default 0.5) × the local contour-segment length at the intersection, so circles follow the cell size on AMR meshes, capped at 0.45 × the distance to the nearest other intersection;
  • the circle is sampled at 64 points and doubled (reusing samples) until no phase step exceeds π/2, up to 4096; a non-finite circle (the residual failed on it) is retried at half and a quarter of the radius; still unresolved → filtered, flag :winding_unresolved;
  • net zeros inside → root; net poles → pole; a zero count is checked once on a circle eight times wider that may reach 0.9 of the distance to the nearest intersection (a pole's contour estimate can be half a cell off), and otherwise goes to filtered_roots with flag :spurious;
  • the classification replaces the pole threshold, the validity gate and the above-pole/outside-Re filter; a root is polished with its classifying circle as the trust region;
  • the argument principle requires a single-valued meromorphic residual on and inside the contour; branch cuts and under-resolved phase turns invalidate the inference. Finite sampling is not a proof (documented).

The coupled path passes the determinant as residual with polish = false, so in winding mode the determinant is evaluated around intersections only to count; in threshold mode it is not passed and nothing changes.

Interface. GrowthRateResult gains classification and three winding vectors aligned with valid_roots/poles/filtered_roots (empty in threshold mode); the 13-argument constructor still works. find_growth_rates gains classification, zero_winding, winding_rfrac and polish (default true, so residual alone keeps polishing as before). HDF5: Diagnostics/{ValidRoots,Poles,FilteredRoots}Winding are written only when a winding classification ran, so threshold-mode files are unchanged. _write_ragged_complex! is now _write_ragged! and takes Float64 rows as well.

Cost. Each intersection costs 64 to a few thousand residual evaluations. On diiid_slayer_n1 the winding variant runs in 271 s against 279 s for threshold mode, and its growth rates agree with threshold mode to about 1e-8 relative.

Not in this PR. Zeros the scan does not resolve as contour crossings are still missed, in both modes. The default stays threshold; switching it would move results and belongs in its own PR.

Stacked on #577 (bugfix/slayer-polish-antianalytic).


🤖 Generated with Claude Code

@github-actions github-actions Bot added the feature New capability label Oct 26, 2026
@matt-pharr
matt-pharr added this pull request to stack #601 October 27, 2026 08:55
@d-burg d-burg modified the milestone: GPEC v2.0.0 Oct 28, 2026
@d-burg
d-burg requested a review from matt-pharr October 29, 2026 03:25
@d-burg d-burg self-assigned this Oct 29, 2026
@d-burg
d-burg marked this pull request as ready for review October 29, 2026 04:29
d-burg and others added 4 commits October 29, 2026 10:33
Add root_classification = "winding" to [SLAYER] and classification = :winding
to find_growth_rates. Each contour intersection is classified by the winding
number of the dispersion residual on a small circle around it (argument
principle): a root, a pole, or a near-miss, instead of the pole threshold and
the above-pole/geometry filters. The SLAYER residual is a function of conj(Q),
so its zeros wind -1; zero_winding = +1 serves holomorphic residuals. The
reported root is the least stable zero, polished inside its circle; winding
numbers are stored on GrowthRateResult and written to
Tearing/Diagnostics/*Winding. The default stays "threshold".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… analysis and harden its circles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e coupled root in develop's normalization

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… root classification

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matt-pharr
matt-pharr force-pushed the feature/tearing-winding-classification branch from fbdd6eb to 18d88c9 Compare October 29, 2026 09:34
Describe finite phase sampling as an estimate rather than a proof. Note midpoint and count-convergence checks as possible defensive extensions without changing classification or polishing.
@matt-pharr

Copy link
Copy Markdown
Collaborator

Defensive winding-count hardening is deferred; the current classifier remains unchanged.

A synthetic counterexample shows that sampled phase increments below pi/2 do not certify the winding count: on the unit circle, f(z) = (z - 0.999 exp(ipi/64)) / (z - 1.001 exp(ipi/64)) has winding +1, but the initial 64 samples alias it to zero. A possible future extension is to require midpoint variation and agreement across successive refinements, returning unresolved when the evaluation budget is exhausted.

We tested that prototype on SDCC against this PR's rebased head, 18d88c9, using the DIII-D-like uncoupled case, alternate chi_perp_e transport, coupled case, and a coarse coupled scan. Roots, frequencies, growth rates, poles, and flags were unchanged in all four original/proposed comparisons. Both methods agreed with all 41 independently audited production circles. In 32 radius-stress checks, every finite accepted count agreed with the dense reference; the prototype additionally returned unresolved for one circle where the original and dense reference correctly returned zero. References used phase-offset dense grids and refinement checks; these are numerical evidence, not mathematical certification.

The prototype required 4–5 times as many classifier evaluations on production circles, adding roughly 2–4 seconds in the measured classifier work. All 369 focused checks passed on the prototype. This establishes a synthetic failure mode and a possible defensive improvement, but does not demonstrate a classification failure in the tested production cases. The hardening is therefore not being included as a numerical change in this PR.

A documentation-only correction removes the claims that the sampled count is an exact proof and records hardening as a possible extension. It is committed locally as 36d54d2 and is not pushed yet. Executable syntax was verified unchanged. Full regression-harness provenance remains the existing fbdd6eb run; the SDCC study is supplemental validation of 18d88c9 plus the separately recorded prototype, not a new full harness stamp.

SDCC evidence: array 1300248, report job 1300259, /home/ITER/pharrm/Projects/GPEC_dev/pr597-winding-validation-20261029/report.txt. Prototype patch SHA-256: 1b0b2825f3a77f3c4f1bca5877568a58da9745eddabea3b9fbfcf2e574771d5a.

Human review/approval remains with Matthew; no automated approval or merge was performed.

@matt-pharr matt-pharr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is good besides some wordy documentation. Went through and tested it pretty extensively with claude, I think this is fine as the version for 2.0.0. It came up with special cases where it might not work, but we couldn't produce those so it might be fake lol. Approve

@matt-pharr
matt-pharr enabled stacked auto-merge October 29, 2026 11:13
stacked auto-merge was automatically disabled October 29, 2026 11:27

Auto-merge was disabled for another pull request in the stack

@matt-pharr
matt-pharr merged commit e192749 into develop Oct 29, 2026
10 checks passed
@matt-pharr
matt-pharr deleted the feature/tearing-winding-classification branch October 29, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants