Skip to content

Streaming Merge Trigger

The causal per-sample counterpart of the batch merge-trigger orchestrator: a StreamingMergeTrigger engine whose push(phases_rad, positions_m, t_s) composes the MIF-003 merge-window streak (the LOCK_HOLD_CYCLES debounce), an incremental MIF-011 axial-separation envelope check (the absolute, dominant safety veto), and the arm/bank-ready gates the MIF-008 fabric receives as input wires. Per-sample cost is a fixed number of scalar operations over the n-channel state — no trajectory buffer, no allocation after construction on the Rust path.

from scpn_mif_core import (
    KinematicSafetySpec,
    MergeWindowSpec,
    StreamingTriggerSpec,
    dispatched_streaming_merge_trigger,
)

engine = dispatched_streaming_merge_trigger(
    StreamingTriggerSpec(
        merge_window=MergeWindowSpec(phase_tolerance_rad=0.05, spatial_tolerance_m=0.01),
        safety=KinematicSafetySpec(tolerance_m=0.02),
        bank_feasible=True,   # the MIF-005 verdict, latched at arm time
    )
)
for t_s, phases, positions in sensor_stream:
    sample = engine.push(phases, positions, t_s=t_s)
    if sample.decision != "hold_no_lock":
        break  # fire / abort_unsafe / abort_bank_infeasible — all latched

Decision semantics

Per-sample precedence mirrors the batch pipeline: an envelope violation latches ABORT_UNSAFE (dominant veto); a sustained lock then latches FIRE when bank-feasible or ABORT_BANK_INFEASIBLE when not; otherwise the engine holds. All three terminal decisions are one-shot latches — later samples update observables only.

Causal semantics versus the batch pipeline

The batch pipeline is retrospective: it certifies safety over the whole approach before deciding, so a violation after first lock still aborts the shot. The streaming engine is causal: it decides at each sample using only the past, and cannot un-fire a pulse that already left the fabric. On every trace whose first envelope violation does not come strictly after first lock, the final streaming decision equals the batch outcome (tests/unit/kinematic/test_streaming_trigger.py pins each shared decision class). The one divergence class — violation strictly after first lock, where the batch analysis reports ABORT_UNSAFE while the streaming engine has already fired — is the physical meaning of a real-time trigger and is pinned by its own test, not hidden.

Mapping to the RTL fabric

FIRE corresponds to the fabric's one-shot trigger_pulse; the debounce streak is the LOCK_HOLD_CYCLES rule; the safety veto is absolute and dominant, as in the fabric. The fabric cannot emit a bank-infeasibility diagnosis — it simply never fires while bank_ready is low — so ABORT_BANK_INFEASIBLE is the software-visible name for that silent state.

Backends

dispatched_streaming_merge_trigger follows bench/dispatch.toml (kinematic.streaming_trigger): the Rust engine (scpn_mif_core_rs.StreamingMergeTrigger) when the extension is available, with the pure-Python reference as the guaranteed floor. Parity is bit-exact — the decision sequence and every per-sample float observable are asserted identical across backends in tests/unit/kinematic/test_streaming_trigger_rust_parity.py. The in-process Rust per-push cost (without FFI overhead) is measured separately by the criterion benchmark scpn-mif-rs/crates/mif-kinematic/benches/streaming_trigger.rs; the Python-visible cost is measured by bench/kernels/bench_streaming_trigger.py and recorded in bench/results/streaming_trigger.json.

API

streaming_trigger

Causal per-sample merge-trigger decision engine.

:class:StreamingMergeTrigger is the software mirror of the MIF-008 trigger fabric's decision semantics: a per-sample :meth:~StreamingMergeTrigger.push that composes the MIF-003 merge-window streak (the LOCK_HOLD_CYCLES debounce), an incremental MIF-011 axial-separation envelope check (the absolute, dominant safety veto), and the arm/bank-ready gates the fabric receives as input wires. Per-sample cost is a fixed number of scalar operations over the n-channel state.

Relationship to the batch pipeline (evaluate_merge_trigger):

  • The batch pipeline is a retrospective analysis — it certifies safety over the whole approach before deciding, so a violation after first lock still aborts the shot.
  • This engine is causal — it decides at each sample using only the past. FIRE latches at the first sustained lock; a violation on a strictly later sample cannot un-fire a pulse that already left the fabric. On every trace whose first envelope violation does not come strictly after first lock, the final streaming decision equals the batch outcome; the divergence class is documented and tested, not hidden.

Relationship to the RTL: FIRE corresponds to the fabric's one-shot trigger_pulse. The fabric cannot emit a bank-infeasibility diagnosis — it simply never fires while bank_ready is low — so ABORT_BANK_INFEASIBLE is the software-visible name for that silent state, latched when a sustained lock is reached without a feasible pulse.

StreamingTriggerDecision

Bases: StrEnum

Per-sample decision emitted by the streaming trigger engine.

StreamingTriggerSpec(merge_window, safety, bank_feasible, armed=True) dataclass

Immutable configuration for a streaming trigger session.

Parameters

merge_window : MergeWindowSpec MIF-003 merge-window tolerances (phase, spatial, debounce streak). safety : KinematicSafetySpec MIF-011 sampled axial-separation envelope. bank_feasible : bool The MIF-005 feasibility verdict for the requested compression pulse, latched at arm time — the fabric's bank_ready input wire. armed : bool, optional Whether the lane is armed; an unarmed engine never fires (the fabric's arm input wire). Defaults to True.

StreamingTriggerSample(decision, window, separation_m, safety_slack_m, sample_index) dataclass

One evaluated sample: the latched decision plus per-sample observables.

Attributes

decision : StreamingTriggerDecision The (possibly latched) decision after this sample. window : MergeWindowSample The underlying merge-window evaluation for this sample. separation_m : float Axial separation max(z) - min(z) for this sample, in metres. safety_slack_m : float Envelope slack for this sample in metres (>= 0 is safe). For the first sample this is the initial margin tolerance - |separation|; afterwards it is the one-step envelope slack. sample_index : int Zero-based index of this sample in the session.

StreamingMergeTrigger(spec)

Causal streaming merge-trigger decision engine (see module docs).

decision property

Current (latched) decision.

first_fire_time_s property

Time of the sample that latched FIRE, or None.

first_violation_index property

Zero-based index of the first envelope violation, or None.

samples_seen property

Number of samples pushed so far.

reset()

Reset the engine to its post-construction state.

push(phases_rad, positions_m, t_s=None)

Evaluate one [phases, positions] sample and return the decision.

Decision precedence per sample mirrors the batch pipeline: an envelope violation latches ABORT_UNSAFE (dominant veto); a sustained lock then latches FIRE when bank-feasible or ABORT_BANK_INFEASIBLE when not; otherwise the engine holds. FIRE and both aborts are one-shot latches: once reached, later samples update observables only.

Parameters

phases_rad, positions_m : ArrayLike Per-channel phases (rad) and axial positions (m); equal lengths. t_s : float or None, optional Strictly increasing sample time in seconds, or None.

Returns

StreamingTriggerSample The decision and the per-sample observables.