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 > 0Mercier unstable,D_R > 0resistive-interchange unstable,C_A < 0high-n ballooning unstable). A criterion the run did not evaluate staysNone, never a marginal zero.dcon_edge_scananddcon_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_intervals | Where a radial criterion profile is strictly on its unstable side. |
dcon_edge_comparison | Full-edge minus peak-dW-truncated least-stable W_t of one case (#792, #940). Convention |
dcon_edge_scan | DCON’s edge scan: where Re dW_edge peaks, where it changes sign, where DCON truncated. Convention |
dcon_local_stability | DCON’s three local criteria, located: Mercier, resistive interchange, ballooning. Convention |
load_stability_atlas | Read the Tier A stability atlas tables from their directory. |
stability_atlas_populations | The ideal, resistive and local-criterion populations of the stability atlas. Convention |
No function matches.
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.
| Name | Type | Unit | Description |
|---|---|---|---|
psi_n | array_like | - | Normalized poloidal flux of each sample, increasing. |
values | array_like | criterion units | The criterion at each sample. |
unstable_side | {"positive", "negative"} | - | Which sign is unstable; zero is marginal and never counts as unstable. |
evaluated | array_like of bool or None, optional | - | Samples the solver actually computed; |
| Returns | |||
result | dict | - | The located unstable side of the profile, keyed as below. |
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).
| Name | Type | Unit | Description |
|---|---|---|---|
full | mapping | - | The full-edge row of |
truncated | mapping | - | The peak-dW-truncated row of the same case and n. |
| Returns | |||
result | dict | - |
|
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
- Issue #792, the full-edge vs truncated comparison.
- Issue #940,
DconEdgeAnalysisfollow-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.
| Name | Type | Unit | Description |
|---|---|---|---|
row | mapping | - | One row of |
| Returns | |||
result | dict or None | - |
|
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
- 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 outDCON’s three local criteria, located: Mercier, resistive interchange, ballooning.
| Name | Type | Unit | Description |
|---|---|---|---|
row | mapping | - | One row of |
| Returns | |||
result | dict | - | One located criterion per DCON local criterion, keyed as below. |
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
- 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.
| Name | Type | Unit | Description |
|---|---|---|---|
directory | str or Path | - | The atlas directory holding |
| Returns | |||
atlas_n | pandas.DataFrame | table | One row per (slice, n). |
atlas_surfaces | pandas.DataFrame | table | One row per (slice, n, solver, rational surface). |
Raises
FileNotFoundErrorEither 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.
| Name | Type | Unit | Description |
|---|---|---|---|
atlas_n | pandas.DataFrame | table | The atlas’s per-(slice, n) table: |
atlas_surfaces | pandas.DataFrame | table | The atlas’s per-surface table: |
| Returns | |||
populations | StabilityAtlasPopulations | - | 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
- Ideal:
W_t(full edge, mpsi 512) of every row, grouped byn. - Resistive: per slice, solver and
n, the largestRe Delta'at mpsi 512 over all rational surfaces. - Local:
max D_Iandmin C_Aof each slice’sn = 1row, where the run evaluated them (the atlas’s reference DCON run, mpsi 256). - 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
- Issue #1429, the Tier A stability atlas; layers decided there.
- Issue #1852, these population histograms.