Skip to content

Oscillators: P / I / S Channels

Three extraction channels convert domain signals into phase states. This is the universal interface through which any coupled-cycle system enters the SCPN Phase Orchestrator. The choice of channel determines how raw data becomes a phase on [0, 2pi), a frequency in rad/s, and a confidence score in [0, 1].

The key insight: once a signal is expressed as a PhaseState, the UPDE engine treats it identically regardless of origin. An EEG alpha wave (P), a packet arrival process (I), and a protocol state machine (S) all become entries in the same phase vector, coupled through the same K_nm matrix. Domain knowledge lives in the extractor and the binding spec, not in the engine.


P — Physical Channel

Input: Continuous waveform (voltage, pressure, displacement, acceleration, temperature oscillation — any real-valued time series with a dominant oscillatory component).

Method: Hilbert transform via the analytic signal:

a(t) = s(t) + i * H[s(t)]
theta(t) = arg(a(t))         — instantaneous phase
A(t) = |a(t)|                — instantaneous amplitude (envelope)
omega(t) = d(unwrap(theta))/dt  — instantaneous frequency

The Hilbert transform H[s] computes the imaginary part of the analytic signal. For a narrowband signal (e.g., EEG filtered to the alpha band 8-12 Hz), this yields a clean, slowly varying phase. For broadband signals, bandpass filtering before extraction is essential — the Hilbert transform of a broadband signal produces a noisy, rapidly varying phase.

Implementation detail: PhysicalExtractor applies optional zero-phase Butterworth band-pass filtering, then calls scipy.signal.hilbert at the original signal length. Optional edge trimming selects the final retained interior phase. The angular frequency is the median gradient of unwrapped phase multiplied by the sample rate, in rad/s.

Quality metric: Variation of the analytic envelope:

quality = clip(1 - std(abs(a)) / mean(abs(a)), 0, 1)

A mean envelope below 1e-15 gives zero quality. Envelope magnitudes and statistics use scaled arithmetic to avoid intermediate overflow for large, representable analytic envelopes. Filtering and the Hilbert transform must produce finite values; otherwise public extraction raises ValueError. This score measures envelope regularity, not a signal-to-noise power ratio; it does not establish physical observability.

When to use: - EEG, MEG, LFP (neural oscillations) - ECG (heartbeat waveform) - Accelerometer, gyroscope (mechanical vibration) - AC voltage/current (power systems) - Acoustic signals (machinery monitoring) - Any signal where the observable is a continuous oscillating quantity

When NOT to use: - Event streams (use I-channel instead) - Discrete state sequences (use S-channel instead) - Signals with no oscillatory component (e.g., monotonic trends)

Extractor: PhysicalExtractor in oscillators.physical.

Rust path: spo-oscillators::physical::extract_from_analytic is exposed as spo_kernel.physical_extract() through the FFI. The Rust implementation uses the same envelope and phase equations. Floating-point results agree within the owning tests' tolerances; this is not a bitwise replay guarantee.


I — Informational Channel

Input: Event timestamps (sorted ascending). Each timestamp marks the occurrence of a discrete event: a neural spike, a network packet arrival, a heartbeat R-peak, a job completion, a user click.

Method: A regular point process has an inherent frequency (its rate). The extractor represents the observed event train by its median instantaneous frequency and cumulative phase:

IEI_k = t_{k+1} - t_k           — inter-event interval
f_k = 1 / IEI_k                 — instantaneous frequency
omega = 2 * pi * median(f_k)    — angular frequency (robust to outliers)

The extractor returns the cumulative median-frequency phase over the observed train, not an interpolation between the two most recent events:

theta = (2 * pi * median(f_k) * (t_last - t_first)) mod (2 * pi)

Phase lies in [0, 2pi) and is invariant under a shift of timestamp origin. Amplitude is the mean instantaneous frequency. For an even interval count, median(1 / IEI) need not equal 1 / median(IEI): timestamps [0, 0.125, 0.375] yield 6 Hz, phase pi/2, amplitude 6 and quality 0.75.

Quality metric: Inverse coefficient of variation:

CV = std(IEI) / mean(IEI)
quality = 1 / (1 + CV)

Regular events (constant IEI, CV=0) score quality=1.0. Bursty or irregular events (high CV) score low. A Poisson process (CV=1) scores quality=0.5 — barely usable.

When to use: - Neural spike trains - Network packet arrivals - Heartbeat R-peaks (when only peak times are available, not the full waveform — otherwise use P-channel on the ECG signal) - Job completion events in distributed systems - User interaction events (clicks, keystrokes) - Any observable that is a point process

Implementation detail: InformationalExtractor handles edge cases: - Fewer than 2 events: returns quality=0 (no phase estimate possible). - Duplicate timestamps: ignore zero intervals; an all-identical train returns zero phase, frequency, amplitude and quality. No interval flooring is applied. - Non-sorted input: raise ValueError without modifying the timestamps. - Quality uses population standard deviation of the positive intervals.

Extractor: InformationalExtractor in oscillators.informational.

Rust path: spo-oscillators::informational::event_phase() is exposed via FFI. The extractor validates timestamps and removes duplicates before calling it. The raw kernel instead returns zero phase, frequency and quality for non-positive intervals, including duplicate or unsorted timestamps. ring_phase() belongs to the symbolic channel, not the informational module.


S — Symbolic Channel

Input: Sequence of integer state indices (ring labels wrap modulo N), observed at discrete time steps. Protocol states, workflow stages, Markov chain positions, categorical labels.

Method: Two modes:

Ring Mode (default)

Maps discrete state to equally spaced phases on the unit circle:

theta = (2 * pi * (s mod N) / N) mod (2 * pi)

State 0 maps to phase 0, state 1 to 2pi/N, etc. This preserves the circular topology — state N-1 is adjacent to state 0.

Frequency is computed from phase differences between consecutive observations:

omega = (theta_current - theta_previous) / dt

where the phase difference is wrapped to [-pi, pi) before division. The first frequency is zero; an exact half-turn selects the negative direction.

Graph Mode

Graph mode accumulates absolute linear index differences along the observed sequence, then normalises that cumulative distance:

theta = (2 * pi * cumulative_distance / total_distance) mod (2 * pi)

It does not use an adjacency graph. A singleton uses ring mapping; a stationary multi-state sequence produces zero phases. Label spacing therefore matters in graph mode; ring aliases are not graph-distance aliases.

Signed and unsigned 64-bit labels retain their values, including their full minimum-to-maximum spans. Narrower integer arrays promote within the same signedness. Graph differences and cumulative walk lengths use exact integer arithmetic before conversion to float64; they neither overflow signed subtraction nor saturate at machine-word capacity. The phase output itself is rounded to float64, so this is not an exact-rational phase representation.

Backend equivalence is numerical rather than bitwise: ring phases and graph qualities can differ in the last float64 bits for vocabularies above 2**53, because Rust converts the operands before division while Python rounds the integer quotient. Integer residues and graph distances still retain every bit.

Quality metric: Transition regularity:

Transition type Quality
Single step (k = 1) 1.0
Stall (k = 0) 0.2
Multi-step jump (k > 1) max(0.1, 1.0 - (k - 1) / N)
First observation initial_transition_quality, default 0.5

Graph distance k is the absolute linear index difference. Ring distance uses d = |s_new - s_old| mod N and k = min(d, N-d), including signed and out-of-vocabulary labels; a full cycle is a stall.

Low quality indicates the state machine is behaving unexpectedly (stalling, skipping states), which makes the phase estimate unreliable.

When to use: - TCP/protocol state machines - Workflow/pipeline stages - Markov chain position tracking - Categorical variable cycling (e.g., traffic light states) - Game/simulation state - Any observable that is a finite state machine with cyclic structure

Extractor: SymbolicExtractor in oscillators.symbolic.

Rust path: spo-oscillators::symbolic provides vector ring phases, graph-walk phases and linear transition qualities through the corresponding *_rust FFI functions. The public extractor scores cyclic ring quality in Python on both backend paths; without the kernel it also computes phases and graph quality in Python.

Both paths accept one-dimensional strided and read-only observations. Public input normalisation copies unaligned arrays before native access. Vocabulary sizes are integers ≥ 2 without a public upper bound; counts exceeding the native target's usize capacity use Python, even with the extension installed. The API integer contract and public diagnostic document these boundaries. The dated raw observations record each real execution path without an isolated speedup claim.


Mixed-Channel Systems

A single domain can use all three channels simultaneously. The binding spec oscillator_families dict maps family names to channels:

oscillator_families:
  heart_wave:
    channel: P
    extractor_type: physical
  spike_train:
    channel: I
    extractor_type: informational
  protocol_state:
    channel: S
    extractor_type: symbolic
    config:
      n_states: 8

All channels output PhaseState with identical fields. The UPDE engine operates on the unified phase vector — it does not distinguish channels. Cross-channel coupling (e.g., a P-channel EEG oscillator coupled to an I-channel spike train) is handled naturally through K_nm entries that span oscillator families.

Cross-Channel Coupling Considerations

Coupling between channels of different types requires care:

  • P-P coupling: natural. Both have continuous phase dynamics.
  • I-I coupling: natural. Both have event-driven phase.
  • P-I coupling: the I-channel phase is piecewise linear (resets at each event), while the P-channel phase is smooth. The coupling term sin(theta_P - theta_I) handles this correctly, but the effective coupling strength may need to be lower to avoid jitter from I-channel resets.
  • S-X coupling: symbolic phase is quantised. Coupling to continuous channels works but introduces discrete jumps in the coupling force. Higher n_states reduces quantisation effects.

Initial Phase Extraction

The extract_initial_phases() utility extracts PhaseState from all oscillator families in a binding spec given raw signal data:

from scpn_phase_orchestrator.oscillators import extract_initial_phases

states = extract_initial_phases(
    binding_spec=spec,
    signals={"heart_wave": ecg_data, "spike_train": spike_times},
    sample_rates={"heart_wave": 256.0, "spike_train": 30000.0},
)

Quality Gating

Extracted phases with quality < min_quality (default 0.3) are down-weighted by PhaseQualityScorer.downweight_mask(). The weight array multiplies into K_nm row-wise, effectively decoupling unreliable oscillators without removing them from the state vector.

Amplitude-weighted aggregate quality uses finite quality/amplitude pairs, a 1e-12 amplitude floor and weights scaled by their maximum before summation. This preserves finite means for amplitudes near the float64 limit in Python and Rust. It does not change the empirical gating or collapse thresholds.

Collapsed oscillators (quality < 0.1 for majority of states) trigger detect_collapse(), which the supervisor interprets as a DEGRADED or CRITICAL condition depending on the scope of collapse.

Quality Calibration

The default thresholds (0.3 for gating, 0.1 for collapse) are empirical. Domain-specific calibration:

  1. Run the system on representative data with quality logging enabled.
  2. Plot quality distributions per channel.
  3. Set min_quality at the point where phase estimates become visually unreliable (e.g., Hilbert phase no longer tracks the dominant oscillation).
  4. Set collapse threshold at the point where the majority of oscillators produce noise rather than signal.

See docs/ASSUMPTIONS.md § Quality Gating for the current defaults and their provenance.


Channel Selection Guide

Observable Channel Reason
EEG voltage P Continuous waveform with dominant oscillation
Neural spike times I Point process, not continuous
Heart rate from R-peaks I Event timestamps
ECG waveform P Continuous, use Hilbert on QRS complex
Network packet arrivals I Event timestamps
TCP state transitions S Finite state machine
Machine vibration P Continuous mechanical oscillation
Workflow stage S Discrete categorical
Stock price P Continuous (after detrending)
Trade events I Point process
Traffic light state S Cyclic discrete states
Respiratory waveform P Continuous
Servo motor position P Continuous angular signal

When in doubt: if the signal is continuous and oscillatory, use P. If it is a series of event times, use I. If it is a sequence of discrete states, use S.


References

  • [gabor1946] D. Gabor (1946). Theory of communication. J. IEE 93, 429-457. — Analytic signal and instantaneous phase (P channel).
  • [pikovsky2001] A. Pikovsky, M. Rosenblum & J. Kurths (2001). Synchronization: A Universal Concept in Nonlinear Sciences. Cambridge UP. — Phase extraction from time series, quality metrics.
  • [lachaux1999] J.-P. Lachaux et al. (1999). Measuring phase synchrony in brain signals. Human Brain Mapping 8, 194-208. — PLV definition used in quality gating.
  • [daley2003] D. J. Daley & D. Vere-Jones (2003). An Introduction to the Theory of Point Processes. Springer. — Point process theory underlying I-channel extraction.