Repository navigation
Analysis/ErrorFields - FEATURE - Add error-field assessment plots and coil-array phasing maps - #453
Conversation
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
…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
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
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). |
… 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
|
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 ( |
… 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
… 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
…rrorfields-risk" This reverts commit 1e6ae4e.
…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
…rrorfields-risk" This reverts commit 1e6ae4e.
…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>
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
|
@ebursch two commits landed after your approval, both mechanical — flagging rather than assuming.
|
…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
|
@ebursch one more post-approval commit, It needed the same care as the earlier |
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>
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>
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.ErrorFieldsfunctions that readgpec.h5and takelabel => h5pathlists 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 field100·|Vᴴb̃|/‖b̃‖;extreme_phasingreads 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 takesVector{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.analyze_example.jlnow 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|atarg δ_1 − arg δ_2, minimum||δ_1| − |δ_2||; resonant fraction equals100|Vᴴb̃|/‖b̃‖and never exceeds 100; three-array grid uses cumulative phases; guards.gpec.h5(returns aPlots.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_mapnormalizes 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 hasnw = −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
Analysis.ErrorFieldsplots the error-field assessment fromgpec.h5— coil sensitivities, tolerance Monte Carlo distributions, locking risk vs tolerance, threshold scaling, the dominant mode, and coil-array phasing maps — with lists oflabel => filepairs to overplot design revisions;ErrorFields.phasing_mapcomputes the phasing map itself.Regression report
Harness, re-run at this branch head
d83c9e8bfcompared againstdevelop(9578c9b86), casediiid_n1:Every tracked quantity is bit-identical; only wall-clock
Runtimediffers, which theharness does not track. The branch carries a merge of the current
develop.