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_qMatch rational surfaces across toroidal mode numbers by their q. Convention
amplification_ratioHow much larger one run’s resonant response is than another’s.
chirikovThe island-overlap parameter, by either of the two definitions in use. Convention
composite_drive_at_qSum one resonant column over every mode that resonates at a given q. Convention
critical_island_widthThe island width at which a surface’s islands reach its neighbour’s. Convention
dominant_modeThe leading direction of a coupling matrix, weighted by surface area. Convention
edge_overlap_metricHow much of an external field lands on the dominant coupling mode. Convention
energy_norm_matrixThe inverse square root of an energy matrix, from its eigendecomposition. Convention
finite_width_deltaThe jump in a field’s radial derivative across a finite-width layer. Convention
group_coincident_islandsCollapse islands that sit on the same surface into one. Convention
helical_phase_sweepThe composite drive at one surface, as a function of the helical angle. Convention
island_overlap_widthHow far in from the boundary the islands overlap without a break. Convention
island_pairsEvery adjacent pair of islands, with the gap between their separatrices. Convention
jump_widthThe resistive-layer width a Lundquist number implies. Convention
lundquist_numberThe Lundquist number, the resistive over the Alfven time.
nearest_surface_spacingDistance from each rational surface to its nearest neighbour.
penetration_ratioIsland width as a fraction of the width at which islands would overlap. Convention
q_composite_tableOne row per rational surface, summed over the modes that reach it. Convention
reduce_delta_eCombine a per-mode edge-overlap metric into one number. Convention
reduce_resonantReduce one resonant column over one radial window. Convention
resonant_deltaThe resonance parameter from the jump in a stored spectral field. Convention
resonant_geometric_factorMeasure the surface factor that turns a resonance parameter into a flux. Convention
resonant_metricsReduce every named column of a resonant table over every window. Convention
resonant_windowsThe core, edge and total radial windows a reduction is taken over. Convention
rms_resonant_fieldRoot-mean-square resonant field over a radial window. Convention
shielded_fieldThe field a finite-width layer’s shielding current produces. Convention
toroidal_phase_auditTest a stored toroidal harmonic against the same field resampled in real space. Convention

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.

NameTypeUnitDescription
tablesmapping of int to mappingn/a

One resonant table per toroidal mode number, each carrying at least a q_rational column – the shape vaft.code.gpec.GpecProfileOutput.resonant_table returns. psi_n_rational is recorded when present.

integrality_tolerancefloat, optional-

How far n * q may sit from an integer and still count as a rational surface of that mode.

Returns
—tuple of AlignedSurfacen/a

Every q at which any mode resonates, ascending, each carrying one SurfaceMember per mode in the input.

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

  • ValueError A mode number is not a positive integer, a table has no q_rational, a q is not finite, or n * q is not an integer for a surface the run itself reports.
Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Turn each reported q into the fraction m / n and refuse a surface whose n * q is not integral – a run reporting one is reporting something that is not a rational surface.
  2. Collect the union of those fractions across modes, ascending.
  3. For every mode at every q, record resonant when the run reports a surface, non_resonant when n * q is not an integer, and absent when 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

  1. gpec_multimode_metrics.align_surfaces_by_q, which matched the single nearest surface within q_tol = 0.05 of four hard-coded targets (2, 3, 4, 5). Three departures. The window is absolute, so it admits more than one rational once the spacing 1 / n falls 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 that q at all. And its tolerance test was abs(q - target) > q_tol, which is False for a nan, so a nan q was 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.

NameTypeUnitDescription
psi_normarray_like-

Rational-surface coordinate of the case.

valuesarray_likeany

The case’s resonant column.

reference_psi_normarray_like-

Rational-surface coordinate of the reference, usually the vacuum run

reference_valuesarray_likeany

The reference’s resonant column, in the same unit as values.

windowResonantWindow-

The interval both are reduced over.

statisticstr, optionaln/a

One of RESONANT_STATISTICS.

Returns
—float-

Case over reference; nan when either window is empty or the reference reduces to zero.

Processing steps · Applicability · Limitations · Provenance

Processing steps

  1. Reduce each run over window independently, so the two need not share a rational-surface set.
  2. 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

  1. 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).chirikov

The island-overlap parameter, by either of the two definitions in use.

NameTypeUnitDescription
psi_normarray_like-

Rational-surface positions.

widtharray_like-

Full island width per surface.

definitionstr, optionaln/a

"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.

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 IslandPairs carries the indices that map each pair back.

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

  • ValueError definition is 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

  1. gpec_multimode_metrics.compute_surface_chirikov and compute_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.

NameTypeUnitDescription
tablesmapping of int to mappingn/a

One resonant table per toroidal mode number, as align_surfaces_by_q takes.

qfloat-

The rational surface to sum at.

columnstrn/a

The column to sum. Complex is the point – a magnitude carries no phase to interfere with.

weightsmapping of int to complex, optionaln/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_tolerancefloat, optional-

Passed to align_surfaces_by_q.

angle_pointsint, 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 16 * n_max + 1.

Returns
—CompositeDriverad

peak and peak_angle are the largest real amplitude over the helical angle and where it occurs; quadrature is sqrt(sum |z|**2); linear_bound is sum |z|, the value the peak would reach if every mode aligned. peak, quadrature and linear_bound carry column’s own units and peak_angle is an angle.

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

  • ValueError No mode resonates at q, the column is missing from a resonating mode’s table, a contribution is not finite, angle_points is less than 2, or a weight names a mode that is not in tables.
Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Align the surfaces and keep the modes that resonate at q.
  2. Weight each mode’s contribution.
  3. Evaluate sum_n Re(z_n exp(i n u)) over u in [0, 2 pi), take the largest, and refine it by golden-section search.
  4. 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

  1. The size has to follow n_max because the sum is a trigonometric polynomial of that degree and has up to 2 n_max maxima. 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 at n_max = 20 – worst deficit 1.5e-3, on the two-mode case (14, 19) – and in 281 of 400 at n_max = 400, worst deficit 0.20. Sized from n_max and 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

  1. gpec_multimode_metrics.composite_drive_at_q, which returned abs(sum(z)) at the caller’s fixed phases and sum(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 is linear_bound. The angle the legacy swept was never identified as the helical one.
  2. The legacy’s own multi-n artefact records the phase convention it combined under: sample/gpec_optimization_examples/ q2p0_harmonic/gpec_multimode_summary_n12.json carries phase_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.

NameTypeUnitDescription
psi_normarray_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.

Applicability · Limitations · Provenance

Applicability

Machine-independent.

Limitations

Undefined for a single surface, which has no neighbour: nan.

Provenance

  1. 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.
  2. GPEC leaves its own w_isl_v_crit at 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.

NameTypeUnitDescription
couplingarray_liken/a

Response-by-control coupling, complex.

response_areaarray_like, optionalm^2

Area per response row. Unweighted when omitted.

control_areaarray_like, optionalm^2

Area per control column. Unweighted when omitted.

Returns
—DominantModen/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

  • ValueError The 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

  1. Check the areas before taking their square root.
  2. Weight rows and columns, decompose, map the leading pair back.

Applicability

Machine-independent.

Provenance

  1. 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.

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.

NameTypeUnitDescription
couplingarray_like-

Coupling matrix, modes by harmonics, complex.

external_fieldarray_likeT

The external field on the same harmonic basis, complex.

b_t0floatT

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.

normarray_like, optional-

An energy norm to apply to coupling first, from energy_norm_matrix. Omit it when the matrix is already normed – GPEC’s own C_xe is.

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

  • ValueError The shapes disagree, the coupling matrix or the field holds a non-finite entry, the coupling matrix is empty or identically zero, or b_t0 is not a positive finite real number.
Processing steps · Applicability · Limitations · Provenance

Processing steps

  1. Apply norm to the coupling matrix when one is given.
  2. Take its singular value decomposition.
  3. Project the field onto the conjugated right singular vectors.
  4. 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

  1. Conventions register C-25: the dominant singular vector needs explicit gauge fixing or the complex overlap phase is arbitrary.
  2. Reproduces GPEC’s own O_CPhi_xe to 3e-16 and its Phi_overlap_norm to 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.

NameTypeUnitDescription
eigenvaluesarray_like-

Eigenvalues of the energy matrix.

eigenvectorsarray_like-

Its eigenvectors, one per row, matching eigenvalues.

negative_eigenvaluesstrn/a

What to do with an eigenvalue at or below zero, one of NEGATIVE_EIGENVALUE_POLICIES. Required: the three constructions this replaces answered it three ways, and the answer changes the metric.

Returns
—ndarray-

W^{-1/2}, square and Hermitian.

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

  • ValueError negative_eigenvalues is not one of the three; the shapes disagree or eigenvectors is 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

  1. Apply the policy to the eigenvalues.
  2. Form V^H diag(lambda^{-1/2}) V over 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

  1. 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.

NameTypeUnitDescription
psi_normarray_like-

Radial grid, strictly increasing.

fieldarray_likeT

The resonant harmonic on that grid, complex.

rational_psi_normarray_like-

Where each layer is centred.

widthsarray_like-

Each layer’s full width.

Returns
—ndarrayT

d field / d psi at psi_s + w/2 minus at psi_s - w/2, one complex value per surface.

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

  • ValueError The 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

  1. Differentiate the field on its grid.
  2. Interpolate the derivative to each layer’s two edges.
  3. 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

  1. 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.

NameTypeUnitDescription
psi_normarray_like-

Island positions, possibly with repeats.

widtharray_like-

Full width of each.

policystr, optionaln/a

One of COINCIDENT_POLICIES: envelope takes the widest of a stack, rss adds them in quadrature.

psi_tolfloat, 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 psi_tol of the previous collapses together however far the chain reaches.

Returns
—tuple-

(psi_norm, width, members) – one entry per distinct surface, sorted outward, with members giving the input indices that went into each.

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

  • ValueError policy is not one of COINCIDENT_POLICIES, psi_tol is negative, or width is 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

  1. 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.

NameTypeUnitDescription
tablesmapping of int to mappingn/a

One resonant table per toroidal mode number.

qfloat-

The rational surface to sweep at.

columnstrn/a

The column to sum.

anglesarray_likerad

Helical angles to evaluate at.

weightsmapping of int to complex, optionaln/a

Per-mode complex weight, applied before the sum.

integrality_tolerancefloat, optional-

Passed to align_surfaces_by_q.

Returns
—ndarrayn/a

The real amplitude at each angle, in the order given. Units follow column.

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

  • ValueError As composite_drive_at_q, or an angle is not finite.
Processing steps · Applicability · Limitations · Provenance

Processing steps

  1. Align, weight and collect the contributions, as composite_drive_at_q does.
  2. 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

  1. 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.

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.

NameTypeUnitDescription
psi_normarray_like-

Island positions.

widtharray_like-

Full island width per surface.

separatrixfloat, optional-

Where the boundary is, which the width is measured in from.

policystr, optionaln/a

Passed to group_coincident_islands.

psi_tolfloat, optional-

Passed to group_coincident_islands.

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

  • ValueError separatrix is not finite, policy/psi_tol are invalid, or the islands do not pass island_pairs’ checks.
Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Collapse coincident surfaces under policy.
  2. Form the adjacent pairs and their gaps.
  3. If the outermost island does not reach separatrix, the width is zero and the region is not edge-connected.
  4. If the outermost pair does not touch, likewise.
  5. 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

  1. 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.

NameTypeUnitDescription
psi_normarray_like-

Rational-surface positions.

widtharray_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

  • 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 · Defaults · Applicability · Provenance

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.

Applicability

Machine-independent.

Provenance

  1. 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.

NameTypeUnitDescription
lundquistarray_like-

Lundquist number per rational surface.

modelJumpWidthModeln/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

  • ValueError A Lundquist number is not finite and positive.
Processing steps · Applicability · Provenance

Processing steps

  1. Check the Lundquist numbers are finite and positive.
  2. Apply the model’s law.

Applicability

Machine-independent.

Provenance

  1. 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 / alfven

The Lundquist number, the resistive over the Alfven time.

NameTypeUnitDescription
tau_resistivearray_likes

Resistive diffusion time per rational surface.

tau_alfvenarray_likes

Alfven time per rational surface.

Returns
—ndarray-

S = tau_r / tau_a per surface.

Raises

  • ValueError The two disagree in shape, or a time is not finite and positive.
Processing steps · Applicability · Provenance

Processing steps

  1. Check both times are finite, positive and the same shape.
  2. Divide.

Applicability

Machine-independent.

Provenance

  1. library/resistive_gpec_metrics.py::lundquist_number (branch codex/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 unsorted

Distance from each rational surface to its nearest neighbour.

NameTypeUnitDescription
psi_normarray_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 · Applicability · Provenance

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

  1. 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.

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.

NameTypeUnitDescription
psi_normarray_like-

Rational-surface positions.

widtharray_like-

Full island width per surface, in the same units.

Returns
—ndarray-

width / critical_island_width; one at overlap.

Convention

A ratio, not a verdict: the value one is where islands touch, and comparing against it is the caller’s to do.

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.
Applicability · Provenance

Applicability

Machine-independent.

Provenance

  1. 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.

NameTypeUnitDescription
tablesmapping of int to mappingn/a

One resonant table per toroidal mode number.

columnstrn/a

The column to sum.

weightsmapping of int to complex, optionaln/a

Per-mode complex weight.

integrality_tolerancefloat, optional-

Passed to align_surfaces_by_q.

shared_onlybool, optionaln/a

Keep only the surfaces every mode resonates at.

Returns
—dict of str to ndarrayn/a

Columns q, n_modes, peak, peak_angle, quadrature and linear_bound, plus psi_norm_<n> and m_<n> per mode (nan and -1 where that mode does not resonate). Rows ascend in q, and every aligned surface gets one.

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

  • ValueError As composite_drive_at_q, or shared_only leaves nothing.
Processing steps · Applicability · Limitations · Provenance

Processing steps

  1. Align the surfaces across the modes.
  2. Compute the composite drive at each.
  3. 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

  1. gpec_multimode_metrics.compute_q_composite_table, which built rows only for four hard-coded q values, 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; and sum_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.

NameTypeUnitDescription
valuesmapping of int to float-

One EdgeOverlap.delta_e per toroidal mode number.

combination{'quadrature', 'linear'}, optionaln/a

quadrature is sqrt(sum (w d)**2); linear is sum w d.

weightsmapping of int to float, optional-

Per-mode weight; every mode present in values needs one if any does.

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

  • ValueError values is empty, a value or weight is not finite or not real, a value is negative, the combination is not one of the two, or weights names a mode that values does not.
Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Weight each mode’s value.
  2. 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

  1. gpec_multimode_metrics.delta_e_weighted_norm, whose use_complex_sum=True branch took abs(sum(w * complex(v))). Its inputs are real by construction – delta_e_vector reads projected_overlap, a magnitude – so that branch equalled the linear sum and the “coherent” name promised a phase that had already been discarded upstream. It is linear here, under a name that claims no more than it does. Its companion delta_e_vector was a dictionary comprehension over an attribute name, with an unvalidated region returning None for 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.

NameTypeUnitDescription
psi_normarray_like-

Normalized poloidal flux of each rational surface.

valuesarray_likeany

One column of the resonant table, real or complex.

windowResonantWindow-

The interval to reduce over.

statisticstr, optionaln/a

One of RESONANT_STATISTICS.

Returns
—floatany

The reduced value, in the column’s own unit; nan when the window holds no surface.

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

  • ValueError psi_norm and values differ in length, or statistic is not one of RESONANT_STATISTICS.
Processing steps · Applicability · Limitations · Provenance

Processing steps

  1. Take the magnitude of a complex column; leave a real one signed.
  2. Select the surfaces inside window.
  3. 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

  1. 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.

NameTypeUnitDescription
psi_normarray_like-

Radial grid the field is stored on.

fieldarray_likeT

One harmonic of the Jacobian-weighted contravariant flux, J b . grad psi, on that grid – GPEC’s Jbgradpsi at m = m_pol. Complex.

psi_rationalfloat-

Where the surface is.

areafloatm^2

Area of that surface.

dq_dpsi_normfloat-

dq/dpsi_N at the surface.

chi1float-

The equilibrium’s poloidal flux normalization.

m_pol, n_torint-

The harmonic’s mode numbers, m_pol = n_tor * q.

offsetfloat, optional-

Where the jump is evaluated, as a fraction of 1 / (n |dq/dpsi_N|).

Returns
—ResonantJump-

delta is the unitless resonance parameter [-]; offset is the distance in psi_N the jump was taken over.

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

  • ValueError The grid and the field disagree in length, a value is not finite, the shear is zero, chi1 or 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

  1. Place the evaluation points at psi_rational +- offset / (n |q'|).
  2. Fit a cubic to the field on each side separately, over the band between half and four times that distance, and differentiate it.
  3. 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

  1. GPEC/gpec/gpout.f:1678-1688, which evaluates bwp1_mn at respsi -+ sing_spot/(nn |q1|) and forms delta = (rbwp1mn - lbwp1mn) / (twopi * chi1). The factor of area here is the singflx_mn/area(ising) normalization applied at :1705, which the stored Jbgradpsi already 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.

NameTypeUnitDescription
deltaarray_like-

Resonance parameter per rational surface, complex.

phi_resarray_likeT

Pitch-resonant flux per rational surface, complex.

n_torint-

Toroidal mode number the two were computed at.

Returns
—ndarrayT

G per surface, real and positive.

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

  • ValueError The two disagree in length, a value is not finite, a delta is 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

  1. Form -n * Phi_res / Delta per surface.
  2. 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

  1. The chain this collapses is GPEC/gpec/gpout.f:1690-1705: I_res from Delta through j_c, then Phi_res from I_res through fsurf_indmats, which gpvacuum.f:236-342 builds 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.

NameTypeUnitDescription
tablemapping of str to ndarrayany

A resonant table, as vaft.code.gpec.GpecProfileOutput.resonant_table returns it; it must carry psi_n_rational.

windowsmapping of str to ResonantWindow-

The windows to reduce over, from resonant_windows.

columnssequence of str, optionaln/a

Which columns to reduce; those of RESONANT_RESPONSE_COLUMNS the table carries, by default. Anything else – a coordinate, or an equilibrium profile sampled at the surfaces – has to be named, so that reducing it is a decision rather than a side effect.

statisticstr, optionaln/a

One of RESONANT_STATISTICS.

Returns
—dictany

Keyed (column, window), in the column’s own unit.

Convention

Inherits reduce_resonant’s – magnitudes for complex columns, the column’s own unit – and the window convention of whatever resonant_windows produced.

Raises

  • KeyError The table carries no psi_n_rational, no response column at all, or not one that columns named.
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

  1. 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.

NameTypeUnitDescription
pedestalPedestalTop, optional-

The pedestal boundary, from vaft.process.profile.pedestal_top. Omit it only together with legacy=True.

legacybool, optional-

Return LEGACY_WINDOWS instead, for reproducing a published number that used them.

Returns
—dict of str to ResonantWindow-

Keyed core, edge, total, each carrying its own bounds and the source they were derived from.

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

  • ValueError Neither a pedestal nor legacy=True was given, or the pedestal’s coordinate is not psi_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

  1. 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.
  2. 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.

NameTypeUnitDescription
psi_normarray_like-

Normalized poloidal flux of each rational surface.

phi_resarray_likeT

Resonant flux per surface, complex as GPEC writes it.

windowResonantWindow-

The interval to reduce over.

Returns
—floatT

RMS magnitude, in tesla; nan when the window holds no surface.

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

  1. gpec_analysis_metrics.compute_rms_resonant_field, whose to_gauss=True default 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 @ jump

The field a finite-width layer’s shielding current produces.

NameTypeUnitDescription
deltaarray_likeT

The derivative jump per surface, from finite_width_delta, complex.

self_inductancearray_liken/a

The surfaces’ self-inductance matrix, square and matching delta. Required.

Returns
—ndarrayT

L @ delta, complex.

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

  • ValueError The matrix is not square, does not match delta, or holds a non-finite value.
Processing steps · Applicability · Provenance

Processing steps

  1. Check the matrix matches the surfaces.
  2. Multiply.

Applicability

Machine-independent.

Provenance

  1. 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.

NameTypeUnitDescription
storedarray_like of complexany

The harmonic as the file holds it, shape (P, K) for P positions and K vector components.

sampledarray_like of floatany

The same physical field evaluated independently at those P positions and M toroidal angles, shape (P, M, K), in the units of stored.

phi_radarray_likerad

The M toroidal angles sampled was evaluated at, equally spaced and covering one full period without repeating its endpoint.

n_torint-

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

  • ValueError The three arrays disagree in shape, any of them holds a non-finite value, phi_rad is not one equally spaced period, it carries too few samples to resolve n_tor, n_tor is not a non-zero integer, or stored is identically zero so no relative norm exists.
Processing steps · Assumptions · Applicability · Limitations · Provenance

Processing steps

  1. Project the sampled field onto the mode: C_measured = 2 * mean_phi(B(phi) e^{+i n phi}).
  2. 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.
  3. 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

  1. 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.

results matching ""

    No results matching ""