Process reference: onset

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

Overview

Every detector here answers one question about one waveform – when does it become active, and on what evidence – and returns an OnsetRecord. Nothing here knows what the waveform is. The diagnostic preprocessing, the choice of which signal is authoritative for a plasma onset and the verdicts built on top live in vaft.omas.plasma_timing and vaft.validation.

Three ideas, kept separable because a study of the VEST raw database showed each does a different job. The study is workflow/plasma_onset/scan_corpus.py and its table test/data/onset_corpus.json (its summary carries the counts); the figures quoted below are from the hand-reviewed first pass that motivated the rules:

  • threshold – baseline + max(fraction * peak, sigma * robust_sigma). The fraction-of-peak term makes the boundary independent of a channel’s noise floor (two H-alpha channels with a ten-fold noise difference agree to 0.2 ms); the sigma term keeps a weak record from being thresholded at its own noise.
  • persistence – the signal must stay above threshold for hold_s. 0.5 ms removes the coil-firing pickup spikes from plasma-current records (1 false onset in 84 shots, from 51) and most isolated optical spikes.
  • morphology – width, prominence and integral of the accepted run. Width is the one that matters on that corpus: the residual false onsets after persistence are 0.5-1 ms flashes on failed breakdowns and the noisy burst an ECH switch-on induces in every channel; min_width_s = 1 ms removes them (1 in 51 non-plasma shots, and that one is genuine light).

For a pulse-shaped record principal_pulse_onset walks back from the global maximum, so an isolated excursion is unreachable by construction; its one weakness is a record with no pulse, where the maximum is the pickup – pickup_scale measures the record’s own impulsive excursions and a floor on the pulse amplitude relative to them refuses that case.

Two contracts every consumer can rely on: OnsetRecord.time is always a sample of the input grid (or None), which is what lets a half-open window [start, onset) exclude exactly the samples from the onset onward; and no onset is never the whole record – a detector that finds nothing says so with time=None and a flag, never by returning the record bounds.

Functions

active_windowThe window over which a waveform is active, onset to offset. Convention
excess_thresholdBaseline, spread, peak and activity threshold of one waveform.
isolated_excursionsHow many runs above a threshold are shorter than a given length.
median_smoothMedian filter that removes excursions shorter than half the kernel. Convention
pickup_scaleThe record’s own impulsive-excursion amplitude. Convention
principal_pulse_onsetOnset of the pulse that contains the global maximum. Convention
principal_pulse_window(onset, offset) of the pulse holding the global maximum.
robust_baselineMedian and robust sigma (1.4826 * MAD) over the reference samples.
robust_peakThe representative peak of a record: its largest sustained excursion, not its loudest sample. Convention
run_featuresWidth, peak, prominence and integral of one run above a baseline. Convention
sustained_excess_onsetFirst run above the threshold that persists and has the right shape. Convention
zero_crossing_after_excursionFirst sample after an anchored excursion where the record changes sign. Convention
zero_phase_lowpassForward-backward Butterworth low-pass: no group delay. Convention

active_window

Machine-independentConvention-sensitive source
active_window(time, values, *, fraction=0.02, sigma=5.0, hold_s=0.0005, min_width_s=0.0, min_prominence_sigma=0.0, min_integral_fraction=0.0, gap_s=0.001, post_quiet_s=0.002, principal_only=False, pickup_floor=0.0, impulse_max_s=0.002, reference_mask=None, reference_fraction=0.2, trailing_fraction=0.1, trailing_max_fraction=0.1, end_fraction=None, collapse_fallback=True, collapse_rate_fraction=0.1, collapse_min_drop=0.5, search_mask=None, prefilter_samples=1, cutoff_hz=None, fs=None, order=4)

The window over which a waveform is active, onset to offset.

The detector behind every plasma window in VAFT: what the onset detectors do for the start, plus the harder half, which is where a pulse ends.

NameTypeUnitDescription
timearray_likes

Time grid of the record.

valuesarray_likeany

The waveform.

fractionfloat, optional-

Fraction-of-peak term of the onset threshold.

sigmafloat, optional-

Robust-sigma term of the onset threshold.

hold_sfloat, optionals

How long a run must stay above the threshold to be a segment.

min_width_sfloat, optionals

Shortest accepted segment; 0 disables the test.

min_prominence_sigmafloat, optional-

How far a segment must rise above its surroundings, in robust sigmas; 0 disables the test.

min_integral_fractionfloat, optional-

Fraction of the record’s total positive excess a segment must carry; 0 disables the test.

gap_sfloat, optionals

Longest dip below the threshold that does not split a segment.

post_quiet_sfloat, optionals

A segment beginning within this of the previous one’s end is merged into it.

principal_onlybool, optional-

Keep only the segment holding the global maximum.

pickup_floorfloat, optional-

Principal-segment floor relative to pickup_scale; 0 disables the test.

impulse_max_sfloat, optionals

Longest run still treated as an impulse.

reference_maskarray_like of bool or None, optional-

Samples that define the quiet reference; None selects the leading reference_fraction of the record.

reference_fractionfloat, optional-

Fraction of the record used as the reference when no mask is given.

trailing_fractionfloat, optional-

Fraction of the record at its end used as the trailing reference.

trailing_max_fractionfloat, optional-

How far above the leading baseline that trailing level may sit, as a fraction of the peak, for it to be used at all.

end_fractionfloat or None, optional-

Fraction of the peak the signal must fall below, above the trailing level, for the window to end; fraction when None.

collapse_fallbackbool, optional-

Whether to end a window that never falls below the end threshold at the last steep fall.

collapse_rate_fractionfloat, optional-

How steep that fall must be, as a fraction of the steepest fall in the record.

collapse_min_dropfloat, optional-

Fraction of the level present before the fall that it must remove.

search_maskarray_like of bool or None, optional-

Samples the detector may look at; the peak that scales the threshold is taken inside it too. None is the whole record.

prefilter_samplesint, optional-

Median-filter kernel applied first; 1 means no filtering.

cutoff_hzfloat or None, optionalHz

Zero-phase low-pass cutoff; None leaves the record unfiltered.

fsfloat or None, optionalHz

Sample rate; inferred from time when None.

orderint, optional-

Butterworth order of that low-pass.

Returns
—PulseWindows

onset and offset records, both on samples of the input grid, the accepted segments in time order, and the flags that say what the window is not: offset_at_record_end (still active at the last sample), onset_at_record_start, multiple_segments, offset_from_collapse, trailing_segment_dropped, offset_threshold_above_peak.

Convention

offset.time is the last sample of the pulse – the last sample above the end threshold, or, with offset_from_collapse, the last sample of the quench that ended it – so a half-open consumer window is [onset.time, time[offset.index + 1]). A window is never assumed: nothing here reports the record bounds when it found no pulse.

Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Median-filter over prefilter_samples, low-pass (zero-phase) when a cutoff_hz is given and the grid can carry it.
  2. Threshold as excess_threshold against the leading reference.
  3. Bridge dips shorter than gap_s; keep the runs that pass persistence (hold_s) and morphology as segments; merge a segment that begins within post_quiet_s of the previous one’s end into it, so a brief quiet moment does not end the window while a real gap does.
  4. With principal_only, keep only the segment holding the global maximum, guarded by the impulsive-run and pickup_floor rules of principal_pulse_onset; otherwise the window is the envelope of every segment and multiple_segments says when there is more than one.
  5. Judge the offset against a trailing reference – the last trailing_fraction of the record, used only when it is quiet (spread within three times the leading one) and sits below trailing_max_fraction of the peak – at end_fraction of the peak above it. Where that end threshold is never crossed and collapse_fallback is on, end the window at the end of the last steep fall after the peak (collapse_rate_fraction, collapse_min_drop) and flag offset_from_collapse.

The baseline after a pulse need not be the one before it: a VEST plasma-current record settles a few percent of its peak above zero once the plasma is gone, and after a termination the Rogowski keeps reading the induced vessel current – a few to ten percent of the peak, decaying over tens of milliseconds – which no level between it and the trailing baseline separates from plasma. The collapse does. The last fall, not the steepest: a plasma survives a mid-pulse drop and terminates later.

Defaults

The signature values are numerical convenience; trailing_fraction, trailing_max_fraction, collapse_rate_fraction and collapse_min_drop are empirical, fixed on the hand-reviewed VEST discharges and then checked against the whole corpus [corpus]. The VEST values a pipeline actually runs with are policy [vest.yaml].

Applicability

Machine-independent.

Limitations

The end rule is the weak one. A disruption’s vessel-current tail can sit at a third of the peak for a hundred milliseconds, above any end threshold that still separates plasma from a normal termination, and is then ended by the collapse fallback rather than by the threshold. The threshold is scaled from the amplitude the detector accepted – the largest segment, not the largest sample in the search stretch – because a spike the segment tests refused would otherwise set the level the pulse has to fall below (#726).

And the window is the envelope of its segments, which for a record of two brief blips tens of milliseconds apart is mostly gap: three corpus records report windows that are 7 to 19 % above threshold. multiple_segments says a window has gaps but not how much of it is gap, and the extent is consumed as a duration – tracked in issue #752.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.
  3. The VEST values for these rules are policy, not defaults: vaft/machine_mapping/vest.yaml carries them in plasma_timing, discharge_timing and plasma_features, resolved by vaft.machine_mapping.utils and passed in by vaft.omas.plasma_timing, vaft.omas.discharge_timing and vaft.omas.plasma_features.

excess_threshold

Machine-independent source
excess_threshold(values, reference_mask, *, fraction, sigma, search_mask=None)

Baseline, spread, peak and activity threshold of one waveform.

The threshold is baseline + max(fraction * peak, sigma * robust_sigma). The fraction-of-peak term makes the boundary independent of a channel’s noise floor – two VEST H-alpha channels with a ten-fold noise difference agree to 0.2 ms on it – and the sigma term keeps a weak record from being thresholded at its own noise.

NameTypeUnitDescription
valuesarray_likeany

The waveform.

reference_maskarray_like of bool or None-

Samples that define the quiet reference, passed to robust_baseline.

fractionfloat-

Fraction of the peak excess the threshold sits at.

sigmafloat-

Number of robust sigmas the threshold sits at.

search_maskarray_like of bool or None, optional-

Samples the detector may look at; the peak that scales the threshold is taken inside it too. None is the whole record.

Returns
baselinefloatany

Median of the reference samples.

robust_sigmafloatany

Robust sigma of the reference samples.

peakfloatany

Largest excess over the baseline inside the search mask, nan when the baseline is not finite or the mask selects nothing.

thresholdfloatany

The activity threshold, in the units of the record, not of the excess

Defaults · Applicability · Limitations · Provenance

Defaults

None; both terms are the caller’s. The VEST values are a machine-specific setting [vest.yaml].

Applicability

Machine-independent.

Limitations

A negative peak (every sample below the baseline inside the mask) contributes nothing: the fraction term is clipped at zero and the threshold falls back to the sigma term. Detectors treat a non-finite baseline or threshold as no evidence rather than thresholding on it.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.
  3. The VEST values for these rules are policy, not defaults: vaft/machine_mapping/vest.yaml carries them in plasma_timing, discharge_timing and plasma_features, resolved by vaft.machine_mapping.utils and passed in by vaft.omas.plasma_timing, vaft.omas.discharge_timing and vaft.omas.plasma_features.

isolated_excursions

Machine-independent source
isolated_excursions(values, threshold, max_run_samples)
Show source vaft/process/onset.py, lines 694–733
def isolated_excursions(values, threshold: float, max_run_samples: int) -> int:
    """How many runs above a threshold are shorter than a given length.

    The count of spikes a record carries: the detectors report it as
    ``isolated_excursions_before_onset`` so a consumer can see how noisy the
    stretch before an onset was without re-reading the waveform.

    Parameters
    ----------
    values : array_like
        The waveform [any].
    threshold : float
        Level a sample must exceed to be part of a run [any].
    max_run_samples : int
        Runs strictly shorter than this many samples are counted [-].

    Returns
    -------
    int
        Number of such runs [-].

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

    Limitations
    -----------
    Purely a count: which runs, and where, is not reported.  The detectors keep
    the runs they rejected, with reasons, in ``OnsetRecord.rejected``.

    Provenance
    ----------
    .. [409] Issue #409, which introduced these primitives and fixed their rules
       against the VEST raw database.
    .. [corpus] ``workflow/plasma_onset/scan_corpus.py`` and the table it writes,
       ``test/data/onset_corpus.json``: the scan of VEST shots 39900-41700 the
       thresholds, hold times and widths were judged on.
    """
    y = np.asarray(values, dtype=float).reshape(-1)
    return sum(1 for a, b in _runs(y > threshold) if (b - a) < int(max_run_samples))

How many runs above a threshold are shorter than a given length.

The count of spikes a record carries: the detectors report it as isolated_excursions_before_onset so a consumer can see how noisy the stretch before an onset was without re-reading the waveform.

NameTypeUnitDescription
valuesarray_likeany

The waveform.

thresholdfloatany

Level a sample must exceed to be part of a run.

max_run_samplesint-

Runs strictly shorter than this many samples are counted.

Returns
—int-

Number of such runs.

Applicability · Limitations · Provenance

Applicability

Machine-independent.

Limitations

Purely a count: which runs, and where, is not reported. The detectors keep the runs they rejected, with reasons, in OnsetRecord.rejected.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.

median_smooth

Machine-independentConvention-sensitive source
median_smooth(values, kernel_samples)
Show source vaft/process/onset.py, lines 329–390
def median_smooth(values, kernel_samples: int) -> np.ndarray:
    """Median filter that removes excursions shorter than half the kernel.

    The prefilter every optical detector runs first: an isolated digitizer spike
    is not evidence of light, and a median of an odd kernel removes any excursion
    narrower than half of it without moving the edges of the ones it keeps.

    Parameters
    ----------
    values : array_like
        The waveform; non-finite samples are linearly interpolated first [any].
    kernel_samples : int
        Median kernel width in samples, forced odd; ``1`` or less returns the
        finite-filled input unchanged [-].

    Returns
    -------
    numpy.ndarray
        The filtered waveform, same length and grid as the input [any].

    Defaults
    --------
    None; the kernel is the caller's.  The VEST widths are a machine-specific
    setting [vest.yaml]: five samples on the slow optical and current
    channels, one (no filtering) where the rule wants the raw record.

    Convention
    ----------
    Zero-phase by construction: a median filter of an odd kernel introduces no
    group delay, so an onset read off the output is not shifted from the input's.
    An even ``kernel_samples`` is incremented rather than applied, because an even
    median averages two order statistics and does shift edges.

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

    Limitations
    -----------
    A kernel at or above the record length is clipped to the longest odd kernel
    that fits.  A flat top wider than the kernel survives, which is what lets
    :func:`robust_peak` still see a railed digitizer.

    Provenance
    ----------
    .. [409] Issue #409, which introduced these primitives and fixed their rules
       against the VEST raw database.
    .. [corpus] ``workflow/plasma_onset/scan_corpus.py`` and the table it writes,
       ``test/data/onset_corpus.json``: the scan of VEST shots 39900-41700 the
       thresholds, hold times and widths were judged on.
    """
    y = _fill_non_finite(np.asarray(values, dtype=float).reshape(-1))
    k = int(kernel_samples)
    if k <= 1:
        return y
    if k % 2 == 0:
        k += 1
    if k >= y.size:
        k = y.size if y.size % 2 else y.size - 1
        if k <= 1:
            return y
    return scipy_signal.medfilt(y, kernel_size=k)

Median filter that removes excursions shorter than half the kernel.

The prefilter every optical detector runs first: an isolated digitizer spike is not evidence of light, and a median of an odd kernel removes any excursion narrower than half of it without moving the edges of the ones it keeps.

NameTypeUnitDescription
valuesarray_likeany

The waveform; non-finite samples are linearly interpolated first.

kernel_samplesint-

Median kernel width in samples, forced odd; 1 or less returns the finite-filled input unchanged.

Returns
—numpy.ndarrayany

The filtered waveform, same length and grid as the input.

Convention

Zero-phase by construction: a median filter of an odd kernel introduces no group delay, so an onset read off the output is not shifted from the input’s. An even kernel_samples is incremented rather than applied, because an even median averages two order statistics and does shift edges.

Defaults · Applicability · Limitations · Provenance

Defaults

None; the kernel is the caller’s. The VEST widths are a machine-specific setting [vest.yaml]: five samples on the slow optical and current channels, one (no filtering) where the rule wants the raw record.

Applicability

Machine-independent.

Limitations

A kernel at or above the record length is clipped to the longest odd kernel that fits. A flat top wider than the kernel survives, which is what lets robust_peak still see a railed digitizer.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.

pickup_scale

Machine-independentConvention-sensitive source
pickup_scale(values, baseline, robust_sigma, dt, *, impulse_max_s=0.002, sigma=6.0)
Show source vaft/process/onset.py, lines 736–806
def pickup_scale(values, baseline: float, robust_sigma: float, dt: float,
                 *, impulse_max_s: float = 2.0e-3, sigma: float = 6.0) -> float:
    """The record's own impulsive-excursion amplitude.

    The largest absolute excess over the baseline among runs above
    ``sigma * robust_sigma`` that last less than ``impulse_max_s`` -- on a
    plasma-current record, the coil-firing pickup measured on that very shot.
    :func:`principal_pulse_onset` compares a candidate pulse against it, which is
    what refuses a record whose maximum *is* the pickup.

    Parameters
    ----------
    values : array_like
        The waveform [any].
    baseline : float
        Level the excess is measured from [any].
    robust_sigma : float
        Robust spread of the reference, as returned by
        :func:`robust_baseline` [any].
    dt : float
        Sample spacing, used to turn ``impulse_max_s`` into samples [s].
    impulse_max_s : float, optional
        Longest run still counted as an impulse [s].
    sigma : float, optional
        Number of robust sigmas a sample must clear to be part of a run [-].

    Returns
    -------
    float
        The largest such excursion, or ``0.0`` when there is none or the
        baseline or spread is not usable [any].

    Defaults
    --------
    ``sigma = 6`` is empirical and deliberately above the detectors' five: white
    noise does not reach six sigma in a record of a few thousand samples, so the
    scale measures pickup rather than the noise tail.  ``impulse_max_s = 2 ms``
    is empirical too, the longest coil-firing transient seen on VEST
    plasma-current records [corpus].

    Convention
    ----------
    Absolute: a pickup transient of either sign counts, because the induced
    excursion's polarity depends on the coil and the winding, not on the plasma.

    Applicability
    -------------
    Machine-independent.  Both defaults were fixed on VEST records.

    Limitations
    -----------
    The scale is measured wherever the caller points it.  Inside a pulse the
    pulse's own noisy threshold crossings would be counted, so
    :func:`principal_pulse_onset` measures it outside the principal run, one
    impulse length clear of both edges.

    Provenance
    ----------
    .. [409] Issue #409, which introduced these primitives and fixed their rules
       against the VEST raw database.
    .. [corpus] ``workflow/plasma_onset/scan_corpus.py`` and the table it writes,
       ``test/data/onset_corpus.json``: the scan of VEST shots 39900-41700 the
       thresholds, hold times and widths were judged on.
    """
    y = np.asarray(values, dtype=float).reshape(-1)
    if not (np.isfinite(baseline) and np.isfinite(robust_sigma) and robust_sigma > 0):
        return 0.0
    excess = np.abs(y - baseline)
    max_samples = max(1, int(round(float(impulse_max_s) / float(dt))))
    peaks = [excess[a:b].max() for a, b in _runs(excess > sigma * robust_sigma) if (b - a) < max_samples]
    return float(max(peaks)) if peaks else 0.0

The record’s own impulsive-excursion amplitude.

The largest absolute excess over the baseline among runs above sigma * robust_sigma that last less than impulse_max_s – on a plasma-current record, the coil-firing pickup measured on that very shot. principal_pulse_onset compares a candidate pulse against it, which is what refuses a record whose maximum is the pickup.

NameTypeUnitDescription
valuesarray_likeany

The waveform.

baselinefloatany

Level the excess is measured from.

robust_sigmafloatany

Robust spread of the reference, as returned by robust_baseline.

dtfloats

Sample spacing, used to turn impulse_max_s into samples.

impulse_max_sfloat, optionals

Longest run still counted as an impulse.

sigmafloat, optional-

Number of robust sigmas a sample must clear to be part of a run.

Returns
—floatany

The largest such excursion, or 0.0 when there is none or the baseline or spread is not usable.

Convention

Absolute: a pickup transient of either sign counts, because the induced excursion’s polarity depends on the coil and the winding, not on the plasma.

Defaults · Applicability · Limitations · Provenance

Defaults

sigma = 6 is empirical and deliberately above the detectors’ five: white noise does not reach six sigma in a record of a few thousand samples, so the scale measures pickup rather than the noise tail. impulse_max_s = 2 ms is empirical too, the longest coil-firing transient seen on VEST plasma-current records [corpus].

Applicability

Machine-independent. Both defaults were fixed on VEST records.

Limitations

The scale is measured wherever the caller points it. Inside a pulse the pulse’s own noisy threshold crossings would be counted, so principal_pulse_onset measures it outside the principal run, one impulse length clear of both edges.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.

principal_pulse_onset

Machine-independentConvention-sensitive source
principal_pulse_onset(time, values, *, fraction=0.02, sigma=5.0, reference_mask=None, reference_fraction=0.2, search_mask=None, cutoff_hz=None, fs=None, order=4, pickup_floor=3.0, impulse_max_s=0.002, bridge_samples=2)

Onset of the pulse that contains the global maximum.

An excursion not connected to the maximum can never be chosen, which is what makes this the detector for a record known to be pulse-shaped. A record with no pulse has its maximum at the pickup, and two floors guard that case.

NameTypeUnitDescription
timearray_likes

Time grid of the record.

valuesarray_likeany

The waveform.

fractionfloat, optional-

Fraction-of-peak term of the threshold.

sigmafloat, optional-

Robust-sigma term of the threshold.

reference_maskarray_like of bool or None, optional-

Samples that define the quiet reference; None selects the leading reference_fraction of the record.

reference_fractionfloat, optional-

Fraction of the record used as the reference when no mask is given.

search_maskarray_like of bool or None, optional-

Samples the detector may look at; the peak that scales the threshold is taken inside it too. None is the whole record.

cutoff_hzfloat or None, optionalHz

Zero-phase low-pass cutoff; None leaves the record unfiltered.

fsfloat or None, optionalHz

Sample rate; inferred from time when None.

orderint, optional-

Butterworth order of that low-pass.

pickup_floorfloat, optional-

How many times the record’s own impulsive scale the pulse must exceed; 0 disables the test.

impulse_max_sfloat, optionals

Longest run still treated as an impulse, both for the principal-run test and for pickup_scale.

bridge_samplesint, optional-

Longest dip below the threshold that does not split the run.

Returns
—OnsetRecords

time is the first sample of the principal run, or None with no_onset and one of peak_below_noise, principal_run_impulsive, peak_below_pickup_floor, search_mask_empty or record_too_short. evidence carries the threshold terms, the peak’s time and index and the measured pickup_scale.

Convention

The onset is the run’s first sample. The low-pass is zero-phase, so the onset is not shifted by the filtering.

Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Low-pass the record (zero-phase) when a cutoff_hz is given and the grid can carry it; otherwise flag lowpass_skipped.
  2. Settle the reference, measure baseline and spread, threshold as excess_threshold.
  3. Refuse when the peak does not clear sigma robust sigmas (peak_below_noise).
  4. Walk back and forward from the maximum inside search_mask while the excess stays above the threshold; that connected run is the pulse.
  5. Refuse a run narrower than impulse_max_s (principal_run_impulsive) and a peak below pickup_floor times pickup_scale measured outside the run (peak_below_pickup_floor); otherwise the onset is the run’s first sample.

Defaults

Numerical convenience, as in sustained_excess_onset; pickup_floor and impulse_max_s are empirical, fixed on VEST plasma-current records [corpus]. The VEST values are policy [vest.yaml].

Applicability

Machine-independent.

Limitations

Only for a record whose activity is one pulse: on a record with two comparable pulses the answer is the onset of whichever holds the maximum, with no flag saying the other exists. active_window reports that case as multiple_segments.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.
  3. The VEST values for these rules are policy, not defaults: vaft/machine_mapping/vest.yaml carries them in plasma_timing, discharge_timing and plasma_features, resolved by vaft.machine_mapping.utils and passed in by vaft.omas.plasma_timing, vaft.omas.discharge_timing and vaft.omas.plasma_features.

principal_pulse_window

Machine-independent source
principal_pulse_window(time, values, **kwargs)
Show source vaft/process/onset.py, lines 1754–1818
def principal_pulse_window(time, values, **kwargs) -> tuple[OnsetRecord, OnsetRecord]:
    """``(onset, offset)`` of the pulse holding the global maximum.

    One :func:`active_window` call with ``principal_only=True`` and the principal
    defaults, for callers that want the pair of records rather than the window
    object.  Every other keyword argument is forwarded to :func:`active_window`
    unchanged.

    Parameters
    ----------
    time : array_like
        Time grid of the record [s].
    values : array_like
        The waveform [any].

    Returns
    -------
    onset : OnsetRecord
        The principal run's first sample -- exactly what
        :func:`principal_pulse_onset` returns -- or ``None`` with the window's
        flags [s].
    offset : OnsetRecord
        The principal run's last sample, carrying the window's flags, so
        ``offset_at_record_end`` and ``offset_from_collapse`` reach a caller that
        never sees the :class:`PulseWindow` [s].

    Defaults
    --------
    ``pickup_floor = 3`` and ``hold_s = 0`` are set here unless the caller
    overrides them: empirical, and the pair that makes this agree with
    :func:`principal_pulse_onset` -- the pulse is the run holding the maximum, so
    persistence is redundant and the pickup floor does the refusing.

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

    Limitations
    -----------
    Both records carry the *window's* evidence rather than an onset detector's, so
    ``evidence`` is keyed as :func:`active_window` keys it.

    Provenance
    ----------
    .. [409] Issue #409, which introduced these primitives and fixed their rules
       against the VEST raw database.
    .. [corpus] ``workflow/plasma_onset/scan_corpus.py`` and the table it writes,
       ``test/data/onset_corpus.json``: the scan of VEST shots 39900-41700 the
       thresholds, hold times and widths were judged on.
    """
    kwargs.setdefault("pickup_floor", 3.0)
    kwargs.setdefault("hold_s", 0.0)
    window = active_window(time, values, principal_only=True, **kwargs)
    if not window.found:
        none = OnsetRecord(time=None, index=None, method="principal_pulse", evidence=dict(window.evidence),
                           flags=window.flags)
        return none, OnsetRecord(time=None, index=None, method="principal_pulse_offset",
                                 evidence=dict(window.evidence), flags=window.flags)
    onset = OnsetRecord(time=window.onset.time, index=window.onset.index, method="principal_pulse",
                        evidence=dict(window.evidence), flags=window.onset.flags,
                        rejected=window.onset.rejected, accepted=window.onset.accepted)
    offset = OnsetRecord(time=window.offset.time, index=window.offset.index,
                         method="principal_pulse_offset", evidence=dict(window.evidence),
                         flags=window.flags, accepted=window.offset.accepted)
    return onset, offset

(onset, offset) of the pulse holding the global maximum.

One active_window call with principal_only=True and the principal defaults, for callers that want the pair of records rather than the window object. Every other keyword argument is forwarded to active_window unchanged.

NameTypeUnitDescription
timearray_likes

Time grid of the record.

valuesarray_likeany

The waveform.

Returns
onsetOnsetRecords

The principal run’s first sample – exactly what principal_pulse_onset returns – or None with the window’s flags.

offsetOnsetRecords

The principal run’s last sample, carrying the window’s flags, so offset_at_record_end and offset_from_collapse reach a caller that never sees the PulseWindow.

Defaults · Applicability · Limitations · Provenance

Defaults

pickup_floor = 3 and hold_s = 0 are set here unless the caller overrides them: empirical, and the pair that makes this agree with principal_pulse_onset – the pulse is the run holding the maximum, so persistence is redundant and the pickup floor does the refusing.

Applicability

Machine-independent.

Limitations

Both records carry the window’s evidence rather than an onset detector’s, so evidence is keyed as active_window keys it.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.

robust_baseline

Machine-independent source
robust_baseline(values, reference_mask=None)
Show source vaft/process/onset.py, lines 266–326
def robust_baseline(values, reference_mask=None) -> tuple[float, float]:
    """Median and robust sigma (``1.4826 * MAD``) over the reference samples.

    The MAD is computed here rather than through ``vaft.formula`` so this module
    stays a scipy-only import.

    Parameters
    ----------
    values : array_like
        The waveform [any].
    reference_mask : array_like of bool or None, optional
        Samples to measure over; ``None`` uses all of them [-].

    Returns
    -------
    baseline : float
        Median of the finite reference samples, ``nan`` when fewer than two
        exist [any].
    robust_sigma : float
        ``1.4826`` times the median absolute deviation about that median, on the
        same samples, ``nan`` under the same condition [any].

    Defaults
    --------
    The MAD-to-sigma factor ``1.4826`` (``MAD_TO_SIGMA``) is a physical constant
    of the Gaussian distribution, not a tuning value: it makes the returned
    spread equal the standard deviation for Gaussian noise.

    Assumptions
    -----------
    The reference samples are quiet.  Nothing here checks that; a caller that
    cannot guarantee it asks a detector, which settles the reference itself and
    flags ``reference_contaminated``.

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

    Limitations
    -----------
    ``(nan, nan)`` on fewer than two finite reference samples, so a caller can
    flag the reference rather than threshold against garbage.  A reference of
    identical samples gives ``robust_sigma == 0``; a threshold built on it
    degenerates to the fraction-of-peak term alone.

    Provenance
    ----------
    .. [409] Issue #409, which introduced these primitives and fixed their rules
       against the VEST raw database.
    .. [corpus] ``workflow/plasma_onset/scan_corpus.py`` and the table it writes,
       ``test/data/onset_corpus.json``: the scan of VEST shots 39900-41700 the
       thresholds, hold times and widths were judged on.
    """
    y = np.asarray(values, dtype=float).reshape(-1)
    if reference_mask is not None:
        y = y[np.asarray(reference_mask, dtype=bool).reshape(-1)]
    y = y[np.isfinite(y)]
    if y.size < 2:
        return float("nan"), float("nan")
    median = float(np.median(y))
    return median, float(MAD_TO_SIGMA * np.median(np.abs(y - median)))

Median and robust sigma (1.4826 * MAD) over the reference samples.

The MAD is computed here rather than through vaft.formula so this module stays a scipy-only import.

NameTypeUnitDescription
valuesarray_likeany

The waveform.

reference_maskarray_like of bool or None, optional-

Samples to measure over; None uses all of them.

Returns
baselinefloatany

Median of the finite reference samples, nan when fewer than two exist.

robust_sigmafloatany

1.4826 times the median absolute deviation about that median, on the same samples, nan under the same condition.

Defaults · Assumptions · Applicability · Limitations · Provenance

Defaults

The MAD-to-sigma factor 1.4826 (MAD_TO_SIGMA) is a physical constant of the Gaussian distribution, not a tuning value: it makes the returned spread equal the standard deviation for Gaussian noise.

Assumptions

The reference samples are quiet. Nothing here checks that; a caller that cannot guarantee it asks a detector, which settles the reference itself and flags reference_contaminated.

Applicability

Machine-independent.

Limitations

(nan, nan) on fewer than two finite reference samples, so a caller can flag the reference rather than threshold against garbage. A reference of identical samples gives robust_sigma == 0; a threshold built on it degenerates to the fraction-of-peak term alone.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.

robust_peak

Machine-independentConvention-sensitive source
robust_peak(time, values, *, polarity='positive', prefilter_samples=5, cutoff_hz=None, fs=None, order=4, sigma=5.0, level_fraction=0.5, min_width_s=0.001, max_candidates=8, edge_samples=1, impulse_max_s=0.002, reference_mask=None, reference_fraction=0.2, search_mask=None, bridge_samples=2)

The representative peak of a record: its largest sustained excursion, not its loudest sample.

The measurement behind every peak value VAFT reports – the plasma current, a line’s brightness, the diamagnetic flux – for records where the loudest sample is routinely a coil-firing impulse or a digitizer glitch.

NameTypeUnitDescription
timearray_likes

Time grid of the record.

valuesarray_likeany

The waveform.

polarity{'positive', 'negative', 'absolute'}, optional-

What counts as up.

prefilter_samplesint, optional-

Median-filter kernel applied first; 1 means no filtering.

cutoff_hzfloat or None, optionalHz

Zero-phase low-pass cutoff; None leaves the record unfiltered.

fsfloat or None, optionalHz

Sample rate; inferred from time when None.

orderint, optional-

Butterworth order of that low-pass.

sigmafloat, optional-

How many robust sigmas the excursion must clear.

level_fractionfloat, optional-

Height at which the run holding a candidate is measured, as a fraction of the candidate’s own excursion.

min_width_sfloat, optionals

How long that run must last for the candidate to be a peak rather than a spike.

max_candidatesint, optional-

How many candidates may be refused before the record is given up on; must be at least 1.

edge_samplesint, optional-

How close to the edge of the search stretch a peak may lie before peak_at_window_edge is flagged.

impulse_max_sfloat, optionals

Longest run treated as an impulse, for both the refusal mask and the reported pickup_scale.

reference_maskarray_like of bool or None, optional-

Samples that define the quiet reference; None selects the leading reference_fraction of the record.

reference_fractionfloat, optional-

Fraction of the record used as the reference when no mask is given.

search_maskarray_like of bool or None, optional-

Samples the detector may look at; the peak that scales the threshold is taken inside it too. None is the whole record.

bridge_samplesint, optional-

Longest dip below a candidate’s level that does not split its run.

Returns
—PeakRecordany

value is the smoothed record’s extreme sample in the accepted run, and time the sample of the input grid it sits on – or None with no_peak and one reason. evidence carries the baseline, the robust sigma, the rule values, raw_value, raw_max and raw_max_time, the measured pickup_scale and the number of refused candidates.

Convention

polarity decides the sign of the answer: with negative or absolute the reported value is still the record’s own signed sample, not the score, so a VEST diamagnetic flux keeps its stored sign (positive for a paramagnetic plasma, #1196). value is the smoothed sample; the raw one is in the evidence.

Raises

  • ValueError When polarity is not one of the three, or max_candidates is below one.
Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Median-filter over prefilter_samples and, when a cutoff_hz is given and the grid can carry it, zero-phase low-pass.
  2. Settle the reference and take the baseline as its median.
  3. Orient the record by polarity: positive scores the excess over the baseline, negative the deficit, absolute the magnitude.
  4. Take the largest score inside search_mask as a candidate; the run holding it at level_fraction of its height must last min_width_s. A narrower run is a spike: it is refused (rejected, reason impulsive), masked whole – shoulders included, bounded by impulse_max_s and by the baseline crossing, so it cannot come back – and the next candidate is tried, up to max_candidates times.
  5. Report the accepted run’s extreme sample, with the raw record’s own maximum kept in the evidence.

Defaults

The signature values are numerical convenience. The VEST values are policy in the plasma_features block [vest.yaml], per signal: the plasma current, H-alpha, each impurity line and the diamagnetic flux carry their own prefilter, sigma, level_fraction and min_width_s, and an H-alpha value is never reused for an impurity line.

Applicability

Machine-independent.

Limitations

Flags on a found peak say when the answer is not the loudest sample: peak_is_spike (the raw maximum lay in a refused run), raw_max_outside_run (it lies outside the accepted run for any other reason), peak_at_window_edge (the excursion may continue past the search stretch), peak_plateau (the raw record holds its extreme value for three samples or more – a railed digitizer), reference_flat (the reference has no spread; a peak is still reported, because an integrated record can have an exactly zero lead). A record whose every excursion is impulsive returns all_candidates_impulsive rather than a value.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.
  3. The VEST values for these rules are policy, not defaults: vaft/machine_mapping/vest.yaml carries them in plasma_timing, discharge_timing and plasma_features, resolved by vaft.machine_mapping.utils and passed in by vaft.omas.plasma_timing, vaft.omas.discharge_timing and vaft.omas.plasma_features.

run_features

Machine-independentConvention-sensitive source
run_features(time, values, baseline, start, stop)
Show source vaft/process/onset.py, lines 621–691
def run_features(time, values, baseline: float, start: int, stop: int) -> RunFeatures:
    """Width, peak, prominence and integral of one run above a baseline.

    The temporal shape of a candidate run, on which the morphology rules of the
    detectors are stated.  Prominence comes from ``scipy.signal.peak_prominences``
    on the whole excess record, so it measures how far the run rises above its
    surroundings, not above the baseline.

    Parameters
    ----------
    time : array_like
        Time grid of the record; must be as long as ``values`` [s].
    values : array_like
        The waveform [any].
    baseline : float
        Level the excess is measured from [any].
    start, stop : int
        Half-open index bounds of the run, ``values[start:stop]`` [-].

    Returns
    -------
    RunFeatures
        ``start_time`` and ``end_time`` (first and last sample of the run) [s],
        ``width_s`` (``samples * dt``) [s], ``samples`` [-], ``peak`` and
        ``prominence`` (excess over the baseline) [any], ``peak_time`` [s] and
        ``integral`` (excess integrated over the run) [any*s].

    Convention
    ----------
    ``end_time`` is the run's last sample, not the first sample after it, so a
    run of one sample has ``start_time == end_time`` and ``width_s == dt``.  The
    sample spacing is the median of ``diff(time)``, so a record with a few
    irregular samples still gets a representative width.

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

    Limitations
    -----------
    A run touching either end of the record has no surroundings on that side;
    ``prominence`` then falls back to the run's own peak excess.  Nothing checks
    that ``start`` and ``stop`` bound a run that is actually above the baseline.

    Provenance
    ----------
    .. [409] Issue #409, which introduced these primitives and fixed their rules
       against the VEST raw database.
    .. [corpus] ``workflow/plasma_onset/scan_corpus.py`` and the table it writes,
       ``test/data/onset_corpus.json``: the scan of VEST shots 39900-41700 the
       thresholds, hold times and widths were judged on.
    """
    t, y = _as_arrays(time, values)
    seg = y[start:stop] - baseline
    dt = float(np.median(np.diff(t)))
    i_max = start + int(np.argmax(seg))
    excess = y - baseline
    if 0 < i_max < y.size - 1:
        prominence = float(scipy_signal.peak_prominences(excess, [i_max])[0][0])
    else:
        prominence = float(seg.max())
    return RunFeatures(
        start_time=float(t[start]),
        end_time=float(t[stop - 1]),
        width_s=float((stop - start) * dt),
        samples=int(stop - start),
        peak=float(seg.max()),
        peak_time=float(t[i_max]),
        prominence=prominence,
        integral=float(np.sum(seg) * dt),
    )

Width, peak, prominence and integral of one run above a baseline.

The temporal shape of a candidate run, on which the morphology rules of the detectors are stated. Prominence comes from scipy.signal.peak_prominences on the whole excess record, so it measures how far the run rises above its surroundings, not above the baseline.

NameTypeUnitDescription
timearray_likes

Time grid of the record; must be as long as values.

valuesarray_likeany

The waveform.

baselinefloatany

Level the excess is measured from.

start, stopint-

Half-open index bounds of the run, values[start:stop].

Returns
—RunFeaturesany*s

start_time and end_time (first and last sample of the run) [s], width_s (samples * dt) [s], samples [-], peak and prominence (excess over the baseline) [any], peak_time [s] and integral (excess integrated over the run).

Convention

end_time is the run’s last sample, not the first sample after it, so a run of one sample has start_time == end_time and width_s == dt. The sample spacing is the median of diff(time), so a record with a few irregular samples still gets a representative width.

Applicability · Limitations · Provenance

Applicability

Machine-independent.

Limitations

A run touching either end of the record has no surroundings on that side; prominence then falls back to the run’s own peak excess. Nothing checks that start and stop bound a run that is actually above the baseline.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.

sustained_excess_onset

Machine-independentConvention-sensitive source
sustained_excess_onset(time, values, *, fraction=0.02, sigma=5.0, hold_s=0.0005, min_width_s=0.0, min_prominence_sigma=0.0, min_integral_fraction=0.0, reference_mask=None, reference_fraction=0.2, search_mask=None, prefilter_samples=1, bridge_samples=2)

First run above the threshold that persists and has the right shape.

The general-purpose detector: it makes no assumption that the record is pulse-shaped, so it is the one to use where the first activity matters and a later, larger excursion does not.

NameTypeUnitDescription
timearray_likes

Time grid of the record.

valuesarray_likeany

The waveform.

fractionfloat, optional-

Fraction-of-peak term of the threshold.

sigmafloat, optional-

Robust-sigma term of the threshold.

hold_sfloat, optionals

How long a run must stay above the threshold.

min_width_sfloat, optionals

Shortest accepted run; 0 disables the test.

min_prominence_sigmafloat, optional-

How far a run must rise above its surroundings, in robust sigmas; 0 disables the test.

min_integral_fractionfloat, optional-

Fraction of the record’s total positive excess a run must carry; 0 disables the test.

reference_maskarray_like of bool or None, optional-

Samples that define the quiet reference; None selects the leading reference_fraction of the record.

reference_fractionfloat, optional-

Fraction of the record used as the reference when no mask is given.

search_maskarray_like of bool or None, optional-

Samples the detector may look at; the peak that scales the threshold is taken inside it too. None is the whole record.

prefilter_samplesint, optional-

Median-filter kernel applied first; 1 means no filtering.

bridge_samplesint, optional-

Longest dip below the threshold that does not split a run.

Returns
—OnsetRecords

time is the accepted run’s first sample of the input grid, or None with no_onset and a reason. evidence carries the baseline, spread, peak, threshold and the rule values; rejected the runs that crossed the threshold and failed, each with its reason.

Convention

The onset is the run’s first sample, so a half-open consumer window [start, onset) excludes exactly the samples from the onset onward.

Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Median-filter the record over prefilter_samples (an optical channel’s isolated spikes are removed before they can be counted).
  2. Settle the reference stretch and measure baseline and spread on it.
  3. Threshold as excess_threshold; refuse the record outright when the baseline, spread or peak is degenerate.
  4. Bridge gaps of at most bridge_samples below the threshold, so a single noise sample during a rise does not split the run.
  5. Walk the runs in time order; the first that passes persistence (hold_s) and morphology (min_width_s, min_prominence_sigma, min_integral_fraction) is the onset, at its first sample. Every run that fails is kept in rejected with the reason.

Defaults

The signature values are numerical convenience – a working detector for a caller with no policy – and are documented as rules, not as numbers to copy. The VEST values are policy [vest.yaml]; the reasoning behind their magnitudes, and the false-onset counts that fixed them, is in the module docstring and in the corpus table [corpus].

Applicability

Machine-independent.

Limitations

No pulse shape is assumed, so an early isolated excursion that passes both persistence and morphology is the onset. Where the record is known to be pulse-shaped and the principal pulse is what is wanted, principal_pulse_onset is unreachable by such an excursion by construction.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.
  3. The VEST values for these rules are policy, not defaults: vaft/machine_mapping/vest.yaml carries them in plasma_timing, discharge_timing and plasma_features, resolved by vaft.machine_mapping.utils and passed in by vaft.omas.plasma_timing, vaft.omas.discharge_timing and vaft.omas.plasma_features.

zero_crossing_after_excursion

Machine-independentConvention-sensitive source
zero_crossing_after_excursion(time, values, *, anchor_time, fraction=0.05, sigma=5.0, hold_s=0.0005, anchor_tolerance_s=0.0005, approach_fraction=0.1, approach_hysteresis=2.0, reference_mask=None, reference_fraction=0.2, search_mask=None, prefilter_samples=1, bridge_samples=2)

First sample after an anchored excursion where the record changes sign.

Written for a drive waveform: the loop voltage crosses zero once the ohmic swing that drove it is over, and that crossing – not the record’s loudest moment – is the event. Anchoring is what makes a later, larger pulse irrelevant: the excursion is judged where the drive starts.

NameTypeUnitDescription
timearray_likes

Time grid of the record.

valuesarray_likeany

The waveform.

anchor_timefloats

Where the excursion is expected to start, usually another detector’s onset.

fractionfloat, optional-

Fraction-of-peak term of the excursion threshold.

sigmafloat, optional-

Robust-sigma term of the excursion threshold.

hold_sfloat, optionals

How long the excursion must stay above that threshold.

anchor_tolerance_sfloat, optionals

How far the run’s start may lie from the anchor when it does not contain it.

approach_fractionfloat, optional-

Fraction of the extremum the decay must reach to count as an approach

approach_hysteresisfloat, optional-

Multiple of that level the record must climb back above for the approach to be reported as one that did not cross; the hysteresis keeps noise around the level from counting as a re-rise.

reference_maskarray_like of bool or None, optional-

Samples that define the quiet reference; None selects the leading reference_fraction of the record.

reference_fractionfloat, optional-

Fraction of the record used as the reference when no mask is given.

search_maskarray_like of bool or None, optional-

Samples the detector may look at; the peak that scales the threshold is taken inside it too. None is the whole record.

prefilter_samplesint, optional-

Median-filter kernel applied first; 1 means no filtering.

bridge_samplesint, optional-

Longest dip below the threshold that does not split the excursion.

Returns
—OnsetRecords

time is the first sample past the crossing, or None with no_excursion_at_anchor or no_zero_crossing. evidence carries the signed extremum (baseline-relative), its time, the run bounds, the run’s start relative to the anchor, and the approach minimum when there was one.

Convention

The sign is measured relative to the baseline, not to zero, so a record with an offset crosses where it returns through its own quiet level. The crossing must be strict: a sample exactly at the baseline is not one.

Processing steps · Defaults · Applicability · Limitations · Provenance

Processing steps

  1. Median-filter over prefilter_samples, settle the reference, measure baseline and spread.
  2. Threshold |values - baseline| at max(fraction * peak, sigma * robust_sigma), the peak taken inside search_mask.
  3. Take the first sustained run (hold_s) that starts within anchor_tolerance_s of anchor_time or contains the anchor sample; with none, return no_excursion_at_anchor.
  4. From that run’s extremum, walk forward to the first sample whose baseline-relative sign is opposite to the extremum’s – strictly, so a sample exactly at the baseline is not a crossing.
  5. Watch the decay in between: when the deviation drops below approach_fraction of the extremum and climbs back above approach_hysteresis times that level before crossing, flag approached_without_crossing and record the minimum and its time.

Defaults

The signature values are numerical convenience; the VEST loop-voltage values – the fraction, the hold, the anchor tolerance and both approach numbers – are policy in the discharge_timing.vloop block [vest.yaml].

Applicability

Machine-independent.

Limitations

One crossing only, the first after the anchored excursion. A record that decays towards the baseline and turns back without reaching it is reported as approached_without_crossing and no_zero_crossing – the flag is evidence for a consumer, not an event.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.
  3. The VEST values for these rules are policy, not defaults: vaft/machine_mapping/vest.yaml carries them in plasma_timing, discharge_timing and plasma_features, resolved by vaft.machine_mapping.utils and passed in by vaft.omas.plasma_timing, vaft.omas.discharge_timing and vaft.omas.plasma_features.

zero_phase_lowpass

Machine-independentConvention-sensitive source
zero_phase_lowpass(values, cutoff_hz, fs, order=4)
Show source vaft/process/onset.py, lines 393–462
def zero_phase_lowpass(values, cutoff_hz: float, fs: float, order: int = 4) -> np.ndarray:
    """Forward-backward Butterworth low-pass: no group delay.

    An onset read off the output is not shifted from the input's.  A causal filter
    of the same order moved the plasma-current onset by +0.1 to +0.9 ms on VEST
    records; this one by less than a sample.

    Parameters
    ----------
    values : array_like
        The waveform; non-finite samples are linearly interpolated first [any].
    cutoff_hz : float
        Low-pass cutoff [Hz].
    fs : float
        Sample rate of the record [Hz].
    order : int, optional
        Butterworth order; the filter is applied twice, so the effective
        roll-off is twice this [-].

    Returns
    -------
    numpy.ndarray
        The filtered waveform, same length and grid as the input [any].

    Raises
    ------
    ValueError
        When the record has no more samples than ``filtfilt``'s padding length,
        ``3 * (order + 1)``.  The detectors call :func:`_too_short` first and
        report ``record_too_short`` instead of raising.

    Defaults
    --------
    ``order = 4`` is a numerical convenience: the same order the VEST soft X-ray
    viewer and ``vaft.process.signal_processing`` use, steep enough to matter and
    short enough to pad on a few-thousand-sample record.

    Convention
    ----------
    Zero-phase (``scipy.signal.filtfilt``, forward then backward), which is
    mandatory wherever an onset time is read off the result and is the opposite
    choice from ``vaft.process.signal_processing.butterworth_lowpass``, whose
    default is causal to match the validated viewer.

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

    Limitations
    -----------
    A cutoff at or above the record's Nyquist frequency cannot be designed; the
    detectors detect that case themselves and report ``lowpass_skipped`` rather
    than filtering.

    Provenance
    ----------
    .. [409] Issue #409, which introduced these primitives and fixed their rules
       against the VEST raw database.
    .. [corpus] ``workflow/plasma_onset/scan_corpus.py`` and the table it writes,
       ``test/data/onset_corpus.json``: the scan of VEST shots 39900-41700 the
       thresholds, hold times and widths were judged on.
    """
    y = _fill_non_finite(np.asarray(values, dtype=float).reshape(-1))
    padlen = 3 * (int(order) + 1)  # filtfilt's default for a b/a filter of this order
    if y.size <= padlen:
        raise ValueError(
            f"a zero-phase low-pass of order {order} needs more than {padlen} samples; "
            f"the record has {y.size}"
        )
    return np.asarray(butterworth_lowpass(y, float(cutoff_hz), float(fs), int(order), zero_phase=True))

Forward-backward Butterworth low-pass: no group delay.

An onset read off the output is not shifted from the input’s. A causal filter of the same order moved the plasma-current onset by +0.1 to +0.9 ms on VEST records; this one by less than a sample.

NameTypeUnitDescription
valuesarray_likeany

The waveform; non-finite samples are linearly interpolated first.

cutoff_hzfloatHz

Low-pass cutoff.

fsfloatHz

Sample rate of the record.

orderint, optional-

Butterworth order; the filter is applied twice, so the effective roll-off is twice this.

Returns
—numpy.ndarrayany

The filtered waveform, same length and grid as the input.

Convention

Zero-phase (scipy.signal.filtfilt, forward then backward), which is mandatory wherever an onset time is read off the result and is the opposite choice from vaft.process.signal_processing.butterworth_lowpass, whose default is causal to match the validated viewer.

Raises

  • ValueError When the record has no more samples than filtfilt’s padding length, 3 * (order + 1). The detectors call _too_short first and report record_too_short instead of raising.
Defaults · Applicability · Limitations · Provenance

Defaults

order = 4 is a numerical convenience: the same order the VEST soft X-ray viewer and vaft.process.signal_processing use, steep enough to matter and short enough to pad on a few-thousand-sample record.

Applicability

Machine-independent.

Limitations

A cutoff at or above the record’s Nyquist frequency cannot be designed; the detectors detect that case themselves and report lowpass_skipped rather than filtering.

Provenance

  1. Issue #409, which introduced these primitives and fixed their rules against the VEST raw database.
  2. workflow/plasma_onset/scan_corpus.py and the table it writes, test/data/onset_corpus.json: the scan of VEST shots 39900-41700 the thresholds, hold times and widths were judged on.

results matching ""

    No results matching ""