Skip to content

Analysis/ErrorFields - FEATURE - Add error-field assessment plots and coil-array phasing maps - #453

Merged
ebursch merged 82 commits into
developfrom
feature/errorfields-analysis-plots
Sep 28, 2026
Merged

ebursch merged 82 commits into
developfrom
feature/errorfields-analysis-plots

Conversation

@logan-nc

@logan-nc logan-nc commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator

Eighth PR of the OMFIT → Julia error-field tolerance migration, stacked on #452. It adds the plots of the assessment and the coil-array phasing map: everything the paper's figure scripts produced from the OMFIT project, as Analysis.ErrorFields functions that read gpec.h5 and take label => h5path lists so coil-design revisions overplot.

What it does

  • src/ErrorFields/Phasing.jl (compute side): phasing_map(sens, dom, coil_names) / phasing_map("gpec.h5", coil_names; psi_low, psi_high, mode) — the dominant-mode overlap per kilo-ampere-turn of N coil arrays against their N−1 relative current-pattern phases, |Σ_k δ_k e^{iφ_k}|, plus the resonant fraction of the applied field 100·|Vᴴb̃|/‖b̃‖; extreme_phasing reads the extreme off the grid. Closed form on already-stored spectra, no optimizer, as in the paper's EFCC phasing figure.
  • src/Analysis/ErrorFields.jl (plots; each takes Vector{Pair{String,String}} with a single-path wrapper): plot_coil_sensitivities (grouped bars matched by coil name: shift per mm, tilt per 0.1°, or nominal), plot_tolerance_pdf (intrinsic/corrected distributions, as-designed marks), plot_locking_risk (risk vs tolerance scale with batch spread and the allowable scale at a target marked), plot_threshold_scaling (threshold density and P(lock|δ) against the distributions), plot_dominant_mode_spectrum (|V[m, mode]|, :steppre), plot_phasing_map (line for two arrays, filled contour for three), plot_error_field_summary (2×2). Panels report missing data instead of erroring.
  • The example's analyze_example.jl now uses these instead of hand-rolled plots.

Verification

  • test/runtests_phasing.jl (synthetic spectra): two-array map equals |δ_1 + δ_2 e^{iφ}|, its maximum |δ_1| + |δ_2| at arg δ_1 − arg δ_2, minimum ||δ_1| − |δ_2||; resonant fraction equals 100|Vᴴb̃|/‖b̃‖ and never exceeds 100; three-array grid uses cumulative phases; guards.
  • The Solovev run test now smoke-tests every plot on the run's gpec.h5 (returns a Plots.Plot, saves a PNG) including the two-hoop phasing line and the missing-data panels on a file without risk output.

Convention worth a look (second commit): phasing_map normalizes each array by the magnitude of its ampere-turns, |nw| × max|I|. The zero of each phase axis is then the array's current pattern exactly as the run specified it, with the winding sense of its geometry file included — a negative winding multiplier (the DIII-D C-coil file has nw = −4) or a negated current pattern shows up in the map as the half-turn it physically is. The OMFIT phasing script divided by the signed product, which erases how a device defines positive current in that array; it was right for the original device only because every array there had a positive multiplier. The first commit also rejected a negative product as "no current"; that guard now checks only the magnitude.

cc @matt-pharr

Release note

  • Audience: users
  • Numerical impact: none (harness @ d83c9e8)
  • Migration: none

Analysis.ErrorFields plots the error-field assessment from gpec.h5 — coil sensitivities, tolerance Monte Carlo distributions, locking risk vs tolerance, threshold scaling, the dominant mode, and coil-array phasing maps — with lists of label => file pairs to overplot design revisions; ErrorFields.phasing_map computes the phasing map itself.

Regression report

Regression Report: diiid_n1
====================================================================================================================
Ref 1: develop  @ 9578c9b86 (2026-09-24)
       env: julia 1.11.7, x86_64-linux-gnu, manifest 06e27666 (pinned), 16 threads/16 BLAS
Ref 2: feature/errorfields-analysis-plots  @ d83c9e8bf (2026-09-24)
       env: julia 1.11.7, x86_64-linux-gnu, manifest 06e27666 (pinned), 16 threads/16 BLAS
--------------------------------------------------------------------------------------------------------------------
Quantity                                      develop          feature/errorfields-analysis-plots  Diff       Status
--------------------------------------------------------------------------------------------------------------------
total energy Re(et[1])                        8.012318e-01     8.012318e-01                        0.0e+00    OK    
total energy Im(et[1])                        4.142529e-05     4.142529e-05                        0.0e+00    OK    
plasma energy Re(ep[1])                       -1.348486e+00    -1.348486e+00                       0.0e+00    OK    
vacuum energy Re(ev[1])                       2.149718e+00     2.149718e+00                        0.0e+00    OK    
vacuum matrix min eigenvalue                  1.873976e-01     1.873976e-01                        0.0e+00    OK    
plasma energy (all)                           [35 elem]        [35 elem]                           0.0e+00    OK    
vacuum energy (all)                           [35 elem]        [35 elem]                           0.0e+00    OK    
total energy (all)                            [35 elem]        [35 elem]                           0.0e+00    OK    
ODE steps (saved)                             2576             2576                                0.0e+00    OK    
ODE steps (total)                             4572             4572                                0.0e+00    OK    
q0                                            1.204212e+00     1.204212e+00                        0.0e+00    OK    
q95                                           4.781723e+00     4.781723e+00                        0.0e+00    OK    
beta_t                                        1.327024e-02     1.327024e-02                        0.0e+00    OK    
beta_n                                        1.372511e+00     1.372511e+00                        0.0e+00    OK    
internal inductance li1                       8.842392e-01     8.842392e-01                        0.0e+00    OK    
internal inductance li2                       7.080847e-01     7.080847e-01                        0.0e+00    OK    
internal inductance li3                       7.304433e-01     7.304433e-01                        0.0e+00    OK    
poloidal beta betap1                          6.680744e-01     6.680744e-01                        0.0e+00    OK    
poloidal beta betap2                          5.349834e-01     5.349834e-01                        0.0e+00    OK    
poloidal beta betap3                          5.518761e-01     5.518761e-01                        0.0e+00    OK    
# singular surfaces                           5                5                                   0.0e+00    OK    
singular psi locations                        [5 elem]         [5 elem]                            0.0e+00    OK    
singular q values                             [5 elem]         [5 elem]                            0.0e+00    OK    
current beta betaj                            4.236478e-01     4.236478e-01                        0.0e+00    OK    
plasma volume                                 1.829472e+01     1.829472e+01                        0.0e+00    OK    
plasma current                                1.152130e+00     1.152130e+00                        0.0e+00    OK    
mpert                                         35               35                                  0.0e+00    OK    
npert                                         1                1                                   0.0e+00    OK    
toroidal field bt0                            2.006573e+00     2.006573e+00                        0.0e+00    OK    
wall field bwall                              3.880145e-01     3.880145e-01                        0.0e+00    OK    
aspect ratio                                  2.845746e+00     2.845746e+00                        0.0e+00    OK    
elongation kappa                              1.708350e+00     1.708350e+00                        0.0e+00    OK    
q profile (checksum)                          0cd285cea88d...  0cd285cea88d...                     identical  OK    
pressure profile (checksum)                   a1c48b266622...  a1c48b266622...                     identical  OK    
Mercier D_I profile (checksum)                eeb06744e795...  eeb06744e795...                     identical  OK    
resistive interchange D_R profile (checksum)  fa37296851f6...  fa37296851f6...                     identical  OK    
ballooning Delta' profile (checksum)          6eb0ea075ccf...  6eb0ea075ccf...                     identical  OK    
island half-widths                            [5 elem]         [5 elem]                            0.0e+00    OK    
Chirikov parameter                            [5 elem]         [5 elem]                            0.0e+00    OK    
||resonant area-weighted field||              5.189179e-04     5.189179e-04                        0.0e+00    OK    
dominant-coupling singular values             [3 elem]         [3 elem]                            0.0e+00    OK    
|forcing overlap with dominant mode|          1.415683e-04     1.415683e-04                        0.0e+00    OK    
|delta_nominal| of coil set 1                 7.055228e-05     7.055228e-05                        0.0e+00    OK    
||ddelta/d(shift)|| over coil sets            1.246233e-04     1.246233e-04                        0.0e+00    OK    
||ddelta/d(tilt)|| over coil sets             4.180643e-07     4.180643e-07                        0.0e+00    OK    
PE plasma energy                              3.422677e+00     3.422677e+00                        0.0e+00    OK    
PE vacuum energy                              3.174510e+00     3.174510e+00                        0.0e+00    OK    
PE surface energy                             5.826684e+00     5.826684e+00                        0.0e+00    OK    
PE toroidal torque                            5.087465e-02     5.087465e-02                        0.0e+00    OK    
NTV torque FGAR [N·m]                         5.296762e-01     5.296762e-01                        0.0e+00    OK    
NTV kinetic energy dW FGAR [J]                7.924971e-02     7.924971e-02                        0.0e+00    OK    
Runtime (s)                                   280.2s           290.8s                                         --    
||forcing b~|| (root-area-weighted)           4.683328e-04     4.683328e-04                        0.0e+00    OK    
resonant area-weighted field b^r              [5 elem]         [5 elem]                            0.0e+00    OK    
====================================================================================================================
Summary: 53 unchanged

Harness, re-run at this branch head

d83c9e8bf compared against develop (9578c9b86), case diiid_n1:

Summary: 53 unchanged

Every tracked quantity is bit-identical; only wall-clock Runtime differs, which the
harness does not track. The branch carries a merge of the current develop.

logan-nc and others added 4 commits September 11, 2026 18:22
Random draws of coil misalignment within a tolerance, the primitives a tolerance Monte Carlo
recombines with the coil sensitivities: the RadialDistribution shapes (Flat = uniform in
radius, UniformArea, Hollow, Ring, PowerLaw) with radial_distribution for the tolerance-file
names; disk_radius / sample_disk (Δx + iΔy) / sample_uncertainty (signed normal amplitude with
a uniform direction); and the two tolerance models — sample_additive (independent shift and
tilt disks) and sample_cylinder (axis endpoints drawn in the top and bottom disks of a cylinder,
the coil placed at the midplane crossing and tilted by the lean, as the (θx, θy) rotation angles
apply_transforms uses). Every sampler takes the caller's rng, scalar exponents, and optional
fixed directions so coherent groups can share a direction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
run_monte_carlo recombines a SensitivityTable with a ToleranceSet: per sample every coil set's
misalignment is drawn within its tolerance (additive or cylinder model plus Gaussian placement
uncertainty), coherent groups get one shared draw with the lateral shift of their rigid
rotation, the unattributed budget a random direction, and the dominant-mode overlap
δ = Σ δ_nominal + S·Δ + T·θ is histogrammed twice — intrinsic and with every correctable term
divided by efc_factor. Names resolve to plain arrays before the loop, group sensitivities are
pre-summed, batches are seeded individually (bit-identical for any thread count) and kept for
error bars; disk_radius takes a sqrt/cbrt fast path for the built-in shapes. The run writes the
full-window dominant-mode summary to ErrorFields/MonteCarlo/ when a tolerance file is named
([ErrorFields.MonteCarlo] settings); any other window, mode, tolerance scale or coil subset is
a post-hoc run_monte_carlo("gpec.h5"; ...) from the echoed tolerances and coil snapshot. The
DIII-D error-field example gains illustrative F-coil tolerances and plots the distributions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
From the Monte Carlo overlap distribution to an engineering answer. Risk.jl carries the nine
published ITPA penetration-threshold fits (ThresholdScaling, threshold_scaling(; n, dataset,
fit); Logan et al. PPCF 62 084001 and NF 60 086010, 2020), the operating point
(ScenarioParameters, density from [ErrorFields.scenario], the rest from the equilibrium),
threshold sampling within the fit errors, and locking_risk: P(lock|δ) is the sampled
threshold's cumulative distribution on the Monte Carlo's own bin edges and the machine's risk
100 ∫ pdf(δ) P(lock|δ) dδ per batch, intrinsic and corrected, with the as-designed and
sharp-threshold risks. tolerance_scan repeats the Monte Carlo over tolerance multipliers and
allowable_tolerance inverts the scan in log-log space for a target risk. The run writes
ErrorFields/Risk/ and ErrorFields/Risk/ToleranceScan/ ([ErrorFields.Risk] settings); the same
functions re-run any window, fit, subset or target from gpec.h5. The example gains the
scenario, a smaller intrinsic C-coil field and tolerances that put the risk curve through the
1 % target.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
… coil-array phasing maps

Analysis.ErrorFields plots the assessment from gpec.h5 — per-coil sensitivities matched by
name across runs, the tolerance Monte Carlo distributions, the locking risk against tolerance
scale with the allowable scale at a target marked, the threshold density and P(lock|δ), the
dominant coupling mode spectrum, coil-array phasing maps, and a four-panel summary — every
function taking a list of label => file pairs so coil-design revisions overplot, with a
single-path wrapper. ErrorFields.phasing_map evaluates |Σ_k δ_k e^{iφ_k}| per kilo-ampere-turn
and the resonant fraction of the applied field on a grid of the relative current-pattern
phases of N arrays from the stored nominal spectra, the closed form behind the paper's EFCC
phasing figure; extreme_phasing reads the extreme off the grid.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
@logan-nc logan-nc self-assigned this Sep 12, 2026
@logan-nc
logan-nc requested a review from ebursch September 12, 2026 00:44
@github-actions github-actions Bot added the feature New capability label Sep 12, 2026
…alization

phasing_map divided each array's spectrum by the signed product of winding multiplier and
peak current and rejected a negative product as "no current", so an array whose geometry file
carries a negative winding multiplier (the DIII-D C-coil) could not be mapped at all, and had
it been, the division would have erased how that device defines positive current. The per-kAt
normalization now uses the magnitude: the zero of each phase axis is the array's current
pattern exactly as specified, winding sense included, and a negative multiplier appears in the
map as the half-turn it physically is (tested). The guard keeps its one purpose, a set with no
current.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
@logan-nc

Copy link
Copy Markdown
Collaborator Author

Stack summary — OMFIT → Julia error-field tolerance migration (updated 2026-09-17)

Nothing in this set merges without a third-party human review, the three small bugfix PRs included.

Full page with the diagram, per-PR table, review order and links to every review package (a private claude.ai artifact until the author shares it; DIII-D-like and synthetic data only): https://claude.ai/code/artifact/408ddd36-e52c-42d8-8f5c-52f680f41da9

Group PRs Base
Bugfixes the stack's numbers depend on — review first #458 root-area weight (careful review: Fourier convention inside one routine), #457 helicity from the current sign, #460 interior-start initialization develop
Prerequisites #446 resonant-coupling SVD API, #447 per-coil-set forcing modes develop
ErrorFields stack, each based on the previous branch #448 sensitivities → #449 tolerance TOML → #450 sampling → #451 Monte Carlo → #452 risk + scan → #453 plots + phasing → #455 NTV limits → #462 NTV torque against rotation #448 on develop (carries merges of #446/#447); rebase the chain after the bugfixes and #446/#447 land

Benchmark, qualitatively. The original OMFIT project's case, rebuilt from that project's own run inputs, is reproduced once #458, #457 and #460 are in: dominant-mode singular values agree to 1e-4, singular-coupling rows match Fortran GPEC at 1.0000 correlation and within 0.5 % in norm, and every coil set's dominant-mode overlap agrees within 1 % once both codes use the same converged toroidal coil grid. The DIII-D-like example agrees with Fortran to 1e-4 throughout. Any comparison shown on these PRs uses the DIII-D-like examples only.

Design issue for what comes after the stack: #461 (momentum-balance module fed by tabulated torque surfaces; stationary solvers only).

logan-nc and others added 7 commits September 17, 2026 16:26
… resonant-coupling window

Follows the PerturbedEquilibrium change that makes psi_N <= 0.9 (CORE_PSI_HIGH) the default
window of the dominant-mode SVD: the gpec.h5 entry points of this stage now default to the same
window instead of the full one, so in-memory and post-hoc analyses agree by default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
… resonant-coupling window

Follows the PerturbedEquilibrium change that makes psi_N <= 0.9 (CORE_PSI_HIGH) the default
window of the dominant-mode SVD: the gpec.h5 entry points of this stage now default to the same
window instead of the full one, so in-memory and post-hoc analyses agree by default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
… resonant-coupling window

Follows the PerturbedEquilibrium change that makes psi_N <= 0.9 (CORE_PSI_HIGH) the default
window of the dominant-mode SVD: the gpec.h5 entry points of this stage now default to the same
window instead of the full one, so in-memory and post-hoc analyses agree by default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
@logan-nc

Copy link
Copy Markdown
Collaborator Author

Window change merged up the stack (2026-09-17): the dominant-mode SVD and every entry point built on it now default to the core window ψ_N ≤ 0.9 (PerturbedEquilibrium.CORE_PSI_HIGH, from #446 at f43dd78), instead of the full window. In-memory and gpec.h5 analyses therefore agree by default, and other windows remain an explicit analysis choice. The DIII-D-like error-field example's analysis uses the core window alone (the earlier edge-only contrast is gone). Tests on this branch pass after the merge; harness at the top of the stack (#462): against develop, 47 unchanged plus the stack's new quantities; against the previous top (98013a6), the window moves exactly what it should and nothing else — diiid_n1: dominant singular values now over the 3 core surfaces instead of 5, the forcing overlap, |delta_nominal| and the shift/tilt sensitivity norms by 6–11 %; diiid_error_field: the C-coil coupling per kAt by 11 %, the residual torque (whose projection uses the mode) by 56 %, the whole-field torque, ω_ref and the scan grid unchanged.

… log line

The curvature and sign-convention comments explained their derivations at length where the
claim itself is the point; the non-positive b_t0 guard and the sensitivity table's basis-length
guard had no test; one log line ran past the 180-column margin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
logan-nc and others added 5 commits September 25, 2026 07:37
… log line

The curvature and sign-convention comments explained their derivations at length where the
claim itself is the point; the non-positive b_t0 guard and the sensitivity table's basis-length
guard had no test; one log line ran past the 180-column margin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
…d distribution

The fixture assertion that the risk was neither zero nor saturated stopped holding once the
root-area weight fix landed from develop: two 2 kA hoops on a toy Solovev equilibrium drive an
overlap around 1e-5, and the ITPA threshold at that density is 2e-3. No plausible density closes a
gap of seventy, so zero risk is the correct answer for this fixture, not a regression.

Assert that instead, and pin the convolution where it can be checked by arithmetic: a flat |δ|
distribution against thresholds spread across it gives half, thresholds entirely above give zero,
entirely below give everything, and lowering them can only raise the risk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
… scaling and name the scenario arguments

runtests_risk.jl built a SensitivityTable with nine positional arguments after the struct gained
delta_per_mm_rim, so the tolerance-scan testset errored at construction. Restore the missing column.

ScenarioParameters took five positive scalars positionally. Transposing any two passes the
all-positive guard and changes the answer quietly: exchanging B_T0 and R_0 alone moves the
threshold by (R/B)^(alpha_B - alpha_R). Take them by keyword.

The ITPA locking thresholds are fitted per toroidal mode number, but a dominant-mode decomposition
spans every n a run carried. Choosing the lowest n and saying nothing applies an n = 1 threshold to
a mode that may be partly n = 2, so ask for a single n and explain what to do instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
…ds-risk

Carries the develop merge down the chain after the sensitivities branch landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
logan-nc and others added 8 commits September 25, 2026 10:42
…ysis-plots

Carries the develop merge down the chain after the tolerance TOML branch
landed. The conflicts were all this branch's own Phasing and Analysis
additions against a parent that does not have them; kept here. The one
both-sides hunk was a line wrap in the example, taken from upstream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
From the Monte Carlo overlap distribution to an engineering answer. Risk.jl carries the nine
published ITPA penetration-threshold fits (ThresholdScaling, threshold_scaling(; n, dataset,
fit); Logan et al. PPCF 62 084001 and NF 60 086010, 2020), the operating point
(ScenarioParameters, density from [ErrorFields.scenario], the rest from the equilibrium),
threshold sampling within the fit errors, and locking_risk: P(lock|δ) is the sampled
threshold's cumulative distribution on the Monte Carlo's own bin edges and the machine's risk
100 ∫ pdf(δ) P(lock|δ) dδ per batch, intrinsic and corrected, with the as-designed and
sharp-threshold risks. tolerance_scan repeats the Monte Carlo over tolerance multipliers and
allowable_tolerance inverts the scan in log-log space for a target risk. The run writes
ErrorFields/Risk/ and ErrorFields/Risk/ToleranceScan/ ([ErrorFields.Risk] settings); the same
functions re-run any window, fit, subset or target from gpec.h5. The example gains the
scenario, a smaller intrinsic C-coil field and tolerances that put the risk curve through the
1 % target.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
… resonant-coupling window

Follows the PerturbedEquilibrium change that makes psi_N <= 0.9 (CORE_PSI_HIGH) the default
window of the dominant-mode SVD: the gpec.h5 entry points of this stage now default to the same
window instead of the full one, so in-memory and post-hoc analyses agree by default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
… log line

The curvature and sign-convention comments explained their derivations at length where the
claim itself is the point; the non-positive b_t0 guard and the sensitivity table's basis-length
guard had no test; one log line ran past the 180-column margin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
…d distribution

The fixture assertion that the risk was neither zero nor saturated stopped holding once the
root-area weight fix landed from develop: two 2 kA hoops on a toy Solovev equilibrium drive an
overlap around 1e-5, and the ITPA threshold at that density is 2e-3. No plausible density closes a
gap of seventy, so zero risk is the correct answer for this fixture, not a regression.

Assert that instead, and pin the convolution where it can be checked by arithmetic: a flat |δ|
distribution against thresholds spread across it gives half, thresholds entirely above give zero,
entirely below give everything, and lowering them can only raise the risk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
… scaling and name the scenario arguments

runtests_risk.jl built a SensitivityTable with nine positional arguments after the struct gained
delta_per_mm_rim, so the tolerance-scan testset errored at construction. Restore the missing column.

ScenarioParameters took five positive scalars positionally. Transposing any two passes the
all-positive guard and changes the answer quietly: exchanging B_T0 and R_0 alone moves the
threshold by (R/B)^(alpha_B - alpha_R). Take them by keyword.

The ITPA locking thresholds are fitted per toroidal mode number, but a dominant-mode decomposition
spans every n a run carried. Choosing the lowest n and saying nothing applies an n = 1 threshold to
a mode that may be partly n = 2, so ask for a single n and explain what to do instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
…ds-risk

Carries the develop merge down the chain after the sensitivities branch landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
ebursch and others added 3 commits September 25, 2026 11:37
…shold scalings with their plasma-current term

Adds Bursch et al. 2026 (arXiv:2604.27317) Eq. 7 (OLS) and Eq. 8 (KDE-weighted
WLS) as the "O,L 2026" n = 1 fits, per the review request on this PR. Both fits
add an |I_p| term, so:

- ThresholdScaling gains alpha_ip; the 2020 fits keep their positional
  constructor and carry (0, 0).
- ScenarioParameters gains i_p (MA). It defaults from the equilibrium's crnt, or
  from Equilibrium/I_p for the file entry points, and the keyword constructor
  leaves it NaN. A fit with a current term refuses a scenario without one.
- threshold_samples draws the current exponent only for fits that have one, so
  every existing fit's sample stream and pinned risk values are unchanged.

Papers are cited in the code annotations, docs/src/error_fields.md and a new
ErrorFields section of docs/src/citations.md, with no PDFs added to resources.
The example and the docs list the new dataset key. The default fit is still
"O,L" WLS.

Tests: runtests_risk.jl and runtests_error_fields.jl pass (161/161).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ublication

Bursch et al., Plasma Phys. Control. Fusion (2026), doi:10.1088/1361-6587/aea7d6, replaces the arXiv:2604.27317 preprint in the code annotations, the test comment, docs/src/error_fields.md and docs/src/citations.md. The volume and article number are not assigned yet (Crossref has none), so the DOI identifies it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ThresholdScaling gains a year field, and every fit's key and label now name it:
"n=<n> <year> <dataset> <fit>", e.g. "n=1 2020 O,L WLS" and "n=1 2026 O,L OLS".
The 2026 fits are the "O,L" dataset with year = 2026 instead of a separate
"O,L 2026" dataset name. The plain dataset string could not say which
publication, and so which database, a fit came from.

- threshold_scaling and RiskControl take year (default 2020, so the default fit
  and every existing TOML keep their meaning); [ErrorFields.Risk] accepts
  year = 2026.
- scaling_label(sc) gives the key. The run log and the missing-current error
  print it.
- The module docstring and table group the fits by year and cite each paper.
  The docs table, TOML reference and example list year.
- The positional ThresholdScaling constructor takes year after n (the "!").
  Later stack branches use only the keyword API.

Tests: runtests_risk.jl and runtests_error_fields.jl pass (165/165). The
DIII-D-like error-field example runs and logs "Locking risk (n=1 2020 O,L WLS)".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ebursch ebursch 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.

Approve

phasing_map checked the mode index and the coil names but nothing about
the basis the decomposition was taken on, so a DominantCoupling from
another equilibrium projected and returned a plausible map. The check is
a no-op for the bare-matrix construction, which carries no basis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
@logan-nc

Copy link
Copy Markdown
Collaborator Author

@ebursch two commits landed after your approval, both mechanical — flagging rather than assuming.

b819450e4 merges the current develop after the tolerance TOML branch landed. Its conflicts were
all this branch's own Phasing and Analysis additions against a parent that does not have them,
so they were kept; the single both-sides hunk was a line wrap in analyze_example.jl, taken from
upstream. Verified afterwards that Phasing.jl, runtests_phasing.jl, the module include and the
exports all survived, and that the parent branch's own files were untouched.

81b2decc4 wires check_mode_basis into phasing_map. That guard shipped with the mode basis
itself but nothing called it. phasing_map validated the mode index and the coil names but nothing
about the basis the decomposition was taken on, so a DominantCoupling from another equilibrium (or
the same one at a different mlow) had the right shape, projected, and returned a plausible map.
The check is a no-op for the bare dominant_coupling(C, rational_psi) construction, which carries
no basis, so the existing fixtures are unaffected.

runtests_phasing.jl is 22/22 on the result. The same wiring for sensitivity_table is #470,
against develop, since Sensitivity.jl has already merged.

…ysis-plots

Picks up the year-keyed threshold scalings after the risk branch was rebuilt on the
current develop. The rebuilt branch shares no ancestor with this one for the
ErrorFields files, so the merge was taken against the pre-rebuild tip, whose content
the rebuilt base reproduces exactly; only the new scaling work comes across.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzKDmtKYDotJWrcxWf1oJu
@logan-nc

Copy link
Copy Markdown
Collaborator Author

@ebursch one more post-approval commit, f97a9649a, which is a merge of your rebuilt
feature/errorfields-risk (the year-keyed scalings) — no new work on this branch.

It needed the same care as the earlier develop merge: the rebuilt risk branch shares no
ancestor with this one for the ErrorFields files, so git proposed nine conflicts including
add/add on Risk.jl and runtests_risk.jl. Merging naively would have clobbered one side.
Instead the merge was taken against the pre-rebuild tip this branch already contained
(cace1c712), after confirming the rebuilt base reproduces that tip's content exactly (empty
diff over src, test, docs, examples); against that base the merge is conflict-free and only
your three scaling commits come across. Verified afterwards that no file differing from risk
is one this branch never touched.

Base automatically changed from feature/errorfields-risk to develop September 28, 2026 12:26
Picks up #452 (locking-risk model), which landed on develop as a squash, so git
saw this branch's copy of the risk commits and the squash as overlapping edits.
All five conflicts were this branch's own additions (Phasing include, export and
test, the plots and phasing doc section, the example's plot block and the plot
tests) against an empty develop side at the same spot; each keeps this branch's
lines. After the merge the diff against develop is this PR's own 10 files, all
additions, and the risk files are identical to develop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ebursch
ebursch merged commit db5415a into develop Sep 28, 2026
7 of 8 checks passed
@ebursch
ebursch deleted the feature/errorfields-analysis-plots branch September 28, 2026 12:50
ebursch added a commit that referenced this pull request Sep 28, 2026
…orfields-diagnostic-plots

Picks up the develop merge carried up the stack (#452 and #453 squashed onto
develop). No file changes, and the diff against the base is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
krystophny pushed a commit to redmod-team/GPEC that referenced this pull request Oct 1, 2026
Brings in OpenFUSIONToolkit#452 and OpenFUSIONToolkit#453, which landed on develop as squashes, so this branch's
history carries them the way develop does. The merge is clean and the diff
against develop is unchanged: the same 13 files, line for line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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