Skip to content

Explosive-Synchronisation Early-Warning Monitor

The monitor.explosive_sync module raises an early warning before a first-order (explosive) synchronisation transition — the abrupt, hysteretic collapse to coherence behind power-grid blackouts and seizure onset. It is the detection layer built on the ordinal-pattern transition entropy primitive in monitor.opt_entropy.

The monitor is passive: it reads node observables and emits a warning record. It never actuates — consistent with SPO's review-only posture.


1. Why ordinal transition entropy is the right signal

Classic critical-slowing-down early-warning signals (rising variance, rising lag-1 autocorrelation; Scheffer et al. 2009) are tuned to continuous (second-order) bifurcations. A first-order synchronisation transition is abrupt and need not slow down before it fires, so variance/autocorrelation react late or not at all.

Ahead of explosive locking, each oscillator's local dynamics instead become more predictable: the diversity of its ordinal-pattern transitions contracts. That contraction is visible in the transition entropy H_T (see monitor.opt_entropy §1.4) well before the macroscopic order parameter jumps. The monitor watches H_T per node and fires when it drops a robust margin below its leading baseline.


2. Algorithm

Given a node-by-time signal array signals of shape (N, T):

  1. Slide. Window starts at 0, step, 2·step, … while a full window fits; each yields one analysis window.
  2. Local entropy field. For every window and node, compute transition_entropy(signals[node, start:start+window], dimension, delay), giving a (W, N) field per_node_entropy. The window must be long enough to admit two ordinal transitions: window ≥ (D − 1)·τ + 3.
  3. Aggregate index. Average across nodes to the headline coherence-regularisation index entropy_index of shape (W,).
  4. Baseline fit. Use the leading n_baseline = max(min_baseline_windows, ⌈baseline_fraction · W⌉) windows. The baseline is summarised by its median m_0 and robust scale s_0 = 1.4826 · MAD (a normal-consistent standard-deviation estimate). The scale is floored at 1e-12 so a perfectly flat baseline does not divide by zero.
  5. Robust drop score. Per window, robust_z = (entropy_index − m_0) / max(s_0, 1e-12) and relative_drop = (m_0 − entropy_index) / m_0 (zero when m_0 = 0).
  6. Alarm. A window past the baseline breaches when both robust_z ≤ −z_threshold and relative_drop ≥ drop_threshold. The alarm fires at the first run of persistence consecutive breaching windows; warning_window is the start of that run and warning_sample its sample index. Requiring two gates (a robust z-score and an absolute fractional drop) and a sustained run suppresses single-window noise.

3. Python API

from scpn_phase_orchestrator.monitor.explosive_sync import (
    explosive_sync_warning,
    ExplosiveSyncWarning,
)

result = explosive_sync_warning(
    signals,            # (N, T) float array; (T,) is treated as one node
    dimension=3,
    delay=1,
    window=128,
    step=16,
    baseline_fraction=0.25,
    min_baseline_windows=3,
    z_threshold=3.0,
    drop_threshold=0.1,
    persistence=2,
)

if result.warning_triggered:
    print("explosive-sync warning at sample", result.warning_sample)
print(result.summary())

ExplosiveSyncWarning is a frozen dataclass carrying the full diagnostic record: window_starts, entropy_index, the per_node_entropy field, robust_z, relative_drop, the baseline fit (baseline_median, baseline_scale, n_baseline_windows), the alarm decision (warning_triggered, warning_window, warning_sample), and the echoed parameters. summary() returns a flat scalar dictionary suitable for logging or metric export. Construction replays the per-node mean, baseline median/MAD, derived z/drop vectors, window grid, and first sustained threshold breach. Every array is copied and made read-only before the record is published.

3.1 Input validation

signals must be a non-empty finite real one- or two-dimensional array. Boolean, complex, text-coercible, malformed, and non-finite inputs are rejected before window arithmetic, while legitimate real numeric-object arrays are normalised to contiguous float64. window, step, min_baseline_windows, and persistence must be positive integers; baseline_fraction must lie in the open interval (0, 1); z_threshold and drop_threshold must be finite and non-negative; window must fit the series and admit two ordinal transitions.


4. Choosing parameters

Parameter Effect Guidance
window Samples per entropy estimate Long enough for a stable H_T (hundreds of samples for D = 3); shorter reacts faster but is noisier.
step Hop between windows Smaller step sharpens the lead time at more compute.
baseline_fraction Reference span Large enough to capture normal variability before any transition.
z_threshold Robust-drop sensitivity Higher rejects more noise; 3.0 is a conventional 3-σ-equivalent.
drop_threshold Minimum fractional drop Guards against tiny but statistically sharp drops on a quiet baseline.
persistence Sustained-breach length ≥ 2 rejects single-window flickers.

The two gates are complementary: z_threshold catches drops that are large relative to baseline noise, while drop_threshold requires the drop to be absolutely meaningful — necessary because a very flat baseline makes the robust z-score explode on negligible movement.


5. Behaviour (tested)

  • Fires on a noise→lock transition in the neighbourhood of the switch and stays silent on stationary noise (test_fires_on_regularisation_transition, test_silent_on_stationary_noise).
  • persistence suppresses flickers — a longer required run never fires earlier than a shorter one (test_persistence_requires_sustained_breach).
  • A high drop_threshold suppresses the warning even on a real transition (test_high_drop_threshold_suppresses_warning).
  • The entropy index equals the per-node mean and is bounded in [0, 1] (test_entropy_index_is_node_mean, test_entropy_index_bounded).
  • A flat baseline does not divide by zero — the scale floor keeps robust_z finite (test_zero_scale_does_not_divide_by_zero).

6. Pipeline position

   signals (N, T) ──▶ explosive_sync_warning ──▶ ExplosiveSyncWarning
        │                      │                        │
        │                      ▼                        ▼
        │            transition_entropy           warning_triggered
        │            (per node, per window)        warning_sample
        ▼                                          entropy_index / per_node field
   any scalar node observable
   (phase velocity, sin θ, …)

The monitor consumes any per-node scalar observable (phase velocity, sin θ, power-injection deviation, …) and emits a warning record for a supervisory or alerting layer. It has no actuation path of its own.


7. Implementation cross-reference

File Role
src/scpn_phase_orchestrator/monitor/explosive_sync.py Monitor + ExplosiveSyncWarning
src/scpn_phase_orchestrator/monitor/opt_entropy.py Transition-entropy compute primitive
tests/test_explosive_sync.py Detection, structure, validation, and guard tests

8. References

  • Scheffer, M. et al. 2009, Nature 461, 53 — "Early-warning signals for critical transitions" (the slowing-down framework this complements).
  • Bandt, C. & Pompe, B. 2002, Phys. Rev. Lett. 88, 174102 — permutation entropy.
  • Gómez-Gardeñes, J., Gómez, S., Arenas, A. & Moreno, Y. 2011, Phys. Rev. Lett. 106, 128701 — explosive synchronisation as a first-order transition.

9. API reference

explosive_sync

Ordinal-pattern transition-entropy early warning for explosive sync.

A first-order (explosive) synchronisation transition — the abrupt, hysteretic collapse to coherence behind power-grid blackouts and seizure onset — is preceded by a regularisation of each oscillator's local dynamics. That regularisation shows up as a drop in the ordinal-pattern transition entropy of the node's observable before the macroscopic order parameter jumps, where variance / autocorrelation critical-slowing-down indicators are weak.

explosive_sync_warning slides a window across a multi-node signal array, computes the per-node transition entropy (monitor/opt_entropy.py, five-backend accelerated), aggregates it into a coherence-regularisation index, and raises a fail-early alarm when the index drops a robust (median / MAD) margin below its leading baseline. The monitor is passive: it reads observables and emits a warning record; it never actuates.

References

  • Scheffer et al. 2009, Nature 461, 53 — early-warning signals for critical transitions (the slowing-down framework this complements).
  • Bandt & Pompe 2002, Phys. Rev. Lett. 88, 174102 — permutation entropy.
  • Gómez-Gardeñes, Gómez, Arenas & Moreno 2011, Phys. Rev. Lett. 106, 128701 — explosive synchronisation as a first-order transition.

Classes

ExplosiveSyncWarning dataclass

ExplosiveSyncWarning(
    window_starts: IntArray,
    entropy_index: FloatArray,
    per_node_entropy: FloatArray,
    robust_z: FloatArray,
    relative_drop: FloatArray,
    baseline_median: float,
    baseline_scale: float,
    n_baseline_windows: int,
    warning_triggered: bool,
    warning_window: int | None,
    warning_sample: int | None,
    dimension: int,
    delay: int,
    window: int,
    step: int,
    z_threshold: float,
    drop_threshold: float,
    persistence: int,
)

Result of an explosive-synchronisation early-warning sweep.

Attributes

window_starts : IntArray First sample index of each analysis window, shape (W,). entropy_index : FloatArray Mean transition entropy across nodes per window, shape (W,); the headline coherence-regularisation index. per_node_entropy : FloatArray Per-node transition entropy, shape (W, N) — the local field. robust_z : FloatArray Median / MAD robust z-score of entropy_index against the baseline, shape (W,). Strongly negative means a sharp drop. relative_drop : FloatArray Fractional drop of entropy_index below the baseline median, shape (W,). baseline_median : float Median entropy index over the leading baseline windows. baseline_scale : float Robust scale (1.4826 × MAD) of the baseline windows. n_baseline_windows : int Number of leading windows used to fit the baseline. warning_triggered : bool Whether a sustained drop crossed both the z and relative-drop gates. warning_window : int | None Index of the first window of the triggering run, or None. warning_sample : int | None Sample index window_starts[warning_window], or None. dimension, delay, window, step : int Echoed analysis parameters. z_threshold, drop_threshold : float Echoed alarm gates. persistence : int Echoed number of consecutive breaching windows required to alarm.

Methods:
summary
summary() -> dict[str, float | int | bool | None]

Return a flat scalar summary for logging or metric export.

Returns

dict[str, float | int | bool | None] Window/baseline counts, the baseline fit, the entropy-index and robust-z extremes, the maximum relative drop, and the alarm verdict.

Source code in src/scpn_phase_orchestrator/monitor/explosive_sync.py
def summary(self) -> dict[str, float | int | bool | None]:
    """Return a flat scalar summary for logging or metric export.

    Returns
    -------
    dict[str, float | int | bool | None]
        Window/baseline counts, the baseline fit, the entropy-index and
        robust-z extremes, the maximum relative drop, and the alarm verdict.
    """
    return {
        "n_windows": int(self.entropy_index.shape[0]),
        "n_baseline_windows": self.n_baseline_windows,
        "baseline_median": self.baseline_median,
        "baseline_scale": self.baseline_scale,
        "min_entropy_index": float(self.entropy_index.min())
        if self.entropy_index.size
        else 0.0,
        "min_robust_z": float(self.robust_z.min()) if self.robust_z.size else 0.0,
        "max_relative_drop": float(self.relative_drop.max())
        if self.relative_drop.size
        else 0.0,
        "warning_triggered": self.warning_triggered,
        "warning_window": self.warning_window,
        "warning_sample": self.warning_sample,
    }

Functions:

explosive_sync_warning

explosive_sync_warning(
    signals: FloatArray,
    *,
    dimension: int = DEFAULT_DIMENSION,
    delay: int = DEFAULT_DELAY,
    window: int = 128,
    step: int = 16,
    baseline_fraction: float = 0.25,
    min_baseline_windows: int = 3,
    z_threshold: float = 3.0,
    drop_threshold: float = 0.1,
    persistence: int = 2,
) -> ExplosiveSyncWarning

Sweep a multi-node signal for an explosive-synchronisation warning.

Parameters

signals : FloatArray Per-node scalar observables (e.g. phase velocity), shape (N, T); a one-dimensional array is treated as a single node. dimension, delay : int Ordinal-pattern embedding dimension D in [2, 7] and positive delay τ passed to transition_entropy. window : int Analysis window length in samples; must admit at least two ordinal transitions, i.e. window ≥ (D − 1)·τ + 3. step : int Hop between consecutive window starts in samples. baseline_fraction : float Leading fraction of windows used to fit the baseline, in (0, 1). min_baseline_windows : int Lower bound on the number of baseline windows. z_threshold : float Robust z-score magnitude below which a window breaches the gate. drop_threshold : float Minimum fractional drop below the baseline median to breach the gate. persistence : int Number of consecutive breaching windows required to raise the alarm.

Returns

ExplosiveSyncWarning The per-window entropy field, baseline fit, and the alarm decision.

Raises

ValueError If the inputs are malformed or the window does not fit the series.

Source code in src/scpn_phase_orchestrator/monitor/explosive_sync.py
def explosive_sync_warning(
    signals: FloatArray,
    *,
    dimension: int = DEFAULT_DIMENSION,
    delay: int = DEFAULT_DELAY,
    window: int = 128,
    step: int = 16,
    baseline_fraction: float = 0.25,
    min_baseline_windows: int = 3,
    z_threshold: float = 3.0,
    drop_threshold: float = 0.1,
    persistence: int = 2,
) -> ExplosiveSyncWarning:
    """Sweep a multi-node signal for an explosive-synchronisation warning.

    Parameters
    ----------
    signals : FloatArray
        Per-node scalar observables (e.g. phase velocity), shape ``(N, T)``;
        a one-dimensional array is treated as a single node.
    dimension, delay : int
        Ordinal-pattern embedding dimension ``D`` in ``[2, 7]`` and positive
        delay ``τ`` passed to ``transition_entropy``.
    window : int
        Analysis window length in samples; must admit at least two ordinal
        transitions, i.e. ``window ≥ (D − 1)·τ + 3``.
    step : int
        Hop between consecutive window starts in samples.
    baseline_fraction : float
        Leading fraction of windows used to fit the baseline, in ``(0, 1)``.
    min_baseline_windows : int
        Lower bound on the number of baseline windows.
    z_threshold : float
        Robust z-score magnitude below which a window breaches the gate.
    drop_threshold : float
        Minimum fractional drop below the baseline median to breach the gate.
    persistence : int
        Number of consecutive breaching windows required to raise the alarm.

    Returns
    -------
    ExplosiveSyncWarning
        The per-window entropy field, baseline fit, and the alarm decision.

    Raises
    ------
    ValueError
        If the inputs are malformed or the window does not fit the series.
    """
    array = _validate_signals(signals)
    dimension, delay = _validate_ordinal_params(dimension, delay)
    window = _validate_positive_int(window, "window")
    step = _validate_positive_int(step, "step")
    min_baseline_windows = _validate_positive_int(
        min_baseline_windows, "min_baseline_windows"
    )
    baseline_fraction = _validate_unit_fraction(baseline_fraction, "baseline_fraction")
    z_threshold = _validate_non_negative_real(z_threshold, "z_threshold")
    drop_threshold = _validate_non_negative_real(drop_threshold, "drop_threshold")
    persistence = _validate_positive_int(persistence, "persistence")

    n_nodes, n_samples = int(array.shape[0]), int(array.shape[1])
    min_window = (dimension - 1) * delay + 3
    if window < min_window:
        raise ValueError(
            f"window {window} must be at least (D - 1)·τ + 3 = {min_window} "
            "to admit two ordinal transitions"
        )
    if window > n_samples:
        raise ValueError(f"window {window} exceeds the series length {n_samples}")

    starts = list(range(0, n_samples - window + 1, step))
    n_windows = len(starts)
    window_starts = np.asarray(starts, dtype=np.int64)
    per_node = np.empty((n_windows, n_nodes), dtype=np.float64)
    for w, start in enumerate(starts):
        segment = array[:, start : start + window]
        for node in range(n_nodes):
            per_node[w, node] = transition_entropy(segment[node], dimension, delay)
    entropy_index = per_node.mean(axis=1)

    n_baseline = max(
        min_baseline_windows,
        int(np.ceil(baseline_fraction * n_windows)),
    )
    n_baseline = min(n_baseline, n_windows)
    baseline = entropy_index[:n_baseline]
    baseline_median = float(np.median(baseline)) if baseline.size else 0.0
    mad = float(np.median(np.abs(baseline - baseline_median))) if baseline.size else 0.0
    baseline_scale = _MAD_TO_STD * mad
    guarded_scale = max(baseline_scale, _SCALE_FLOOR)

    robust_z = (entropy_index - baseline_median) / guarded_scale
    if baseline_median > 0.0:
        relative_drop = (baseline_median - entropy_index) / baseline_median
    else:
        relative_drop = np.zeros_like(entropy_index)

    breaches = (
        (np.arange(n_windows) >= n_baseline)
        & (robust_z <= -z_threshold)
        & (relative_drop >= drop_threshold)
    )
    warning_window = _first_sustained_breach(breaches, persistence)
    warning_triggered = warning_window is not None
    warning_sample = (
        int(window_starts[warning_window]) if warning_window is not None else None
    )

    return ExplosiveSyncWarning(
        window_starts=window_starts,
        entropy_index=np.ascontiguousarray(entropy_index, dtype=np.float64),
        per_node_entropy=np.ascontiguousarray(per_node, dtype=np.float64),
        robust_z=np.ascontiguousarray(robust_z, dtype=np.float64),
        relative_drop=np.ascontiguousarray(relative_drop, dtype=np.float64),
        baseline_median=baseline_median,
        baseline_scale=baseline_scale,
        n_baseline_windows=n_baseline,
        warning_triggered=warning_triggered,
        warning_window=warning_window,
        warning_sample=warning_sample,
        dimension=dimension,
        delay=delay,
        window=window,
        step=step,
        z_threshold=z_threshold,
        drop_threshold=drop_threshold,
        persistence=persistence,
    )