Process reference: perturbation
Generated from the docstrings of vaft.process.perturbation:
27 public functions. Each entry is what
vaft.process.describe("perturbation.<name>") prints.
All process categories
Overview
A 3-D perturbation is felt at the surfaces where it resonates, and a code
like GPEC reports one row per rational surface: the resonant flux, the
shielding current, the island it would open, the Chirikov parameter. Reading
those rows is vaft.code.gpec’s job and it is done; this module is what
turns a table of them into the few numbers a study actually compares.
Nothing here is specific to a machine or to a code. The input is a radial
coordinate and a column of values; resonant_metrics takes the mapping
vaft.code.gpec.GpecProfileOutput.resonant_table returns because that
is the shape the data already has, not because the module knows about GPEC.
Two things this module is careful about, because the code it replaces was not.
Where a region starts and ends is not a constant. The legacy metrics
hard-code the core at psi_n <= 0.8 and the edge at 0.8 to 0.95, and seven
different window sets are scattered across the notebooks that used them
(convention C-16). Decision D-05 settles it: the boundary comes from the
pedestal, through vaft.process.profile.pedestal_top, and every
reduction records which window it used and where that window came from. The
legacy numbers survive only as LEGACY_WINDOWS, so an old result can
still be reproduced deliberately.
An island width is not a threshold. GPEC reports the island each
resonant surface would open, and whether those islands overlap decides
whether the region goes stochastic. The comparison it is made against –
the distance to the nearest neighbouring surface – is pure geometry, and
GPEC leaves its own w_isl_v_crit at zero on an ideal run, so it has to be
computed. It is, here, and it reproduces GPEC’s own Chirikov parameter
exactly: w_isl / d_nn equals the K_isl in the file to the last bit on
every surface of every run checked, which is what makes the derived
quantities trustworthy rather than merely plausible.
A coupling matrix and a field have to be on one basis. A GPEC run
reports five couplings – flux, current, island width, penetrated flux and
Delta – and writes them as C_f_x_out and its four siblings on the
output harmonic basis m_out, which in the DIII-D example has 129
entries. The external field Phi_xe is on m, which has 34. The one
array that pairs with it is C_xe, the flux coupling on the field’s own
basis – its singular values agree with C_f_xe_out_singval to 1.4e-5.
edge_overlap_metric refuses a mismatch rather than broadcasting one,
and nothing here maps a family name to a variable, because four of the five
have no counterpart on the basis the field lives on.
Several mode numbers meet only at a rational surface. A harmonic
(m, n) varies as exp(i (m theta - n zeta)), so on the surface
q = m / n its phase is n (q theta - zeta) – n times one angle,
whatever n is. Every mode resonating at one q is therefore a
harmonic of a single Fourier series, and composite_drive_at_q sums
them as one. Away from such a surface they are unlike things and nothing
here adds them; Chirikov parameters in particular are not additive across
n at all, which the legacy acknowledged by naming the column that added
them sum_K_isl_wrong.
A reduction is not a verdict. vaft.process computes; it does not
decide. The thresholds the legacy compared against – a Chirikov parameter of
1, a penetration ratio of 1, an edge overlap of 7.4e-4 – are arguments here
and defaults nowhere, and nothing in this module returns “stable”. A study
that wants a verdict registers one in vaft.validation.
Units are SI throughout: the resonant flux is in tesla, as GPEC writes it, and is not multiplied by 1e4. The legacy reported gauss, silently, inside the reduction; a factor of ten thousand belongs to a display layer that says which unit it is showing.
Functions
align_surfaces_by_q | Match rational surfaces across toroidal mode numbers by their q.
Convention |
amplification_ratio | How much larger one run’s resonant response is than another’s. |
chirikov | The island-overlap parameter, by either of the two definitions in use. Convention |
composite_drive_at_q | Sum one resonant column over every mode that resonates at a given q.
Convention |
critical_island_width | The island width at which a surface’s islands reach its neighbour’s. Convention |
dominant_mode | The leading direction of a coupling matrix, weighted by surface area. Convention |
edge_overlap_metric | How much of an external field lands on the dominant coupling mode. Convention |
energy_norm_matrix | The inverse square root of an energy matrix, from its eigendecomposition. Convention |
finite_width_delta | The jump in a field’s radial derivative across a finite-width layer. Convention |
group_coincident_islands | Collapse islands that sit on the same surface into one. Convention |
helical_phase_sweep | The composite drive at one surface, as a function of the helical angle. Convention |
island_overlap_width | How far in from the boundary the islands overlap without a break. Convention |
island_pairs | Every adjacent pair of islands, with the gap between their separatrices. Convention |
jump_width | The resistive-layer width a Lundquist number implies. Convention |
lundquist_number | The Lundquist number, the resistive over the Alfven time. |
nearest_surface_spacing | Distance from each rational surface to its nearest neighbour. |
penetration_ratio | Island width as a fraction of the width at which islands would overlap. Convention |
q_composite_table | One row per rational surface, summed over the modes that reach it. Convention |
reduce_delta_e | Combine a per-mode edge-overlap metric into one number. Convention |
reduce_resonant | Reduce one resonant column over one radial window. Convention |
resonant_delta | The resonance parameter from the jump in a stored spectral field. Convention |
resonant_geometric_factor | Measure the surface factor that turns a resonance parameter into a flux. Convention |
resonant_metrics | Reduce every named column of a resonant table over every window. Convention |
resonant_windows | The core, edge and total radial windows a reduction is taken over. Convention |
rms_resonant_field | Root-mean-square resonant field over a radial window. Convention |
shielded_field | The field a finite-width layer’s shielding current produces. Convention |
toroidal_phase_audit | Test a stored toroidal harmonic against the same field resampled in real space. Convention |
No function matches.
align_surfaces_by_q
Machine-independentConvention-sensitive
source
align_surfaces_by_q(tables, *, integrality_tolerance=1e-06)
Match rational surfaces across toroidal mode numbers by their q.
| Name | Type | Unit | Description |
|---|---|---|---|
tables | mapping of int to mapping | n/a | One resonant table per toroidal mode number, each carrying at least a |
integrality_tolerance | float, optional | - | How far |
| Returns | |||
| — | tuple of AlignedSurface | n/a | Every |
Convention
Surfaces are matched on the exact rational m / n, not on a
tolerance around a float. On the DIII-D reference q_rational is
exactly m / n in double precision for every surface of every mode,
and IEEE division is correctly rounded, so two fractions that reduce to
the same rational give the same double – 4/3 and 8/6 compare
equal. The positions do not: at q = 2 the three runs put
psi_n_rational at 0.59364383816324717, 0.59364383816324717 and
0.59364383816403732, first differing in the twelfth significant digit.
q is the key because it is exact; psi is the root finder’s answer
and is not.
Raises
ValueErrorA mode number is not a positive integer, a table has noq_rational, aqis not finite, orn * qis not an integer for a surface the run itself reports.
Processing steps · Defaults · Applicability · Limitations · Provenance
Processing steps
- Turn each reported
qinto the fractionm / nand refuse a surface whosen * qis not integral – a run reporting one is reporting something that is not a rational surface. - Collect the union of those fractions across modes, ascending.
- For every mode at every
q, recordresonantwhen the run reports a surface,non_resonantwhenn * qis not an integer, andabsentwhen it is but the run reports nothing.
Defaults
integrality_tolerance = 1e-6 is a numerical convenience: loose
enough for a q that has been through a file format. Rounding to the
nearest integer is what decides which rational a value belongs to, so
two rationals cannot be confused at any mode number whatever the
tolerance is; it only decides how far from any of them a value may sit
before it is refused, and being absolute on n * q it tightens as
1e-6 / n on q itself. The match is exact – this is not a
matching tolerance.
Applicability
Machine-independent. Nothing here reads a machine, a code or a file; the
input is a column of q per mode number.
Limitations
absent and resonant are statements about one run’s radial extent,
not about the plasma: a surface the run truncated away is real. Nothing
here reads qlim, so absent cannot distinguish a truncated run
from a failed root find.
Provenance
gpec_multimode_metrics.align_surfaces_by_q, which matched the single nearest surface withinq_tol = 0.05of four hard-coded targets(2, 3, 4, 5). Three departures. The window is absolute, so it admits more than one rational once the spacing1 / nfalls below it – at n = 20 exactly, where it silently keeps the nearest and drops the rest. A mode out of tolerance was dropped from the group with no record, which is indistinguishable here from a mode that cannot resonate at thatqat all. And its tolerance test wasabs(q - target) > q_tol, which isFalsefor anan, so ananqwas returned as a match.
amplification_ratio
Machine-independent
source
amplification_ratio(psi_norm, values, reference_psi_norm, reference_values, *, window, statistic='rms')
Show source vaft/process/perturbation.py, lines 534–596
def amplification_ratio(
psi_norm,
values,
reference_psi_norm,
reference_values,
*,
window: ResonantWindow,
statistic: str = "rms",
) -> float:
"""How much larger one run's resonant response is than another's.
Parameters
----------
psi_norm : array_like
Rational-surface coordinate of the case [-].
values : array_like
The case's resonant column [any].
reference_psi_norm : array_like
Rational-surface coordinate of the reference, usually the vacuum run
[-].
reference_values : array_like
The reference's resonant column, in the same unit as ``values`` [any].
window : ResonantWindow
The interval both are reduced over [-].
statistic : str, optional
One of :data:`RESONANT_STATISTICS` [n/a].
Returns
-------
float
Case over reference; ``nan`` when either window is empty or the
reference reduces to zero [-].
Processing steps
----------------
1. Reduce each run over ``window`` independently, so the two need not
share a rational-surface set.
2. Divide.
Limitations
-----------
Both runs are reduced on their *own* surfaces. Two equilibria resonate at
different places, and interpolating one onto the other's surfaces would
invent a resonance where there is none.
Applicability
-------------
Machine-independent.
Provenance
----------
.. [legacy] ``gpec_analysis.calculate_amplification_ratio``, which fell
back to the vacuum column silently when the plasma one was absent --
returning a ratio of one and calling it no amplification. Not carried
over: an absent column raises here.
"""
case = reduce_resonant(psi_norm, values, window=window, statistic=statistic)
reference = reduce_resonant(
reference_psi_norm, reference_values, window=window, statistic=statistic
)
if not np.isfinite(case) or not np.isfinite(reference) or reference == 0.0:
return float("nan")
return float(case / reference)How much larger one run’s resonant response is than another’s.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Rational-surface coordinate of the case. |
values | array_like | any | The case’s resonant column. |
reference_psi_norm | array_like | - | Rational-surface coordinate of the reference, usually the vacuum run |
reference_values | array_like | any | The reference’s resonant column, in the same unit as |
window | ResonantWindow | - | The interval both are reduced over. |
statistic | str, optional | n/a | One of |
| Returns | |||
| — | float | - | Case over reference; |
Processing steps · Applicability · Limitations · Provenance
Processing steps
- Reduce each run over
windowindependently, so the two need not share a rational-surface set. - Divide.
Applicability
Machine-independent.
Limitations
Both runs are reduced on their own surfaces. Two equilibria resonate at different places, and interpolating one onto the other’s surfaces would invent a resonance where there is none.
Provenance
gpec_analysis.calculate_amplification_ratio, which fell back to the vacuum column silently when the plasma one was absent – returning a ratio of one and calling it no amplification. Not carried over: an absent column raises here.
chirikov
Machine-independentConvention-sensitive
source
chirikov(psi_norm, width, *, definition='surface')
Show source vaft/process/perturbation.py, lines 834–888
def chirikov(psi_norm, width, *, definition: str = "surface") -> np.ndarray:
"""The island-overlap parameter, by either of the two definitions in use.
Parameters
----------
psi_norm : array_like
Rational-surface positions [-].
width : array_like
Full island width per surface [-].
definition : str, optional
``"surface"`` centres it on each surface, ``width / d_nn``, and
returns one value per surface. ``"pair"`` takes each adjacent pair,
``(w_inner + w_outer) / 2 / spacing``, and returns one value per
*pair* -- one fewer than the surfaces [n/a].
Returns
-------
ndarray
For ``"surface"``, one value per surface in the caller's order. For
``"pair"``, one per adjacent pair **sorted outward** -- which two
islands are neighbours is a property of the sorted sequence, and
:class:`IslandPairs` carries the indices that map each pair back [-].
Raises
------
ValueError
``definition`` is neither, or two surfaces share a position.
Defaults
--------
``definition="surface"`` is a legacy compatibility value: it is the form
GPEC itself reports as ``K_isl``, so it is what a caller comparing with a
file will want. It also returns a different array length from ``"pair"``.
Convention
----------
The two definitions answer different questions and disagree by design:
the surface form asks whether one island reaches its nearest neighbour,
the pair form whether two particular islands reach each other. GPEC's own
``K_isl`` is the surface form.
Applicability
-------------
Machine-independent.
Provenance
----------
.. [legacy] ``gpec_multimode_metrics.compute_surface_chirikov`` and
``compute_pair_chirikov``.
"""
if definition not in ("surface", "pair"):
raise ValueError(f"definition must be 'surface' or 'pair', not {definition!r}")
if definition == "surface":
return penetration_ratio(psi_norm, width)
return island_pairs(psi_norm, width).chirikovThe island-overlap parameter, by either of the two definitions in use.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Rational-surface positions. |
width | array_like | - | Full island width per surface. |
definition | str, optional | n/a |
|
| Returns | |||
| — | ndarray | - | For |
Convention
The two definitions answer different questions and disagree by design:
the surface form asks whether one island reaches its nearest neighbour,
the pair form whether two particular islands reach each other. GPEC’s own
K_isl is the surface form.
Raises
ValueErrordefinitionis neither, or two surfaces share a position.
Defaults · Applicability · Provenance
Defaults
definition="surface" is a legacy compatibility value: it is the form
GPEC itself reports as K_isl, so it is what a caller comparing with a
file will want. It also returns a different array length from "pair".
Applicability
Machine-independent.
Provenance
gpec_multimode_metrics.compute_surface_chirikovandcompute_pair_chirikov.
composite_drive_at_q
Machine-independentConvention-sensitive
source
composite_drive_at_q(tables, q, column, *, weights=None, integrality_tolerance=1e-06, angle_points=None)
Sum one resonant column over every mode that resonates at a given q.
| Name | Type | Unit | Description |
|---|---|---|---|
tables | mapping of int to mapping | n/a | One resonant table per toroidal mode number, as |
q | float | - | The rational surface to sum at. |
column | str | n/a | The column to sum. Complex is the point – a magnitude carries no phase to interfere with. |
weights | mapping of int to complex, optional | n/a | Per-mode complex weight, applied before the sum. A unit-modulus weight rotates a mode’s phase; a real one rescales its amplitude |
integrality_tolerance | float, optional | - | Passed to |
angle_points | int, optional | - | How finely the helical angle is sampled before the peak is refined. Left unset it is sized from the highest resonating mode number; given, it must be at least |
| Returns | |||
| — | CompositeDrive | rad |
|
Convention
The modes that share a rational surface also share a helical angle,
and that is what makes this sum well defined. A harmonic (m, n)
varies as exp(i (m theta - n zeta)), and on the surface q = m / n
that phase is n (q theta - zeta) – n times the single angle
u = q theta - zeta, whatever n is. So every mode resonating at
one q is a harmonic of one Fourier series in u, the n-th, and
summing them is ordinary Fourier synthesis rather than an addition of
unlike things. Away from a rational surface it would be neither.
The origin of u is a gauge: shifting it by d takes z_n to
z_n exp(i n d), so the amplitude at a particular angle means
nothing unless the caller fixed that origin – through the coil geometry,
say. peak is invariant under the shift and is the number to quote;
peak_angle is reported relative to the input’s own origin and moves
with it. quadrature and linear_bound are invariant too.
The decomposition assumed is exp(i (m theta - n phi)), which is
GPEC’s, and its spectral Phi_res is written without the
-helicity flip its real-space outputs carry, so no helicity factor
enters the sum. What helicity does reach is peak_angle: GPEC’s
phi runs counter-clockwise for a left-handed plasma and clockwise
for a right-handed one, so on a helicity = +1 run the same spectral
convention makes u = q theta + phi in machine coordinates and the
reported angle runs the other way. peak, quadrature and
linear_bound are untouched – conjugating every z_n mirrors
u -> -u, and a mirrored curve has the same height.
The amplitude summed is Re(z exp(i n u)) and not twice it. A real
field reconstructed from positive n alone carries the conjugate
harmonics too, so a comparison against a real-space output will differ
by a factor of two; this follows the convention the legacy’s own
artefact records rather than that one.
Raises
ValueErrorNo mode resonates atq, the column is missing from a resonating mode’s table, a contribution is not finite,angle_pointsis less than 2, or a weight names a mode that is not intables.
Processing steps · Defaults · Applicability · Limitations · Provenance
Processing steps
- Align the surfaces and keep the modes that resonate at
q. - Weight each mode’s contribution.
- Evaluate
sum_n Re(z_n exp(i n u))overuin[0, 2 pi), take the largest, and refine it by golden-section search. - Report the quadrature sum and the aligned-phase bound alongside.
Defaults
angle_points unset is a numerical convenience: 16 * n_max + 1
points, sixteen per period of the top harmonic, but never fewer than
- The size has to follow
n_maxbecause the sum is a trigonometric polynomial of that degree and has up to2 n_maxmaxima. A fixed 721-point grid was measured against a two-million-point scan on 400 random cases per row: it already missed the peak in 2 of 400 atn_max = 20– worst deficit 1.5e-3, on the two-mode case (14, 19) – and in 281 of 400 atn_max = 400, worst deficit 0.20. Sized fromn_maxand refined from every sampled local maximum it misses none.
Applicability
Machine-independent. The helical angle exists wherever q = m / n
does, which is every tokamak.
Limitations
The peak is over the helical angle at one surface; it is not an island
width, and the modes do not combine into one. Chirikov parameters in
particular are not additive across n – the legacy summed them
into a column it named sum_K_isl_wrong, and nothing here reproduces
that sum under a better name.
Provenance
gpec_multimode_metrics.composite_drive_at_q, which returnedabs(sum(z))at the caller’s fixed phases andsum(abs(z))beside it, with a third key duplicating the second. The first is this function’s drive at whatever angle the weights happen to encode, which is gauge-dependent and was reported as though it were not; the second islinear_bound. The angle the legacy swept was never identified as the helical one.- The legacy’s own multi-n artefact records the phase convention it combined under:
sample/gpec_optimization_examples/q2p0_harmonic/gpec_multimode_summary_n12.jsoncarriesphase_convention = "real(B_n * exp(i * n * phi))".
critical_island_width
Machine-independentConvention-sensitive
source
critical_island_width(psi_norm)
Show source vaft/process/perturbation.py, lines 752–792
def critical_island_width(psi_norm) -> np.ndarray:
"""The island width at which a surface's islands reach its neighbour's.
Purely geometric: an island overlaps its neighbour when it is as wide as
the distance to that neighbour, so the critical width *is* the
nearest-neighbour spacing.
Parameters
----------
psi_norm : array_like
Rational-surface positions [-].
Returns
-------
ndarray
Critical width per surface, in the same units as ``psi_norm`` [-].
Convention
----------
Widths are full widths, in normalized poloidal flux, matching what GPEC
writes as ``w_isl``. The identity this rests on -- ``w_isl / w_crit``
equalling GPEC's own ``K_isl`` -- holds because GPEC forms its Chirikov
parameter from a half width over a half distance, and the halves cancel.
Limitations
-----------
Undefined for a single surface, which has no neighbour: ``nan``.
Applicability
-------------
Machine-independent.
Provenance
----------
.. [gpec] Reproduces GPEC's own ``K_isl`` exactly when divided into
``w_isl``: zero relative difference on every surface of the DIII-D
n=1, n=2 and n=3 ideal runs.
.. [ideal] GPEC leaves its own ``w_isl_v_crit`` at zero on an ideal run,
which is why this is computed rather than read.
"""
return nearest_surface_spacing(psi_norm)The island width at which a surface’s islands reach its neighbour’s.
Purely geometric: an island overlaps its neighbour when it is as wide as the distance to that neighbour, so the critical width is the nearest-neighbour spacing.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Rational-surface positions. |
| Returns | |||
| — | ndarray | - | Critical width per surface, in the same units as |
Convention
Widths are full widths, in normalized poloidal flux, matching what GPEC
writes as w_isl. The identity this rests on – w_isl / w_crit
equalling GPEC’s own K_isl – holds because GPEC forms its Chirikov
parameter from a half width over a half distance, and the halves cancel.
Applicability · Limitations · Provenance
Applicability
Machine-independent.
Limitations
Undefined for a single surface, which has no neighbour: nan.
Provenance
- Reproduces GPEC’s own
K_islexactly when divided intow_isl: zero relative difference on every surface of the DIII-D n=1, n=2 and n=3 ideal runs. - GPEC leaves its own
w_isl_v_critat zero on an ideal run, which is why this is computed rather than read.
dominant_mode
Machine-independentConvention-sensitive
source
dominant_mode(coupling, *, response_area=None, control_area=None)
Show source vaft/process/perturbation.py, lines 2878–2953
def dominant_mode(coupling, *, response_area=None, control_area=None) -> DominantMode:
"""The leading direction of a coupling matrix, weighted by surface area.
Parameters
----------
coupling : array_like
Response-by-control coupling, complex [n/a].
response_area : array_like, optional
Area per response row. Unweighted when omitted [m^2].
control_area : array_like, optional
Area per control column. Unweighted when omitted [m^2].
Returns
-------
DominantMode
The leading singular value, and the control and response vectors in
the unweighted basis [n/a].
Raises
------
ValueError
The matrix is not 2-D, an area does not match its dimension, or an
area is not finite and positive.
Convention
----------
Rows are weighted by ``sqrt(area)`` and columns divided by it, so the
decomposition is of ``W_r M W_c**-1`` and the vectors are mapped back
through the weights. The returned control vector is the right singular
vector itself -- ``vh[0].conj()`` -- and a projection onto it is
``np.vdot(control_vector, x)``, which conjugates it; that is not the
``vh.conj() @ x`` defect :func:`edge_overlap_metric` once had.
Applicability
-------------
Machine-independent.
Processing steps
----------------
1. Check the areas **before** taking their square root.
2. Weight rows and columns, decompose, map the leading pair back.
Provenance
----------
.. [legacy] ``resistive_gpec_metrics.py::dominant_mode``. Its guard,
``np.any(np.sqrt(area) <= 0)``, is defeated by a negative area:
``sqrt`` of one is ``nan``, ``nan <= 0`` is ``False``, and the error
surfaced later as ``SVD did not converge`` rather than naming the area.
"""
matrix = np.asarray(coupling, dtype=complex)
if matrix.ndim != 2:
raise ValueError(f"the coupling must be 2-D, not {matrix.ndim}-D")
if not np.all(np.isfinite(matrix)):
raise ValueError("the coupling holds a non-finite value")
def weights(area, size, label):
if area is None:
return np.ones(size)
values = np.atleast_1d(np.asarray(area, dtype=float))
if values.shape != (size,):
raise ValueError(f"{values.size} {label} areas against {size} {label}s")
# Checked on the area, not on its root: sqrt of a negative is nan,
# and nan <= 0 is False.
if not np.all(np.isfinite(values)) or np.any(values <= 0.0):
raise ValueError(f"every {label} area must be finite and positive")
return np.sqrt(values)
rows = weights(response_area, matrix.shape[0], "response")
columns = weights(control_area, matrix.shape[1], "control")
weighted = rows[:, None] * matrix / columns[None, :]
left, singular, right = np.linalg.svd(weighted, full_matrices=False)
return DominantMode(
singular_value=float(singular[0]),
control_vector=right[0].conj() / columns,
response_vector=left[:, 0] / rows,
)The leading direction of a coupling matrix, weighted by surface area.
| Name | Type | Unit | Description |
|---|---|---|---|
coupling | array_like | n/a | Response-by-control coupling, complex. |
response_area | array_like, optional | m^2 | Area per response row. Unweighted when omitted. |
control_area | array_like, optional | m^2 | Area per control column. Unweighted when omitted. |
| Returns | |||
| — | DominantMode | n/a | The leading singular value, and the control and response vectors in the unweighted basis. |
Convention
Rows are weighted by sqrt(area) and columns divided by it, so the
decomposition is of W_r M W_c**-1 and the vectors are mapped back
through the weights. The returned control vector is the right singular
vector itself – vh[0].conj() – and a projection onto it is
np.vdot(control_vector, x), which conjugates it; that is not the
vh.conj() @ x defect edge_overlap_metric once had.
Raises
ValueErrorThe matrix is not 2-D, an area does not match its dimension, or an area is not finite and positive.
Processing steps · Applicability · Provenance
Processing steps
- Check the areas before taking their square root.
- Weight rows and columns, decompose, map the leading pair back.
Applicability
Machine-independent.
Provenance
resistive_gpec_metrics.py::dominant_mode. Its guard,np.any(np.sqrt(area) <= 0), is defeated by a negative area:sqrtof one isnan,nan <= 0isFalse, and the error surfaced later asSVD did not convergerather than naming the area.
edge_overlap_metric
Machine-independentConvention-sensitive
source
edge_overlap_metric(coupling, external_field, *, b_t0, norm=None)
How much of an external field lands on the dominant coupling mode.
The coupling matrix is decomposed, the field is projected onto its singular modes, and the dominant mode’s share is reported per unit field.
| Name | Type | Unit | Description |
|---|---|---|---|
coupling | array_like | - | Coupling matrix, modes by harmonics, complex. |
external_field | array_like | T | The external field on the same harmonic basis, complex. |
b_t0 | float | T | Vacuum toroidal field on axis, which the projection is divided by. Required: the code this replaces defaulted it to one, which silently reports a field in tesla as a dimensionless metric. A GPEC run carries it as a global attribute. |
norm | array_like, optional | - | An energy norm to apply to |
| Returns | |||
| — | EdgeOverlap | - | The metric, the per-mode projection and the singular values. |
Convention
The projection is reported as a magnitude. A right singular vector is fixed only up to a phase, so the complex overlap is a gauge – GPEC’s own vectors and a fresh decomposition of the same matrix differ by exactly that phase – and a caller handed the complex number would be reading the decomposition rather than the plasma. The magnitude is not affected. The dominant mode is the one carrying the most field, which need not be the one with the largest singular value.
Raises
ValueErrorThe shapes disagree, the coupling matrix or the field holds a non-finite entry, the coupling matrix is empty or identically zero, orb_t0is not a positive finite real number.
Processing steps · Applicability · Limitations · Provenance
Processing steps
- Apply
normto the coupling matrix when one is given. - Take its singular value decomposition.
- Project the field onto the conjugated right singular vectors.
- Divide the largest projection by
b_t0.
Applicability
Machine-independent.
Limitations
A coupling matrix that a run never filled decomposes to zero singular values and a projection that is just the field’s own components, which would report a confident metric for a run that computed nothing. An all-zero matrix is refused for that reason; a merely tiny one is not, and a run whose external field is at the level of floating-point residue – the n=2 ideal example has a field norm of 1e-19 – gives a ratio of two residues.
Provenance
- Conventions register C-25: the dominant singular vector needs explicit gauge fixing or the complex overlap phase is arbitrary.
- Reproduces GPEC’s own
O_CPhi_xeto 3e-16 and itsPhi_overlap_normto 4e-5 on the DIII-D n=1, n=2 and n=3 ideal runs.
energy_norm_matrix
Machine-independentConvention-sensitive
source
energy_norm_matrix(eigenvalues, eigenvectors, *, negative_eigenvalues)
The inverse square root of an energy matrix, from its eigendecomposition.
| Name | Type | Unit | Description |
|---|---|---|---|
eigenvalues | array_like | - | Eigenvalues of the energy matrix. |
eigenvectors | array_like | - | Its eigenvectors, one per row, matching |
negative_eigenvalues | str | n/a | What to do with an eigenvalue at or below zero, one of |
| Returns | |||
| — | ndarray | - |
|
Convention
Rows of eigenvectors are the eigenvectors, so the reconstruction is
V^H diag(lambda) V. That is this function’s contract, not a claim
about any file: see the Limitations.
Raises
ValueErrornegative_eigenvaluesis not one of the three; the shapes disagree oreigenvectorsis not square; an eigenvalue is complex or not finite;"raise"was chosen and an eigenvalue is not positive; or the policy left nothing.
Processing steps · Applicability · Limitations · Provenance
Processing steps
- Apply the policy to the eigenvalues.
- Form
V^H diag(lambda^{-1/2}) Vover what survives.
Applicability
Machine-independent.
Limitations
No run in reach satisfies this contract, so it is unvalidated against
real data. The ideal examples carry W_xe as identically zero, and
every policy then refuses. The kinetic example carries a non-zero one,
but that matrix is not Hermitian at all – max|W - W^H| is 1.8 times
max|W| – and its own eigenvector array reconstructs it under none of
the four orientations, the closest being V^T diag V* against the
Hermitian part at a relative error of 1.0. Feeding GPEC’s own pair in
gives a matrix N with max|N W N - I| of order ten. Whatever
W_xe and W_xe_eigenvector are to each other, it is not an
eigendecomposition in the sense used here. Establish that before norming
a coupling matrix with a file’s own arrays.
Provenance
- Conventions register C-11: three different
W^{-1/2}constructions, with different negative-eigenvalue policies, fed the same metric.
finite_width_delta
Machine-independentConvention-sensitive
source
finite_width_delta(psi_norm, field, rational_psi_norm, widths)
The jump in a field’s radial derivative across a finite-width layer.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Radial grid, strictly increasing. |
field | array_like | T | The resonant harmonic on that grid, complex. |
rational_psi_norm | array_like | - | Where each layer is centred. |
widths | array_like | - | Each layer’s full width. |
| Returns | |||
| — | ndarray | T |
|
Convention
A difference of derivatives, not an integral, and not normalized.
The legacy docstring called this an integration across the layer; it
evaluates the derivative at the two layer edges and subtracts. And it is
the raw jump, in the field’s own units – not GPEC’s unitless Delta,
which is this jump scaled by area / (2 pi chi1) and evaluated at
GPEC’s sing_spot rather than at a physical layer edge
(resonant_delta).
Raises
ValueErrorThe grid and the field disagree in length, the grid is not strictly increasing or has fewer than three points, a value is not finite, a width is not positive, a layer’s window reaches past the grid, or a layer holds fewer than two grid points either side of its centre.
Processing steps · Applicability · Limitations · Provenance
Processing steps
- Differentiate the field on its grid.
- Interpolate the derivative to each layer’s two edges.
- Subtract inner from outer.
Applicability
Machine-independent.
Limitations
The derivative is a central difference on the stored grid, so an edge within one node of the centre samples the layer’s own structure rather than the solution beside it – on a kink of slope jump 3, a width of 0.02 on a 0.05 grid returns 0.6. Such a layer is refused rather than answered; at high Lundquist number the paper width law falls below the spacing of a uniform grid, and the caller has to refine it there first.
Provenance
resistive_gpec_metrics.py::finite_width_delta.
group_coincident_islands
Machine-independentConvention-sensitive
source
group_coincident_islands(psi_norm, width, *, policy='envelope', psi_tol=1e-08)
Collapse islands that sit on the same surface into one.
Several toroidal modes resonate at the same q, so a multi-mode study has
islands stacked on one surface. The spacing between surfaces means
nothing until those are one island, and the width that one island should
have is not derivable – hence policy.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Island positions, possibly with repeats. |
width | array_like | - | Full width of each. |
policy | str, optional | n/a | One of |
psi_tol | float, optional | - | How close two positions must be to count as the same surface. Clustering is single-linkage: each island is compared with the last one added, so a chain of islands each within |
| Returns | |||
| — | tuple | - |
|
Convention
envelope is the conservative reading – a stack is as wide as its
widest member – and rss treats the modes as independent
contributions. They differ by up to the square root of the stack size,
and neither is a default the physics supplies, so the choice travels with
the result.
Raises
ValueErrorpolicyis not one ofCOINCIDENT_POLICIES,psi_tolis negative, orwidthis not one per surface.
Defaults · Applicability · Provenance
Defaults
policy="envelope" is a numerical convenience – the conservative of
the two – and psi_tol=1e-8 is an empirical estimate: it separates
the genuinely distinct surfaces of the runs looked at, whose closest
non-coincident pair is 7.9e-13 apart, from the exactly coincident ones.
Neither is supplied by the physics.
Applicability
Machine-independent.
Provenance
gpec_multimode_metrics._group_coincident_islands.
helical_phase_sweep
Machine-independentConvention-sensitive
source
helical_phase_sweep(tables, q, column, *, angles, weights=None, integrality_tolerance=1e-06)
Show source vaft/process/perturbation.py, lines 1980–2057
def helical_phase_sweep(
tables: Mapping[int, Mapping[str, object]],
q: float,
column: str,
*,
angles,
weights: Mapping[int, complex] | None = None,
integrality_tolerance: float = 1e-6,
) -> np.ndarray:
"""The composite drive at one surface, as a function of the helical angle.
Parameters
----------
tables : mapping of int to mapping
One resonant table per toroidal mode number [n/a].
q : float
The rational surface to sweep at [-].
column : str
The column to sum [n/a].
angles : array_like
Helical angles to evaluate at [rad].
weights : mapping of int to complex, optional
Per-mode complex weight, applied before the sum [n/a].
integrality_tolerance : float, optional
Passed to :func:`align_surfaces_by_q` [-].
Returns
-------
ndarray
The real amplitude at each angle, in the order given. Units follow
``column`` [n/a].
Raises
------
ValueError
As :func:`composite_drive_at_q`, or an angle is not finite.
Applicability
-------------
Machine-independent.
Convention
----------
The angle is the helical one, ``u = q theta - zeta``, in which the mode
``n`` advances as ``exp(i n u)``; :func:`composite_drive_at_q` derives
it. Its origin is the input's, and shifting that origin translates the
whole curve rather than changing its shape.
Processing steps
----------------
1. Align, weight and collect the contributions, as
:func:`composite_drive_at_q` does.
2. Evaluate ``sum_n Re(z_n exp(i n u))`` at each requested angle.
Limitations
-----------
A sweep resolves a series whose highest harmonic is the largest
resonating ``n``; sampling it more coarsely than a few points per period
of that harmonic will miss the peak, and nothing here checks the caller's
spacing against it.
Provenance
----------
.. [legacy] ``gpec_multimode_metrics.phase_sweep_composite``, which
rotated one nominated mode through a default three angles -- 0, 90 and
180 degrees, so a sweep could not tell ``+90`` from ``-90`` -- and
returned ``nan`` rows rather than an error when no surface matched.
Rotating one mode and sweeping the shared angle differ once more than
two modes resonate.
"""
requested = np.atleast_1d(np.asarray(angles, dtype=float))
if not np.all(np.isfinite(requested)):
raise ValueError("every angle must be finite")
modes, values = _contributions_at_q(
tables, q, column, weights=weights, integrality_tolerance=integrality_tolerance
)
orders = np.asarray(modes, dtype=float)
return (values[None, :] * np.exp(1j * np.outer(requested, orders))).real.sum(axis=1)The composite drive at one surface, as a function of the helical angle.
| Name | Type | Unit | Description |
|---|---|---|---|
tables | mapping of int to mapping | n/a | One resonant table per toroidal mode number. |
q | float | - | The rational surface to sweep at. |
column | str | n/a | The column to sum. |
angles | array_like | rad | Helical angles to evaluate at. |
weights | mapping of int to complex, optional | n/a | Per-mode complex weight, applied before the sum. |
integrality_tolerance | float, optional | - | Passed to |
| Returns | |||
| — | ndarray | n/a | The real amplitude at each angle, in the order given. Units follow |
Convention
The angle is the helical one, u = q theta - zeta, in which the mode
n advances as exp(i n u); composite_drive_at_q derives
it. Its origin is the input’s, and shifting that origin translates the
whole curve rather than changing its shape.
Raises
ValueErrorAscomposite_drive_at_q, or an angle is not finite.
Processing steps · Applicability · Limitations · Provenance
Processing steps
- Align, weight and collect the contributions, as
composite_drive_at_qdoes. - Evaluate
sum_n Re(z_n exp(i n u))at each requested angle.
Applicability
Machine-independent.
Limitations
A sweep resolves a series whose highest harmonic is the largest
resonating n; sampling it more coarsely than a few points per period
of that harmonic will miss the peak, and nothing here checks the caller’s
spacing against it.
Provenance
gpec_multimode_metrics.phase_sweep_composite, which rotated one nominated mode through a default three angles – 0, 90 and 180 degrees, so a sweep could not tell+90from-90– and returnednanrows rather than an error when no surface matched. Rotating one mode and sweeping the shared angle differ once more than two modes resonate.
island_overlap_width
Machine-independentConvention-sensitive
source
island_overlap_width(psi_norm, width, *, separatrix=1.0, policy='envelope', psi_tol=1e-08)
How far in from the boundary the islands overlap without a break.
Two conditions, and the first is what makes the quantity mean anything:
the outermost island must actually reach separatrix, and from there
the chain is followed inward while adjacent pairs touch. The first gap is
the inner edge of the region. Islands overlapping deep in the core are
not an edge-connected layer however much they overlap, and a region of
zero extent is not connected to anything.
This differs from the code it replaces, which decided the question from whether the outermost pair overlapped and never looked at where the outermost island ended – so a stochastic patch at psi_n = 0.2 came back as an edge layer 0.83 wide.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Island positions. |
width | array_like | - | Full island width per surface. |
separatrix | float, optional | - | Where the boundary is, which the width is measured in from. |
policy | str, optional | n/a | Passed to |
psi_tol | float, optional | - | Passed to |
| Returns | |||
| — | IslandOverlap | - | The width, the first gap that broke the chain, whether the chain reached the boundary at all, and the two Chirikov maxima. |
Convention
Measured inward from separatrix and clipped to it: the result is a
width measured from the boundary, so it cannot exceed the boundary’s own
position. A fixed upper bound of one – what the code this replaces used
– reports a width larger than the plasma whenever the innermost island
extends past the axis.
Raises
ValueErrorseparatrixis not finite,policy/psi_tolare invalid, or the islands do not passisland_pairs’ checks.
Processing steps · Defaults · Applicability · Limitations · Provenance
Processing steps
- Collapse coincident surfaces under
policy. - Form the adjacent pairs and their gaps.
- If the outermost island does not reach
separatrix, the width is zero and the region is not edge-connected. - If the outermost pair does not touch, likewise.
- Otherwise walk inward to the first pair that does not touch; its gap midpoint is the inner boundary. With no such gap, the innermost island’s inner separatrix is.
Defaults
separatrix = 1.0 is a numerical convenience: the last closed flux
surface in normalized poloidal flux. policy and psi_tol are
passed through and documented on group_coincident_islands.
Applicability
Machine-independent.
Limitations
onset_scale is the factor the perturbation field would have to be
multiplied by for the outermost pair to reach contact, on the assumption
that island width scales as the square root of the field. That assumption
is fixed-equilibrium linear response and is recorded, not checked.
Provenance
gpec_multimode_metrics.compute_island_overlap_width.
island_pairs
Machine-independentConvention-sensitive
source
island_pairs(psi_norm, width)
Show source vaft/process/perturbation.py, lines 891–962
def island_pairs(psi_norm, width) -> IslandPairs:
"""Every adjacent pair of islands, with the gap between their separatrices.
Parameters
----------
psi_norm : array_like
Rational-surface positions [-].
width : array_like
Full island width per surface [-].
Returns
-------
IslandPairs
One entry per adjacent pair, sorted outward; empty for fewer than two
surfaces [-].
Raises
------
ValueError
``width`` is not one per surface, two surfaces share a position, or a
position or width is not finite or a width is negative.
Processing steps
----------------
1. Sort outward.
2. For each pair, the gap is the spacing less the two half widths.
3. A gap at or below zero -- within rounding -- is contact.
Defaults
--------
The contact tolerance is a numerical convenience, 1e-12 of the spacing.
Convention
----------
Residual gaps below that fraction of the spacing count as contact rather
than separation, so two separatrices that meet exactly are not reported
as apart -- and "round-off" means the same thing at every radius, which a
fixed absolute tolerance does not.
Applicability
-------------
Machine-independent.
Provenance
----------
.. [legacy] ``gpec_multimode_metrics.compute_pair_chirikov``.
"""
ordered, ordered_width, order = _island_arrays(psi_norm, width)
if ordered.size < 2:
empty = np.empty(0, dtype=float)
index = np.empty(0, dtype=int)
return IslandPairs(index, index, empty, empty, empty, empty, empty,
empty, empty, np.empty(0, dtype=bool))
spacing = np.diff(ordered)
half_sum = 0.5 * (ordered_width[:-1] + ordered_width[1:])
gap = spacing - half_sum
return IslandPairs(
inner_index=order[:-1],
outer_index=order[1:],
inner_psi_norm=ordered[:-1],
outer_psi_norm=ordered[1:],
spacing=spacing,
inner_right=ordered[:-1] + 0.5 * ordered_width[:-1],
outer_left=ordered[1:] - 0.5 * ordered_width[1:],
gap=gap,
chirikov=half_sum / spacing,
# Scaled to the spacing, so "round-off" means the same thing whether
# the surfaces are a thousandth apart or a whole unit. A fixed atol --
# what the code this replaces used, with an rtol against zero that
# could never contribute -- is a different criterion at every radius.
overlaps=gap <= _CONTACT_TOLERANCE * spacing,
)Every adjacent pair of islands, with the gap between their separatrices.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Rational-surface positions. |
width | array_like | - | Full island width per surface. |
| Returns | |||
| — | IslandPairs | - | One entry per adjacent pair, sorted outward; empty for fewer than two surfaces. |
Convention
Residual gaps below that fraction of the spacing count as contact rather than separation, so two separatrices that meet exactly are not reported as apart – and “round-off” means the same thing at every radius, which a fixed absolute tolerance does not.
Raises
ValueErrorwidthis not one per surface, two surfaces share a position, or a position or width is not finite or a width is negative.
Processing steps · Defaults · Applicability · Provenance
Processing steps
- Sort outward.
- For each pair, the gap is the spacing less the two half widths.
- A gap at or below zero – within rounding – is contact.
Defaults
The contact tolerance is a numerical convenience, 1e-12 of the spacing.
Applicability
Machine-independent.
Provenance
gpec_multimode_metrics.compute_pair_chirikov.
jump_width
Machine-independentConvention-sensitive
source
jump_width(lundquist, model)
Show source vaft/process/perturbation.py, lines 2662–2713
def jump_width(lundquist, model: JumpWidthModel) -> np.ndarray:
"""The resistive-layer width a Lundquist number implies.
Parameters
----------
lundquist : array_like
Lundquist number per rational surface [-].
model : JumpWidthModel
The width law. **Required**: the coefficients are one paper's fit,
not a property of the physics, so there is no default to fall back
on [n/a].
Returns
-------
ndarray
Full layer width per surface, in normalized poloidal flux [-].
Raises
------
ValueError
A Lundquist number is not finite and positive.
Convention
----------
``w = floor + scale * S**(-1/3)``. The ``S**(-1/3)`` scaling is the
resistive-inertial layer's; the floor keeps the width finite as ``S``
grows, where the pure scaling would shrink the layer below the grid.
Applicability
-------------
Machine-independent.
Processing steps
----------------
1. Check the Lundquist numbers are finite and positive.
2. Apply the model's law.
Provenance
----------
.. [legacy] ``resistive_gpec_metrics.py::jump_width_from_lundquist``,
which carried the paper's coefficients as keyword defaults -- so a
caller who never chose a model got one anyway.
"""
if not isinstance(model, JumpWidthModel):
raise TypeError(
f"model must be a JumpWidthModel, not {type(model).__name__}; "
"PAPER_JUMP_WIDTH reproduces the legacy scan"
)
values = np.atleast_1d(np.asarray(lundquist, dtype=float))
if not np.all(np.isfinite(values)) or np.any(values <= 0.0):
raise ValueError("every Lundquist number must be finite and positive")
return model.floor + model.scale * np.power(values, -1.0 / 3.0)The resistive-layer width a Lundquist number implies.
| Name | Type | Unit | Description |
|---|---|---|---|
lundquist | array_like | - | Lundquist number per rational surface. |
model | JumpWidthModel | n/a | The width law. Required: the coefficients are one paper’s fit, not a property of the physics, so there is no default to fall back on. |
| Returns | |||
| — | ndarray | - | Full layer width per surface, in normalized poloidal flux. |
Convention
w = floor + scale * S**(-1/3). The S**(-1/3) scaling is the
resistive-inertial layer’s; the floor keeps the width finite as S
grows, where the pure scaling would shrink the layer below the grid.
Raises
ValueErrorA Lundquist number is not finite and positive.
Processing steps · Applicability · Provenance
Processing steps
- Check the Lundquist numbers are finite and positive.
- Apply the model’s law.
Applicability
Machine-independent.
Provenance
resistive_gpec_metrics.py::jump_width_from_lundquist, which carried the paper’s coefficients as keyword defaults – so a caller who never chose a model got one anyway.
lundquist_number
Machine-independent
source
lundquist_number(tau_resistive, tau_alfven)
Show source vaft/process/perturbation.py, lines 2618–2659
def lundquist_number(tau_resistive, tau_alfven) -> np.ndarray:
"""The Lundquist number, the resistive over the Alfven time.
Parameters
----------
tau_resistive : array_like
Resistive diffusion time per rational surface [s].
tau_alfven : array_like
Alfven time per rational surface [s].
Returns
-------
ndarray
``S = tau_r / tau_a`` per surface [-].
Raises
------
ValueError
The two disagree in shape, or a time is not finite and positive.
Applicability
-------------
Machine-independent.
Processing steps
----------------
1. Check both times are finite, positive and the same shape.
2. Divide.
Provenance
----------
.. [legacy] ``library/resistive_gpec_metrics.py::lundquist_number``
(branch ``codex/transp``).
"""
resistive = np.atleast_1d(np.asarray(tau_resistive, dtype=float))
alfven = np.atleast_1d(np.asarray(tau_alfven, dtype=float))
if resistive.shape != alfven.shape:
raise ValueError(f"{resistive.shape} resistive times against {alfven.shape} Alfven times")
for label, values in (("resistive", resistive), ("Alfven", alfven)):
if not np.all(np.isfinite(values)) or np.any(values <= 0.0):
raise ValueError(f"every {label} time must be finite and positive")
return resistive / alfvenThe Lundquist number, the resistive over the Alfven time.
| Name | Type | Unit | Description |
|---|---|---|---|
tau_resistive | array_like | s | Resistive diffusion time per rational surface. |
tau_alfven | array_like | s | Alfven time per rational surface. |
| Returns | |||
| — | ndarray | - |
|
Raises
ValueErrorThe two disagree in shape, or a time is not finite and positive.
Processing steps · Applicability · Provenance
Processing steps
- Check both times are finite, positive and the same shape.
- Divide.
Applicability
Machine-independent.
Provenance
library/resistive_gpec_metrics.py::lundquist_number(branchcodex/transp).
nearest_surface_spacing
Machine-independent
source
nearest_surface_spacing(psi_norm)
Show source vaft/process/perturbation.py, lines 703–749
def nearest_surface_spacing(psi_norm) -> np.ndarray:
"""Distance from each rational surface to its nearest neighbour.
Parameters
----------
psi_norm : array_like
Rational-surface positions, in any order [-].
Returns
-------
ndarray
One distance per surface, in the input's order; ``nan`` for a single
surface, which has no neighbour [-].
Raises
------
ValueError
Two surfaces share a position.
Processing steps
----------------
1. Sort the surfaces outward.
2. Take the smaller of the two gaps either side; an end surface has one.
3. Put the result back in the caller's order.
Applicability
-------------
Machine-independent.
Provenance
----------
.. [legacy] ``gpec_analysis_metrics._nearest_neighbour_spacing`` and
``gpec_multimode_metrics.compute_surface_chirikov``, which computed the
same spacing twice and disagreed about repeated positions: one
tolerated them, the other refused. This refuses.
"""
ordered, _, order = _island_arrays(psi_norm, np.zeros_like(np.asarray(psi_norm, float)))
result = np.full(ordered.size, np.nan)
if ordered.size < 2:
return result
gaps = np.diff(ordered)
result[0], result[-1] = gaps[0], gaps[-1]
if ordered.size > 2:
result[1:-1] = np.minimum(gaps[:-1], gaps[1:])
unsorted = np.empty_like(result)
unsorted[order] = result
return unsortedDistance from each rational surface to its nearest neighbour.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Rational-surface positions, in any order. |
| Returns | |||
| — | ndarray | - | One distance per surface, in the input’s order; |
Raises
ValueErrorTwo surfaces share a position.
Processing steps · Applicability · Provenance
Processing steps
- Sort the surfaces outward.
- Take the smaller of the two gaps either side; an end surface has one.
- Put the result back in the caller’s order.
Applicability
Machine-independent.
Provenance
gpec_analysis_metrics._nearest_neighbour_spacingandgpec_multimode_metrics.compute_surface_chirikov, which computed the same spacing twice and disagreed about repeated positions: one tolerated them, the other refused. This refuses.
penetration_ratio
Machine-independentConvention-sensitive
source
penetration_ratio(psi_norm, width)
Show source vaft/process/perturbation.py, lines 795–831
def penetration_ratio(psi_norm, width) -> np.ndarray:
"""Island width as a fraction of the width at which islands would overlap.
Parameters
----------
psi_norm : array_like
Rational-surface positions [-].
width : array_like
Full island width per surface, in the same units [-].
Returns
-------
ndarray
``width / critical_island_width``; one at overlap [-].
Raises
------
ValueError
``width`` is not one per surface, two surfaces share a position, or a
position or width is not finite or a width is negative.
Convention
----------
A ratio, not a verdict: the value one is where islands touch, and
comparing against it is the caller's to do.
Applicability
-------------
Machine-independent.
Provenance
----------
.. [legacy] ``gpec_analysis_metrics.compute_critical_island_width``.
"""
_island_arrays(psi_norm, width) # refuses a tie or a shape mismatch
with np.errstate(divide="ignore", invalid="ignore"):
return np.asarray(width, dtype=float) / critical_island_width(psi_norm)Island width as a fraction of the width at which islands would overlap.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Rational-surface positions. |
width | array_like | - | Full island width per surface, in the same units. |
| Returns | |||
| — | ndarray | - |
|
Convention
A ratio, not a verdict: the value one is where islands touch, and comparing against it is the caller’s to do.
Raises
ValueErrorwidthis not one per surface, two surfaces share a position, or a position or width is not finite or a width is negative.
Applicability · Provenance
Applicability
Machine-independent.
Provenance
gpec_analysis_metrics.compute_critical_island_width.
q_composite_table
Machine-independentConvention-sensitive
source
q_composite_table(tables, column, *, weights=None, integrality_tolerance=1e-06, shared_only=False)
One row per rational surface, summed over the modes that reach it.
| Name | Type | Unit | Description |
|---|---|---|---|
tables | mapping of int to mapping | n/a | One resonant table per toroidal mode number. |
column | str | n/a | The column to sum. |
weights | mapping of int to complex, optional | n/a | Per-mode complex weight. |
integrality_tolerance | float, optional | - | Passed to |
shared_only | bool, optional | n/a | Keep only the surfaces every mode resonates at. |
| Returns | |||
| — | dict of str to ndarray | n/a | Columns |
Convention
A surface a mode cannot resonate at is not a gap in the data, so its
m_<n> is -1 rather than missing, and every row has every column.
Raises
ValueErrorAscomposite_drive_at_q, orshared_onlyleaves nothing.
Processing steps · Applicability · Limitations · Provenance
Processing steps
- Align the surfaces across the modes.
- Compute the composite drive at each.
- Lay the per-mode positions and poloidal mode numbers out beside it.
Applicability
Machine-independent.
Limitations
Every row is one surface’s own Fourier series; nothing sums down a
column. A radial reduction over these rows would mix surfaces whose
helical angles are different angles, which is what
reduce_resonant refuses to do across modes.
Provenance
gpec_multimode_metrics.compute_q_composite_table, which built rows only for four hard-codedqvalues, dropped any row where no mode matched so the row count depended on the data, and emitted per-mode columns only where a mode was present, leaving a frame whose columns varied by row. Two of its columns are not ported:K_proxy_sqrt, the square root of a flux magnitude standing in for a Chirikov parameter, which is not dimensionally a Chirikov parameter of anything; andsum_K_isl_wrong, whose name was its own warning.
reduce_delta_e
Machine-independentConvention-sensitive
source
reduce_delta_e(values, *, combination='quadrature', weights=None)
Combine a per-mode edge-overlap metric into one number.
| Name | Type | Unit | Description |
|---|---|---|---|
values | mapping of int to float | - | One |
combination | {'quadrature', 'linear'}, optional | n/a |
|
weights | mapping of int to float, optional | - | Per-mode weight; every mode present in |
| Returns | |||
| — | float | - | The combined metric, in the same units as the inputs. |
Convention
Both combinations are homogeneous of degree one in the weights, so
rescaling every weight by the same non-negative factor moves either
result by that factor; what differs is how modes combine with each
other, and quadrature is the default because delta_e is a magnitude
with no phase left to interfere. Negative weights are refused rather
than given a meaning: they are homogeneous of degree one only for
linear – quadrature squares them away – and they reintroduce the
cancellation the negative-value check exists to forbid.
Raises
ValueErrorvaluesis empty, a value or weight is not finite or not real, a value is negative, the combination is not one of the two, orweightsnames a mode thatvaluesdoes not.
Processing steps · Defaults · Applicability · Limitations · Provenance
Processing steps
- Weight each mode’s value.
- Sum the weighted values, in quadrature or linearly.
Defaults
combination = "quadrature" is a conventional choice that follows
from what the input is: delta_e is the magnitude of a projection, so
the modes carry no relative phase and adding them linearly would assume
one they do not have.
Applicability
Machine-independent.
Limitations
Neither combination is a physical amplitude. delta_e measures how
much of one mode’s external field lands on that mode’s own most-coupled
direction, and those directions belong to different singular value
problems, so no combination of them is the overlap of anything.
Provenance
gpec_multimode_metrics.delta_e_weighted_norm, whoseuse_complex_sum=Truebranch tookabs(sum(w * complex(v))). Its inputs are real by construction –delta_e_vectorreadsprojected_overlap, a magnitude – so that branch equalled the linear sum and the “coherent” name promised a phase that had already been discarded upstream. It islinearhere, under a name that claims no more than it does. Its companiondelta_e_vectorwas a dictionary comprehension over an attribute name, with an unvalidated region returningNonefor every mode; assembling that mapping is the caller’s job and needs no function.
reduce_resonant
Machine-independentConvention-sensitive
source
reduce_resonant(psi_norm, values, *, window, statistic='rms')
Reduce one resonant column over one radial window.
Complex columns – the resonant flux, the shielding current, the
resonance parameter – are reduced on their magnitude: a complex mean
depends on a gauge the file does not fix, so averaging the phase would
give an answer that changes with a convention rather than with the
plasma. A real column keeps its sign, so mean of a rotation
profile that changes sign is that mean and not its rectification; pass
numpy.abs(values) to reduce a signed column on magnitude.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Normalized poloidal flux of each rational surface. |
values | array_like | any | One column of the resonant table, real or complex. |
window | ResonantWindow | - | The interval to reduce over. |
statistic | str, optional | n/a | One of |
| Returns | |||
| — | float | any | The reduced value, in the column’s own unit; |
Convention
A complex column reduces on its magnitude and a real one keeps its sign; the value comes back in the column’s own unit, unscaled.
Raises
ValueErrorpsi_normandvaluesdiffer in length, orstatisticis not one ofRESONANT_STATISTICS.
Processing steps · Applicability · Limitations · Provenance
Processing steps
- Take the magnitude of a complex column; leave a real one signed.
- Select the surfaces inside
window. - Drop non-finite samples, then apply
statistic.
Applicability
Machine-independent. Any per-rational-surface column.
Limitations
Returns nan rather than raising when the window is empty: a plasma
with no rational surface in a region is a normal result, and a run with
four rational surfaces has regions that are legitimately empty.
Provenance
- Conventions register C-18: quantities on the rational-surface index must not be averaged with quantities on the mode index, and a complex mean is gauge-dependent.
resonant_delta
Machine-independentConvention-sensitive
source
resonant_delta(psi_norm, field, *, psi_rational, area, dq_dpsi_norm, chi1, m_pol, n_tor, offset=0.0005)
The resonance parameter from the jump in a stored spectral field.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Radial grid the field is stored on. |
field | array_like | T | One harmonic of the Jacobian-weighted contravariant flux, |
psi_rational | float | - | Where the surface is. |
area | float | m^2 | Area of that surface. |
dq_dpsi_norm | float | - |
|
chi1 | float | - | The equilibrium’s poloidal flux normalization. |
m_pol, n_tor | int | - | The harmonic’s mode numbers, |
offset | float, optional | - | Where the jump is evaluated, as a fraction of |
| Returns | |||
| — | ResonantJump | - |
|
Convention
The resonant harmonic is screened, its derivative is not. In an ideal
solution the m = nq component of the perturbed field falls to zero at
its own rational surface. Measured on the DIII-D reference at the q = 2
surface, Jbgradpsi at m = 2 is 2.4e-9 where its neighbours
m = 1, 3, 4 are 1.2e-4, 3.7e-4 and 5.0e-4 – five orders down, and
radially it collapses to its minimum at that surface. Sampling the field
there recovers nothing. What survives is the jump in
its radial derivative, and that is what this takes.
Raises
ValueErrorThe grid and the field disagree in length, a value is not finite, the shear is zero,chi1or the area is zero or non-finite, the offset is not positive, the surface is outside the grid, or one side has too few points to fit.
Processing steps · Defaults · Applicability · Limitations · Provenance
Processing steps
- Place the evaluation points at
psi_rational +- offset / (n |q'|). - Fit a cubic to the field on each side separately, over the band between half and four times that distance, and differentiate it.
- Take the difference, scale by
area / (2 pi chi1).
Defaults
offset = 5e-4 is a legacy compatibility value: GPEC’s compiled-in
sing_spot (gpec/gpec.f:120). It is a namelist parameter, so a
run can and does change it – docs/examples/run_kinetic_example/
gpec.in ships 1e-3 – and the result depends on it: on the DIII-D
reference, evaluating at 1e-3 instead moves the derived Delta by
2.2 to 5.7 per cent and at 2e-3 by up to 13 per cent. A comparison
against a run’s own Delta has to pass that run’s sing_spot.
Agreement being best at the value the reference run actually used is
itself evidence the jump is being taken the way GPEC takes it.
Applicability
Machine-independent.
Limitations
The fit is one-sided because the layer between the two points is where
the solution is least resolved; how well it extrapolates back to the
surface is what limits the accuracy. Against GPEC’s own Delta on 24
rational surfaces of the 147131 reference the magnitude agrees to between
0.4 and 5.5 per cent and the phase to within a degree on all but three.
GPEC re-solves at the evaluation points rather than fitting a stored
grid, and closing the remaining few per cent means doing the same.
Provenance
GPEC/gpec/gpout.f:1678-1688, which evaluatesbwp1_mnatrespsi -+ sing_spot/(nn |q1|)and formsdelta = (rbwp1mn - lbwp1mn) / (twopi * chi1). The factor ofareahere is thesingflx_mn/area(ising)normalization applied at:1705, which the storedJbgradpsialready carries.
resonant_geometric_factor
Machine-independentConvention-sensitive
source
resonant_geometric_factor(delta, phi_res, n_tor)
Measure the surface factor that turns a resonance parameter into a flux.
| Name | Type | Unit | Description |
|---|---|---|---|
delta | array_like | - | Resonance parameter per rational surface, complex. |
phi_res | array_like | T | Pitch-resonant flux per rational surface, complex. |
n_tor | int | - | Toroidal mode number the two were computed at. |
| Returns | |||
| — | ndarray | T |
|
Convention
G = -n * Phi_res / Delta is a property of the equilibrium, not of
the perturbation: it absorbs the surface integral j_c and the
diagonal of the vacuum surface inductance, neither of which knows about
the applied field. Measured on the DIII-D reference it agrees across
n = 1, 2 and 3 at every shared surface to 0.6 per cent, and the values
the three modes report at surfaces only one of them reaches fall on one
smooth curve in q – so a run’s own surfaces are enough to
interpolate G onto surfaces a different n resonates at.
Raises
ValueErrorThe two disagree in length, a value is not finite, adeltais zero or small enough to overflow the ratio, or the ratio is not real to within a tenth of a degree – which means the inputs are not a matched pair from one run.
Processing steps · Applicability · Limitations · Provenance
Processing steps
- Form
-n * Phi_res / Deltaper surface. - Check the result is real, and return its real part.
Applicability
Machine-independent.
Limitations
The 0.6 per cent agreement across mode numbers is systematic rather than
noise – it grows monotonically with n – because the vacuum Green’s
function does depend weakly on the toroidal mode number. G measured
at one n and used at another carries that error. Measuring it at the
n in hand avoids it entirely.
Provenance
- The chain this collapses is
GPEC/gpec/gpout.f:1690-1705:I_resfromDeltathroughj_c, thenPhi_resfromI_resthroughfsurf_indmats, whichgpvacuum.f:236-342builds by calling the VACUUM code twice per surface.
resonant_metrics
Machine-independentConvention-sensitive
source
resonant_metrics(table, *, windows, columns=None, statistic='rms')
Reduce every named column of a resonant table over every window.
| Name | Type | Unit | Description |
|---|---|---|---|
table | mapping of str to ndarray | any | A resonant table, as |
windows | mapping of str to ResonantWindow | - | The windows to reduce over, from |
columns | sequence of str, optional | n/a | Which columns to reduce; those of |
statistic | str, optional | n/a | One of |
| Returns | |||
| — | dict | any | Keyed |
Convention
Inherits reduce_resonant’s – magnitudes for complex columns,
the column’s own unit – and the window convention of whatever
resonant_windows produced.
Raises
KeyErrorThe table carries nopsi_n_rational, no response column at all, or not one thatcolumnsnamed.
Applicability · Limitations · Provenance
Applicability
Machine-independent.
Limitations
A reconstruction run carries rational surfaces and no perturbed response, and a kinetic run may carry no rational-surface block at all – its table comes back empty. Both are refused here rather than reduced into something that looks like an answer.
Provenance
gpec_analysis_metrics.compute_metric_table, which carried the same reductions with the windows and the gauss factor built in.
resonant_windows
Machine-independentConvention-sensitive
source
resonant_windows(pedestal=None, *, legacy=False)
The core, edge and total radial windows a reduction is taken over.
The core/edge boundary comes from the pedestal rather than from a
constant. With a PedestalTop that resolved a fit, the edge begins at
the pedestal’s inner knee; with one that fell back, it begins at the
fallback position, and the window says so.
| Name | Type | Unit | Description |
|---|---|---|---|
pedestal | PedestalTop, optional | - | The pedestal boundary, from |
legacy | bool, optional | - | Return |
| Returns | |||
| — | dict of str to ResonantWindow | - | Keyed |
Convention
Bounds are normalized poloidal flux, endpoints included at both ends, so a rational surface exactly at the boundary is counted in both neighbours rather than dropped by one.
Raises
ValueErrorNeither a pedestal norlegacy=Truewas given, or the pedestal’s coordinate is notpsi_norm.
Defaults · Applicability · Provenance
Defaults
The 0.8 and 0.95 boundaries of LEGACY_WINDOWS are a legacy
compatibility value, not a physical one: they are what the metrics this
module replaces hard-coded.
Applicability
Machine-independent. Any radial coordinate expressed as normalized poloidal flux.
Provenance
- Migration decision D-05: the pedestal top is determined from an EPED-style profile fit, and core, edge and pedestal regions are derived from that result rather than fixed.
- Conventions register C-16: seven different core/edge/pedestal window sets were in use across the code this replaces.
rms_resonant_field
Machine-independentConvention-sensitive
source
rms_resonant_field(psi_norm, phi_res, *, window)
Show source vaft/process/perturbation.py, lines 401–433
def rms_resonant_field(psi_norm, phi_res, *, window: ResonantWindow) -> float:
"""Root-mean-square resonant field over a radial window.
Parameters
----------
psi_norm : array_like
Normalized poloidal flux of each rational surface [-].
phi_res : array_like
Resonant flux per surface, complex as GPEC writes it [T].
window : ResonantWindow
The interval to reduce over [-].
Returns
-------
float
RMS magnitude, in tesla; ``nan`` when the window holds no surface [T].
Convention
----------
Tesla, not gauss. GPEC normalizes this flux by the surface area, so it
carries field units already; the code this replaces multiplied by 1e4
inside the reduction, which put a unit conversion where nothing said so.
Applicability
-------------
Machine-independent.
Provenance
----------
.. [legacy] ``gpec_analysis_metrics.compute_rms_resonant_field``, whose
``to_gauss=True`` default is the factor not applied here.
"""
return reduce_resonant(psi_norm, phi_res, window=window, statistic="rms")Root-mean-square resonant field over a radial window.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_norm | array_like | - | Normalized poloidal flux of each rational surface. |
phi_res | array_like | T | Resonant flux per surface, complex as GPEC writes it. |
window | ResonantWindow | - | The interval to reduce over. |
| Returns | |||
| — | float | T | RMS magnitude, in tesla; |
Convention
Tesla, not gauss. GPEC normalizes this flux by the surface area, so it carries field units already; the code this replaces multiplied by 1e4 inside the reduction, which put a unit conversion where nothing said so.
Applicability · Provenance
Applicability
Machine-independent.
Provenance
gpec_analysis_metrics.compute_rms_resonant_field, whoseto_gauss=Truedefault is the factor not applied here.
shielded_field
Machine-independentConvention-sensitive
source
shielded_field(delta, self_inductance)
Show source vaft/process/perturbation.py, lines 2823–2875
def shielded_field(delta, self_inductance) -> np.ndarray:
"""The field a finite-width layer's shielding current produces.
Parameters
----------
delta : array_like
The derivative jump per surface, from :func:`finite_width_delta`,
complex [T].
self_inductance : array_like
The surfaces' self-inductance matrix, square and matching ``delta``.
**Required** [n/a].
Returns
-------
ndarray
``L @ delta``, complex [T].
Raises
------
ValueError
The matrix is not square, does not match ``delta``, or holds a
non-finite value.
Convention
----------
The legacy returned ``delta`` itself when no inductance was supplied --
a derivative jump relabelled as a shielded field, with nothing in the
result to say the multiplication had not happened. There is no such
fallback here: without the inductance there is no shielded field.
Applicability
-------------
Machine-independent.
Processing steps
----------------
1. Check the matrix matches the surfaces.
2. Multiply.
Provenance
----------
.. [legacy] ``resistive_gpec_metrics.py::shielded_field_from_delta``.
"""
jump = np.atleast_1d(np.asarray(delta, dtype=complex))
matrix = np.asarray(self_inductance, dtype=complex)
if matrix.shape != (jump.size, jump.size):
raise ValueError(
f"the inductance is {matrix.shape} but there are {jump.size} surfaces; "
"it must be square and one row per surface"
)
if not np.all(np.isfinite(matrix)) or not np.all(np.isfinite(jump)):
raise ValueError("the inductance or the jump holds a non-finite value")
return matrix @ jumpThe field a finite-width layer’s shielding current produces.
| Name | Type | Unit | Description |
|---|---|---|---|
delta | array_like | T | The derivative jump per surface, from |
self_inductance | array_like | n/a | The surfaces’ self-inductance matrix, square and matching |
| Returns | |||
| — | ndarray | T |
|
Convention
The legacy returned delta itself when no inductance was supplied –
a derivative jump relabelled as a shielded field, with nothing in the
result to say the multiplication had not happened. There is no such
fallback here: without the inductance there is no shielded field.
Raises
ValueErrorThe matrix is not square, does not matchdelta, or holds a non-finite value.
Processing steps · Applicability · Provenance
Processing steps
- Check the matrix matches the surfaces.
- Multiply.
Applicability
Machine-independent.
Provenance
resistive_gpec_metrics.py::shielded_field_from_delta.
toroidal_phase_audit
Machine-independentConvention-sensitive
source
toroidal_phase_audit(stored, sampled, phi_rad, *, n_tor)
Test a stored toroidal harmonic against the same field resampled in real space.
A complex harmonic C on its own does not say which reconstruction it
belongs to: Re(C e^{-i n phi}) and Re(C e^{+i n phi}) are different
fields, and a file that stores C without saying which one it means is
ambiguous by exactly a complex conjugation. Nothing in the harmonic
resolves that; only an independent evaluation of the same physical field
over phi does. This measures both hypotheses against such an
evaluation and reports how far apart they are.
| Name | Type | Unit | Description |
|---|---|---|---|
stored | array_like of complex | any | The harmonic as the file holds it, shape |
sampled | array_like of float | any | The same physical field evaluated independently at those |
phi_rad | array_like | rad | The |
n_tor | int | - | The toroidal mode number the harmonic belongs to. |
| Returns | |||
| — | ToroidalPhaseAudit | - | Both hypotheses’ relative norms, in total and per component, which one wins and by how much, and the absolute norms the relative ones are taken against. |
Convention
The extracted coefficient is 2 * mean_phi(B e^{+i n phi}), which is the
coefficient of the reconstruction B(phi) = Re(C e^{-i n phi}) in a
counter-clockwise machine angle – the convention GPEC writes and FLARE
reads. It is the conjugate of the kernel
vaft.process.coils_non_axisymmetric.toroidal_mode_decomposition
uses, and twice it, because that one returns the plain sample mean. So a
"conjugate" verdict here means the stored file belongs to the
e^{+i n phi} reconstruction instead, and a consumer of the file has to
conjugate before use.
Raises
ValueErrorThe three arrays disagree in shape, any of them holds a non-finite value,phi_radis not one equally spaced period, it carries too few samples to resolven_tor,n_toris not a non-zero integer, or stored is identically zero so no relative norm exists.
Processing steps · Assumptions · Applicability · Limitations · Provenance
Processing steps
- Project the sampled field onto the mode:
C_measured = 2 * mean_phi(B(phi) e^{+i n phi}). - Take
||C_measured - C_stored||and||C_measured - conj(C_stored)||, each over the positions and divided by||C_stored||on the same axis, once per component and once for the whole array. - The smaller total wins; the separation ratio is the larger over the smaller.
Assumptions
phi_rad is an equally spaced full period, which is what makes the plain
sample mean the Fourier projection; a non-uniform grid is refused rather
than silently mis-weighted.
The sampled field is assumed to carry only this harmonic. M > 2|n|
is checked and refused below it, but that is Nyquist for n alone: a
field also carrying n' aliases it into the measurement whenever
n' = -n (mod M), and needs an M that resolves n' too. A caller
sampling a field of unknown content chooses M from that content, not
from n.
Applicability
Machine-independent. Any stored complex toroidal harmonic with an independent way to evaluate the same field: a coil field against Biot-Savart, a measured field against a model.
Limitations
Says which hypothesis is closer, never that either is right. When the
sampled field has little content at this n – the coil currents do not
excite it – measured_norm falls far below stored_norm, both
relative norms come out near one and the separation near one with them, and
that is the honest answer rather than a convention. The threshold at which
a separation counts as settled is a verdict and lives in
vaft.validation, not here.
An alias from another harmonic is the one failure this cannot report. It
does not resemble “no content”: the aliased projection is a clean complex
number, so the separation comes out large and the answer can be confidently
the wrong one. Measured: a field of Re(C e^{-3i phi}) + Re(C1 e^{-i
phi}) with C1 = C - conj(C), sampled at four angles and projected onto
n = 3, returns exactly conj(C) – a relative norm of 6e-16 for the
wrong hypothesis. The M > 2|n| refusal stops that particular case; only
knowing the field’s content stops the general one.
Provenance
- The legacy implementation’s
gpec_phase.py::audit_gpec_coil_brzphi_phase, which is this measurement; decision D-06 makes it mandatory for a GPEC-to-FLARE handshake. The legacy computed it only for a GPEC coil field and only against its own Biot-Savart; the arithmetic is the same and is machine- and code-independent, so it is separated from both here.