Process reference: mhd_stability

Generated from the docstrings of vaft.process.mhd_stability: 6 public functions. Each entry is what vaft.process.describe("mhd_stability.<name>") prints. All process categories

Overview

The inputs are the rows of vaft.machine_mapping.mhd_linear.extract_dcon_stability, so everything here works from the final ODS without reopening DCON’s own files (#940’s scientific-closure contract).

Three questions, kept apart:

  • criterion_intervals: where a local criterion profile is on its unstable side, as intervals in psi_N with interpolated zero crossings. Pure numerics, no solver knowledge.
  • dcon_local_stability: that, applied to DCON’s three local criteria with DCON’s sign conventions (D_I > 0 Mercier unstable, D_R > 0 resistive-interchange unstable, C_A < 0 high-n ballooning unstable). A criterion the run did not evaluate stays None, never a marginal zero.
  • dcon_edge_scan and dcon_edge_comparison: DCON’s edge scan, its peak and truncation, and the full-edge minus truncated W_t of one case.

None of these is a verdict on the plasma. They locate where a solver’s criterion is met; the stability atlas’s interpretation layer decides what that means (#939).

Functions

criterion_intervalsWhere a radial criterion profile is strictly on its unstable side.
dcon_edge_comparisonFull-edge minus peak-dW-truncated least-stable W_t of one case (#792, #940). Convention
dcon_edge_scanDCON’s edge scan: where Re dW_edge peaks, where it changes sign, where DCON truncated. Convention
dcon_local_stabilityDCON’s three local criteria, located: Mercier, resistive interchange, ballooning. Convention
load_stability_atlasRead the Tier A stability atlas tables from their directory.
stability_atlas_populationsThe ideal, resistive and local-criterion populations of the stability atlas. Convention

criterion_intervals

Machine-independent source
criterion_intervals(psi_n, values, *, unstable_side, evaluated=None)

Where a radial criterion profile is strictly on its unstable side.

NameTypeUnitDescription
psi_narray_like-

Normalized poloidal flux of each sample, increasing.

valuesarray_likecriterion units

The criterion at each sample.

unstable_side{"positive", "negative"}-

Which sign is unstable; zero is marginal and never counts as unstable.

evaluatedarray_like of bool or None, optional-

Samples the solver actually computed; None means all of them.

Returns
resultdict-

The located unstable side of the profile, keyed as below. unstable_intervals: list of [start, end] in psi_N [-], one per run of consecutive unstable samples, each end moved to the interpolated zero crossing when the neighbouring sample is evaluated and on the stable side, otherwise left at the last unstable sample. zero_crossings: psi_N of every sign change between two adjacent evaluated samples, linearly interpolated [-]. unstable_fraction: unstable samples over evaluated samples [-]. extremum and psi_n_at_extremum: the most unstable value and its location [criterion units, -]; the largest value for positive, the smallest for negative. n_evaluated [-]. Every entry is None or empty when no sample is evaluated.

Assumptions · Applicability · Limitations

Assumptions

psi_n increases. Unevaluated or non-finite samples break an interval: nothing is interpolated across them.

Applicability

Machine-independent.

Limitations

Linear interpolation between samples; a sign change and back between two samples is invisible. The resolution is the solver’s radial grid.

dcon_edge_comparison

Machine-independentConvention-sensitive source
dcon_edge_comparison(full, truncated)
Show source vaft/process/mhd_stability.py, lines 375–432
def dcon_edge_comparison(full: Mapping[str, Any], truncated: Mapping[str, Any]) -> dict[str, Any]:
    """Full-edge minus peak-dW-truncated least-stable W_t of one case (#792, #940).

    Parameters
    ----------
    full : mapping
        The full-edge row of
        :func:`vaft.machine_mapping.mhd_linear.extract_dcon_stability` [-].
    truncated : mapping
        The peak-dW-truncated row of the same case and n [-].

    Returns
    -------
    result : dict
        ``n_tor`` [-]; ``W_t_full``, ``W_t_truncated`` and ``delta_W_edge`` =
        ``Re W_t_full - Re W_t_truncated`` [normalized, not Joules];
        ``sign_agreement``: whether both real parts have the same strict sign
        [-]; ``psilim_full``, ``psilim_truncated`` [-].

    Convention
    ----------
    The difference is of two separate solutions, each with its own boundary:
    DCON's full re-run after truncation discards the full-edge eigenvectors, so
    nothing here is a mode-by-mode comparison. Both energies share DCON's
    normalization only because they come from the same equilibrium and n.

    Applicability
    -------------
    Machine-independent.

    Limitations
    -----------
    The rows must be the same case: same ``n_tor`` and treatments
    ``full_edge``/``peak_dw_truncated``; anything else raises. Same equilibrium
    is the caller's to ensure (the payload has no equilibrium hash).

    Provenance
    ----------
    .. [792] Issue #792, the full-edge vs truncated comparison.
    .. [940] Issue #940, ``DconEdgeAnalysis`` follow-up.
    """
    if full.get("edge_treatment") != "full_edge" or truncated.get("edge_treatment") != "peak_dw_truncated":
        raise ValueError("expected one full_edge and one peak_dw_truncated row, got "
                         f"{full.get('edge_treatment')!r} and {truncated.get('edge_treatment')!r}")
    if full.get("n_tor") != truncated.get("n_tor"):
        raise ValueError(f"rows are for different n: {full.get('n_tor')} vs {truncated.get('n_tor')}")
    a, b = full.get("W_t"), truncated.get("W_t")
    delta = None if a is None or b is None else float(np.real(a) - np.real(b))
    agree = None if delta is None else bool(np.real(a) * np.real(b) > 0)
    return {
        "n_tor": full.get("n_tor"),
        "W_t_full": a,
        "W_t_truncated": b,
        "delta_W_edge": delta,
        "sign_agreement": agree,
        "psilim_full": full.get("psilim"),
        "psilim_truncated": truncated.get("psilim"),
    }

Full-edge minus peak-dW-truncated least-stable W_t of one case (#792, #940).

NameTypeUnitDescription
fullmapping-

The full-edge row of vaft.machine_mapping.mhd_linear.extract_dcon_stability.

truncatedmapping-

The peak-dW-truncated row of the same case and n.

Returns
resultdict-

n_tor [-]; W_t_full, W_t_truncated and delta_W_edge = Re W_t_full - Re W_t_truncated [normalized, not Joules]; sign_agreement: whether both real parts have the same strict sign [-]; psilim_full, psilim_truncated.

Convention

The difference is of two separate solutions, each with its own boundary: DCON’s full re-run after truncation discards the full-edge eigenvectors, so nothing here is a mode-by-mode comparison. Both energies share DCON’s normalization only because they come from the same equilibrium and n.

Applicability · Limitations · Provenance

Applicability

Machine-independent.

Limitations

The rows must be the same case: same n_tor and treatments full_edge/peak_dw_truncated; anything else raises. Same equilibrium is the caller’s to ensure (the payload has no equilibrium hash).

Provenance

  1. Issue #792, the full-edge vs truncated comparison.
  2. Issue #940, DconEdgeAnalysis follow-up.

dcon_edge_scan

Machine-independentConvention-sensitive source
dcon_edge_scan(row)

DCON’s edge scan: where Re dW_edge peaks, where it changes sign, where DCON truncated.

NameTypeUnitDescription
rowmapping-

One row of vaft.machine_mapping.mhd_linear.extract_dcon_stability (edge_scan, requested_psiedge, psilim, qlim).

Returns
resultdict or None-

None for a full-edge run (no scan). Otherwise: psi_n_at_peak, q_at_peak, dW_at_peak: the maximum of Re dW_edge over the filled scan entries [-, -, normalized]; psilim, qlim: the truncation DCON used [-]; truncated_at_peak: whether psilim equals the peak’s psi_N within 1e-9; None when psilim is unknown or when the two ends of peak_search_start_bracket disagree on it [-]; zero_crossings_psi_n: psi_N where Re dW_edge changes sign [-]; negative_intervals: psi_N intervals with Re dW_edge < 0 [-]; n_points [-]; peak_search_start: index of the first searched entry [-]; peak_search_start_ambiguous: whether a nominal grid point lies so close to the estimated q(psiedge) that the payload cannot pin the start to one entry [-]; peak_search_start_bracket: the sorted starts the estimate’s tolerance admits (one entry when the start is pinned) [-]; psi_n_at_peak_bracket: the peak’s psi_N from each start of the bracket [-]; q_edge_start: DCON’s qedgestart recovered from the scan.

Convention

dW_edge is DCON’s least-stable total energy against the edge truncation, normalized as W_t is (not Joules). Negative is unstable. The peak is taken as dcon.F:253 does, MAXLOC(REAL(dw_edge(pre_edge:i_edge))): from pre_edge on and over filled entries only (unfilled entries keep psi_edge = 0, sing.f:232). pre_edge skips the nominal grid points INT(q(psiedge)) + i/(nperq_edge*n) below q(psiedge) (sing.f:226-238); those entries are filled but never searched. The zero crossings and negative intervals cover every filled entry.

Applicability · Limitations · Provenance

Applicability

Machine-independent.

Limitations

One scan per run; DCON keeps only the truncated solution’s eigenvectors. The payload has no q profile, so q(psiedge) is extrapolated from the first two filled entries; when a nominal grid point falls inside that first ODE step the start is flagged ambiguous rather than guessed silently, the search is run from both ends of the bracket (as vaft.validation.stability.validate_dcon does), and truncated_at_peak is None rather than a bool when the ends disagree. psi_n_at_peak and its q and dW are always those of the point estimate. nperq_edge is DCON’s default 20, which VAFT’s templates never change. Raises ValueError without a positive n_tor.

Provenance

  1. Issue #940, section 10 (edge-scan post-processing).

dcon_local_stability

Machine-independentConvention-sensitive source
dcon_local_stability(row)
Show source vaft/process/mhd_stability.py, lines 151–218
def dcon_local_stability(row: Mapping[str, Any]) -> dict[str, Optional[dict[str, Any]]]:
    """DCON's three local criteria, located: Mercier, resistive interchange, ballooning.

    Parameters
    ----------
    row : mapping
        One row of :func:`vaft.machine_mapping.mhd_linear.extract_dcon_stability`
        (``psi_n``, ``D_I``, ``D_R``, ``C_A``, ``mercier_evaluated``,
        ``ballooning_evaluated``) [-].

    Returns
    -------
    result : dict
        One located criterion per DCON local criterion, keyed as below [-].

        ``{"mercier": ..., "resistive_interchange": ..., "ballooning": ...}``, each
        the :func:`criterion_intervals` result for ``D_I`` (unstable > 0),
        ``D_R`` (unstable > 0) and ``C_A`` (unstable < 0) [-], plus
        ``criterion`` and ``unstable_side``. ``None`` for a criterion the run
        did not evaluate or whose profile is absent.

    Convention
    ----------
    DCON's signs (``mercier.f``, ``bal.f``): ``D_I > 0`` Mercier unstable,
    ``D_R > 0`` resistive-interchange criterion unstable, ``C_A < 0`` high-n
    ideal ballooning unstable. ``C_A`` is read only where the payload marks it
    evaluated (``C_A_evaluated``), so an unevaluated surface is skipped, not a
    marginal zero. DCON leaves an unevaluated surface at exactly 0, its marginal
    value, so the mask is required: a ``C_A`` profile without a mask of its
    length is refused (``ValueError``), never read as if every surface were
    evaluated.

    Applicability
    -------------
    Machine-independent.

    Limitations
    -----------
    The payload carries no per-surface mask for ``D_I``/``D_R``; ``mer_flag``
    evaluates all of them, so the flag alone decides. A profile whose length
    differs from ``psi_n`` is ``None``, as the payload's own summaries treat it.
    Every payload :func:`vaft.machine_mapping.mhd_linear.extract_dcon_stability`
    reads carries the mask (``read_dcon_output`` sets it whenever ``C_A`` is
    present); only a hand-built row can lack it.

    Provenance
    ----------
    .. [940] Issue #940, section 8 (local-stability post-processing).
    """
    psi = row.get("psi_n")
    flags = {"D_I": row.get("mercier_evaluated"), "D_R": row.get("mercier_evaluated"),
             "C_A": row.get("ballooning_evaluated")}
    names = {"D_I": "mercier", "D_R": "resistive_interchange", "C_A": "ballooning"}
    masks = {"C_A": row.get("C_A_evaluated")}
    out: dict[str, Optional[dict[str, Any]]] = {}
    for criterion, name in names.items():
        values = row.get(criterion)
        if flags[criterion] is not True or values is None or psi is None or np.size(values) != np.size(psi):
            out[name] = None
            continue
        mask = masks.get(criterion)
        if criterion in masks and (mask is None or np.size(mask) != np.size(values)):
            raise ValueError(f"{criterion} without a {criterion}_evaluated mask of its length: a stored zero "
                             "cannot be told from a marginal surface")
        side = DCON_UNSTABLE_SIDE[criterion]
        out[name] = {"criterion": criterion, "unstable_side": side,
                     **criterion_intervals(psi, values, unstable_side=side, evaluated=mask)}
    return out

DCON’s three local criteria, located: Mercier, resistive interchange, ballooning.

NameTypeUnitDescription
rowmapping-

One row of vaft.machine_mapping.mhd_linear.extract_dcon_stability (psi_n, D_I, D_R, C_A, mercier_evaluated, ballooning_evaluated).

Returns
resultdict-

One located criterion per DCON local criterion, keyed as below. {"mercier": ..., "resistive_interchange": ..., "ballooning": ...}, each the criterion_intervals result for D_I (unstable > 0), D_R (unstable > 0) and C_A (unstable < 0) [-], plus criterion and unstable_side. None for a criterion the run did not evaluate or whose profile is absent.

Convention

DCON’s signs (mercier.f, bal.f): D_I > 0 Mercier unstable, D_R > 0 resistive-interchange criterion unstable, C_A < 0 high-n ideal ballooning unstable. C_A is read only where the payload marks it evaluated (C_A_evaluated), so an unevaluated surface is skipped, not a marginal zero. DCON leaves an unevaluated surface at exactly 0, its marginal value, so the mask is required: a C_A profile without a mask of its length is refused (ValueError), never read as if every surface were evaluated.

Applicability · Limitations · Provenance

Applicability

Machine-independent.

Limitations

The payload carries no per-surface mask for D_I/D_R; mer_flag evaluates all of them, so the flag alone decides. A profile whose length differs from psi_n is None, as the payload’s own summaries treat it. Every payload vaft.machine_mapping.mhd_linear.extract_dcon_stability reads carries the mask (read_dcon_output sets it whenever C_A is present); only a hand-built row can lack it.

Provenance

  1. Issue #940, section 8 (local-stability post-processing).

load_stability_atlas

VEST-specific source
load_stability_atlas(directory)
Show source vaft/process/mhd_stability.py, lines 469–509
def load_stability_atlas(directory) -> tuple[Any, Any]:
    """Read the Tier A stability atlas tables from their directory.

    Parameters
    ----------
    directory : str or Path
        The atlas directory holding ``atlas_n.csv`` and ``atlas_surfaces.csv``
        (on vestserver ``~/runs/campaign/atlas/stability``) [-].

    Returns
    -------
    atlas_n : pandas.DataFrame
        One row per (slice, n) [table].
    atlas_surfaces : pandas.DataFrame
        One row per (slice, n, solver, rational surface) [table].

    Raises
    ------
    FileNotFoundError
        Either table is missing.

    Applicability
    -------------
    VEST-specific. The atlas is the VEST Tier A campaign product of
    ``workflow/stability_atlas/build_atlas.py``.

    Limitations
    -----------
    Reads the CSV form; no schema check beyond the columns
    :func:`stability_atlas_populations` uses.
    """
    import pandas as pd

    root = Path(directory).expanduser()
    tables = []
    for name in ("atlas_n.csv", "atlas_surfaces.csv"):
        path = root / name
        if not path.is_file():
            raise FileNotFoundError(f"stability atlas table {path} not found")
        tables.append(pd.read_csv(path))
    return tables[0], tables[1]

Read the Tier A stability atlas tables from their directory.

NameTypeUnitDescription
directorystr or Path-

The atlas directory holding atlas_n.csv and atlas_surfaces.csv (on vestserver ~/runs/campaign/atlas/stability).

Returns
atlas_npandas.DataFrametable

One row per (slice, n).

atlas_surfacespandas.DataFrametable

One row per (slice, n, solver, rational surface).

Raises

  • FileNotFoundError Either table is missing.
Applicability · Limitations

Applicability

VEST-specific. The atlas is the VEST Tier A campaign product of workflow/stability_atlas/build_atlas.py.

Limitations

Reads the CSV form; no schema check beyond the columns stability_atlas_populations uses.

stability_atlas_populations

VEST-specificConvention-sensitive source
stability_atlas_populations(atlas_n, atlas_surfaces)

The ideal, resistive and local-criterion populations of the stability atlas.

NameTypeUnitDescription
atlas_npandas.DataFrametable

The atlas’s per-(slice, n) table: dcon_full_512_W_t, dcon_full_sign_class, max_D_I, min_C_A, mercier_evaluated, ballooning_evaluated, keyed by shot, time_efit_s, efit_lineage, n_tor.

atlas_surfacespandas.DataFrametable

The atlas’s per-surface table: solver, n_tor, delta_prime_mpsi512, two_resolution_consistent with the same key.

Returns
populationsStabilityAtlasPopulations-

Raw values per panel and series, with the per-series counts table.

Convention

Unstable is W_t < 0 (DCON’s energy sign; no |W_t| band), Delta'_max > 0 (a classical tearing drive at any rational surface), D_I > 0 (Mercier) and C_A < 0 (high-n ballooning). QA for W_t is the atlas sign class (robust_*: the sign agrees between mpsi 256 and 512); for Delta'_max it is that the maximizing surface is two-resolution consistent.

Processing steps · Input semantics · Output semantics · Applicability · Limitations · Provenance

Processing steps

  1. Ideal: W_t (full edge, mpsi 512) of every row, grouped by n.
  2. Resistive: per slice, solver and n, the largest Re Delta' at mpsi 512 over all rational surfaces.
  3. Local: max D_I and min C_A of each slice’s n = 1 row, where the run evaluated them (the atlas’s reference DCON run, mpsi 256).
  4. Counts: per series the slices, the QA-passing slices and those on the unstable side of the universal criterion.

Input semantics

The atlas as build_atlas.py writes it (v2): raw solver values plus QA annotations.

Output semantics

Raw values with no QA cut (mpsi 512 for W_t and Delta', mpsi 256 for the local criteria), and a separate counts table. The QA counts are a trust annotation; the unstable counts apply only the universal criteria. Neither changes a value.

Applicability

VEST-specific. The VEST Tier A stability atlas (#1429).

Limitations

Delta'_max is a single number per slice and hides the surface it came from; the atlas’s per-surface table keeps it. W_t is normalized (DCON’s units), comparable within one n and equilibrium only by sign.

Provenance

  1. Issue #1429, the Tier A stability atlas; layers decided there.
  2. Issue #1852, these population histograms.

results matching ""

    No results matching ""