Monitor — EVS (Entrainment)
Validation rejects object-dtype complex phase aliases before float coercion so the EVS battery remains defined on real trial-phase recordings before ITPC, persistence, and frequency-specificity evaluation.| phases_trials | NDArray[float64] | (N_trials, T) | 00, 2\pi)0 Finite real phase per trial per timepoint; boolean and complex aliases rejected |# Entrainment Verification Score (EVS)
1. Mathematical Formalism¶
Inter-Trial Phase Coherence (ITPC)¶
ITPC measures the consistency of phase across repeated trials at each time point:
where \(\theta_k(t)\) is the phase of trial \(k\) at time \(t\). ITPC ranges from 0 (uniform phase distribution) to 1 (identical phase across all trials).
ITPC is the circular analogue of correlation: it measures how consistently a stimulus evokes the same phase response across repetitions.
Mean ITPC¶
The overall ITPC is the mean across all time points:
ITPC Persistence¶
Persistence measures whether phase coherence survives after the stimulus stops:
where \(P\) is the set of time-point indices during and after the stimulus pause window. High persistence indicates genuine neural entrainment rather than a stimulus-locked artefact.
Frequency Specificity¶
The specificity ratio compares ITPC at the target (stimulus) frequency versus a control (non-stimulus) frequency:
Control phases are obtained by rescaling the target-frequency phases:
This models the assumption that the raw signal was bandpass-filtered at each frequency before phase extraction. If entrainment is frequency-specific (genuine), \(\text{Specificity} \gg 1\). If entrainment is broadband (artefact), \(\text{Specificity} \approx 1\).
EVS Decision Rule¶
A system is classified as entrained if and only if all three criteria pass:
Default thresholds: - \(\tau_{\text{itpc}} = 0.6\) (moderate phase coherence) - \(\tau_{\text{pers}} = 0.4\) (survives brief pause) - \(\tau_{\text{spec}} = 1.5\) (50% stronger at target than control)
2. Theoretical Context¶
Why Three Criteria?¶
Single measures of entrainment are insufficient:
-
High ITPC alone can be caused by stimulus-locked artefacts (volume conduction, electromagnetic pickup). The stimulus signal directly contaminates the recording without any neural response.
-
High persistence alone can arise from intrinsic oscillations that happen to be at the stimulus frequency. The oscillator is not entrained — it was already oscillating at that frequency.
-
High specificity alone can occur with weak entrainment that is frequency-specific but too weak to be physiologically meaningful.
The EVS battery requires all three to pass, dramatically reducing false positives.
Phase Entrainment in Neuroscience¶
Entrainment is the process by which an external periodic stimulus (auditory, visual, or electrical) causes endogenous neural oscillations to synchronise to the stimulus frequency. This is distinct from:
- Evoked responses: Transient, stimulus-locked, no sustained oscillation
- Steady-state responses (SSR): Driven by the stimulus, disappear immediately when stimulus stops
- Entrainment: Sustained phase-locking that persists briefly after stimulus cessation — indicating that endogenous oscillators have been "captured" by the stimulus
Applications¶
- Auditory entrainment: Speech rhythm (Giraud & Poeppel, 2012), binaural beats, isochronous tone sequences
- Visual entrainment: Steady-state visually evoked potentials (SSVEP)
- Transcranial alternating current stimulation (tACS): Entraining cortical oscillations for cognitive enhancement
- Brain-computer interfaces: SSVEP-based BCIs use frequency-specific entrainment for user intent decoding
Historical Context¶
- Pikovsky, A., Rosenblum, M., & Kurths, J. (2001): Synchronization: A Universal Concept in Nonlinear Sciences. Comprehensive theory of entrainment in oscillatory systems.
- Giraud, A.-L. & Poeppel, D. (2012): "Cortical oscillations and speech processing." Demonstrated that cortical theta entrains to speech rhythm.
- Lakatos, P. et al. (2008): "Entrainment of neuronal oscillations as a mechanism of attentional selection." Showed that attention modulates entrainment strength.
- Lachaux, J.-P. et al. (1999): "Measuring phase synchrony in brain signals." Introduced phase-locking value (PLV), closely related to ITPC.
- Thut, G. et al. (2011): "Entrainment of perceptually relevant brain oscillations by non-invasive rhythmic stimulation of the human brain." tACS entrainment evidence.
- Nozaradan, S. et al. (2011): "Tagging the neuronal entrainment to beat and meter." Frequency tagging approach to measuring entrainment.
Relation to Fluctara¶
The EVS module is the scientific core of the Fluctara audio entrainment engine. Fluctara generates binaural/monaural beats and isochronous pulses; the EVS monitor verifies whether the target brain oscillation has been successfully entrained by measuring ITPC, persistence, and frequency specificity from the resulting phase dynamics.
3. Pipeline Position¶
Stimulus generator (Fluctara / tACS / auditory)
│
↓
Phase extraction ──→ θ(t) per trial
│
↓ (n_trials × n_timepoints)
┌── EVSMonitor.evaluate() ──────────────────────────┐
│ │
│ 1. compute_itpc(phases_trials) → ITPC(t) │
│ → mean ITPC │
│ │
│ 2. itpc_persistence(phases_trials, pause_idx) │
│ → persistence score │
│ │
│ 3. _frequency_specificity(phases, f_target, f_ctrl)│
│ → specificity ratio (Rust if available) │
│ │
│ 4. Decision: all three ≥ thresholds? │
│ → EVSResult(itpc, persistence, specificity, │
│ is_entrained) │
│ │
└─────────────────────────────────────────────────────┘
│
↓
Supervisor / Fluctara controller adapts stimulus
Input Contracts¶
| Parameter | Type | Shape | Range | Meaning |
|---|---|---|---|---|
phases_trials |
NDArray[float64] |
(N_trials, T) |
finite real radians | Phase per trial per timepoint; coercive text, boolean, and complex aliases are rejected |
pause_indices |
list[int] \| NDArray |
(P,) |
normalised to unique values in \([0, T)\) | Timepoint indices in pause window; out-of-range values are ignored |
target_freq |
float |
scalar | \(> 0\) | Stimulus frequency (Hz) |
control_freq |
float |
scalar | \(> 0\) | Control frequency (Hz) |
Output Contract¶
@dataclass(frozen=True, slots=True)
class EVSResult:
itpc_value: float # Mean ITPC, ∈ [0, 1]
persistence_score: float # ITPC during/after pause, ∈ [0, 1]
specificity_ratio: float # target ITPC / control ITPC, ≥ 0
is_entrained: bool # All three criteria met
4. Features¶
- Three-criterion entrainment verification — ITPC + persistence + frequency specificity for robust false-positive rejection
- ITPC computation — circular mean resultant length across trials
- Persistence measurement — ITPC during stimulus pause window
- Frequency specificity — ratio of target/control ITPC via phase rescaling
- Configurable thresholds — all three thresholds adjustable
- Frozen dataclass result — immutable, hashable EVSResult
- Rust FFI for specificity — native mean_itpc computation whose scalar result is replayed against the canonical NumPy calculation before publication
- Inf handling — returns infinity when control ITPC is zero but target is non-zero
5. Usage Examples¶
Basic: Full EVS Battery¶
import numpy as np
from scpn_phase_orchestrator.monitor.evs import EVSMonitor
n_trials, n_tp = 20, 100
rng = np.random.default_rng(42)
# Simulated entrained phases: consistent across trials at target freq
target_phase = np.linspace(0, 4 * np.pi, n_tp)
noise = rng.normal(0, 0.3, (n_trials, n_tp))
phases = target_phase[np.newaxis, :] + noise
pause_idx = list(range(80, 100)) # Last 20% is pause
monitor = EVSMonitor(
itpc_threshold=0.5,
persistence_threshold=0.3,
specificity_threshold=1.2,
)
result = monitor.evaluate(phases, pause_idx, target_freq=10.0, control_freq=7.0)
print(f"ITPC: {result.itpc_value:.4f}")
print(f"Persistence: {result.persistence_score:.4f}")
print(f"Specificity: {result.specificity_ratio:.4f}")
print(f"Entrained: {result.is_entrained}")
Not Entrained: Random Phases¶
import numpy as np
from scpn_phase_orchestrator.monitor.evs import EVSMonitor
n_trials, n_tp = 20, 100
phases = np.random.default_rng(42).uniform(0, 2 * np.pi, (n_trials, n_tp))
monitor = EVSMonitor()
result = monitor.evaluate(phases, list(range(80, 100)), 10.0, 5.0)
assert not result.is_entrained # Random phases → no entrainment
print(f"ITPC: {result.itpc_value:.4f} (should be low)")
Comparing Entrainment Strengths¶
import numpy as np
from scpn_phase_orchestrator.monitor.evs import EVSMonitor
monitor = EVSMonitor()
rng = np.random.default_rng(42)
target = np.linspace(0, 4 * np.pi, 100)
pause = list(range(80, 100))
for noise_std in [0.1, 0.5, 1.0, 2.0]:
noise = rng.normal(0, noise_std, (20, 100))
phases = target[np.newaxis, :] + noise
result = monitor.evaluate(phases, pause, 10.0, 5.0)
print(f"Noise σ={noise_std:.1f}: ITPC={result.itpc_value:.3f}, "
f"Entrained={result.is_entrained}")
Integration with UPDEEngine¶
import numpy as np
from scpn_phase_orchestrator.upde.engine import UPDEEngine
from scpn_phase_orchestrator.monitor.evs import EVSMonitor
N = 16
eng = UPDEEngine(N, dt=0.01)
monitor = EVSMonitor()
rng = np.random.default_rng(42)
omegas = np.ones(N) * 2 * np.pi * 10.0 # 10 Hz target
knm = np.full((N, N), 0.5); np.fill_diagonal(knm, 0.0)
alpha = np.zeros((N, N))
# Collect trials
trials = []
for trial in range(20):
phases = rng.uniform(0, 2 * np.pi, N)
trial_phases = []
for step in range(100):
phases = eng.step(phases, omegas, knm, 0.0, 0.0, alpha)
trial_phases.append(phases[0]) # Track oscillator 0
trials.append(trial_phases)
phases_trials = np.array(trials)
result = monitor.evaluate(phases_trials, list(range(80, 100)), 10.0, 5.0)
print(f"Engine-based ITPC: {result.itpc_value:.4f}")
6. Technical Reference¶
Class: EVSMonitor¶
evs ¶
EVS and phase-locking metrics for finite two-dimensional phase recordings.
The module implements ITPC, persistence across pauses, and
frequency-specificity checks for Entrainment Verification Signals. A Rust
extension is used when available while the Python fallback remains the
reference-compatible path. Inputs are normalized to finite trials x time
phase arrays, pause indices are bounds-checked, and candidate frequency vectors
must match the trial axis before evidence is reported.
Classes¶
EVSResult
dataclass
¶
EVSResult(
itpc_value: float,
persistence_score: float,
specificity_ratio: float,
is_entrained: bool,
)
Entrainment verification outcome.
EVSMonitor ¶
EVSMonitor(
itpc_threshold: float = 0.6,
persistence_threshold: float = 0.4,
specificity_threshold: float = 1.5,
)
Combine ITPC, persistence, and frequency specificity into one score.
Three criteria must all pass for is_entrained=True:
- Mean ITPC across all time points >=
itpc_threshold - ITPC during/after stimulus pause >=
persistence_threshold - ITPC at the target frequency / ITPC at a control frequency
=
specificity_threshold
The specificity test distinguishes frequency-specific entrainment from broadband phase-locking artefacts.
Source code in src/scpn_phase_orchestrator/monitor/evs.py
Methods:¶
evaluate ¶
evaluate(
phases_trials: FloatArray,
pause_indices: list[int] | IntArray,
target_freq: float,
control_freq: float,
) -> EVSResult
Run the full EVS battery.
Parameters¶
phases_trials : FloatArray shape (n_trials, n_timepoints), phases in radians at the target frequency. pause_indices : list[int] | IntArray time-point indices within/after a stimulus pause window. target_freq : float stimulus frequency (Hz). control_freq : float non-stimulus control frequency (Hz).
Returns¶
EVSResult EVSResult with all three sub-scores and the overall verdict.
Source code in src/scpn_phase_orchestrator/monitor/evs.py
Functions:¶
Constructor Parameters¶
| Parameter | Type | Default | Meaning |
|---|---|---|---|
itpc_threshold |
float |
0.6 | Minimum mean ITPC |
persistence_threshold |
float |
0.4 | Minimum pause-window ITPC |
specificity_threshold |
float |
1.5 | Minimum target/control ratio |
Methods¶
| Method | Input | Output |
|---|---|---|
evaluate(phases_trials, pause_indices, target_freq, control_freq) |
Trial phases + params | EVSResult |
_frequency_specificity(phases_trials, target_freq, control_freq) |
Trial phases + freqs | float ratio |
Rust Engine Function¶
pub fn frequency_specificity(
phases_flat: &[f64], // row-major (n_trials × n_tp)
n_trials: usize,
n_timepoints: usize,
target_freq: f64,
control_freq: f64,
) -> f64 // target_itpc / control_itpc
Internal helper: mean_itpc(phases_flat, n_trials, n_tp) -> f64
computes the mean ITPC across timepoints using vectorised sin/cos
summation.
Auto-Select Logic¶
try:
from spo_kernel import frequency_specificity_rust as _rust_freq_spec
_HAS_RUST = True
except ImportError:
_HAS_RUST = False
Only _frequency_specificity invokes the dedicated Rust function. Its result
is accepted only when it matches the canonical NumPy replay. compute_itpc and
itpc_persistence are computed by the Python itpc module (which may have its
own independently verified native path).
7. Performance Benchmarks¶
Measured on Intel Core i5-11600K @ 3.90 GHz, 32 GB DDR4-2400. Median of 100-200 iterations, random phase data.
Isolated frequency_specificity kernel¶
| Trials × Timepoints | Python (µs) | Rust (µs) | Speedup |
|---|---|---|---|
| 10 × 50 | 42.6 | 20.1 | 2.1x |
| 20 × 100 | 121.0 | 85.4 | 1.4x |
| 50 × 200 | 569.6 | 534.9 | 1.1x |
These figures compare the isolated kernels. They are not the latency of the current publication path: native output is now followed by a complete NumPy replay, so native dispatch supplies independent fault detection rather than an end-to-end speedup claim.
Why Decreasing Speedup?¶
The Python path uses NumPy vectorised operations (np.exp(1j * phases),
np.abs, np.mean), which are BLAS-accelerated for large arrays.
The Rust path uses scalar sin/cos loops. At small sizes, Rust wins
via reduced Python overhead. At large sizes, NumPy's vectorisation
catches up.
Historical Full evaluate() Latency¶
The following 20 × 100 breakdown predates exact native-output replay and is retained only as historical profiling evidence; it must not be treated as a current latency guarantee.
| Component | Time (µs) | Fraction |
|---|---|---|
| compute_itpc | ~80 | ~35% |
| itpc_persistence | ~40 | ~18% |
| frequency_specificity | ~85 | ~37% |
| Decision logic | ~1 | ~0.5% |
| Total | ~230 | 100% |
Memory Usage¶
- Phase arrays: \(N_{\text{trials}} \times T\) floats
- Control phases: \(N_{\text{trials}} \times T\) floats (temporary)
- ITPC values: \(T\) floats
- Total for 20 × 100: ~35 KB
Test Coverage¶
- Rust tests: 6 (evs module in spo-engine)
- Perfect sync high specificity, random phases low specificity, zero frequencies, empty input, mean_itpc synchronised, mean_itpc uniform
- Python owner suite: 63 tests (
tests/test_evs.py) with exact 100% statement and branch coverage of the Python EVS module - EVSResult creation, entrained detection, pause-window normalisation, specificity replay, threshold sensitivity, edge cases, and pipeline wiring
8. Citations¶
-
Pikovsky, A., Rosenblum, M., & Kurths, J. (2001). Synchronization: A Universal Concept in Nonlinear Sciences. Cambridge University Press. ISBN: 978-0-521-59285-7.
-
Giraud, A.-L. & Poeppel, D. (2012). "Cortical oscillations and speech processing: Emerging computational principles and operations." Nature Neuroscience 15(4):511-517. DOI: 10.1038/nn.3063
-
Lakatos, P., Karmos, G., Mehta, A. D., Ulbert, I., & Schroeder, C. E. (2008). "Entrainment of neuronal oscillations as a mechanism of attentional selection." Science 320(5872):110-113. DOI: 10.1126/science.1154735
-
Lachaux, J.-P., Rodriguez, E., Martinerie, J., & Varela, F. J. (1999). "Measuring phase synchrony in brain signals." Human Brain Mapping 8(4):194-208. DOI: 10.1002/(SICI)1097-0193(1999)8:4<194::AID-HBM4>3.0.CO;2-C
-
Thut, G., Schyns, P. G., & Gross, J. (2011). "Entrainment of perceptually relevant brain oscillations by non-invasive rhythmic stimulation of the human brain." Frontiers in Psychology 2:170. DOI: 10.3389/fpsyg.2011.00170
-
Nozaradan, S., Peretz, I., Missal, M., & Mouraux, A. (2011). "Tagging the neuronal entrainment to beat and meter." Journal of Neuroscience 31(28):10234-10240. DOI: 10.1523/JNEUROSCI.0411-11.2011
-
Tallon-Baudry, C., Bertrand, O., Delpuech, C., & Pernier, J. (1996). "Stimulus specificity of phase-locked and non-phase-locked 40 Hz visual responses in human." Journal of Neuroscience 16(13):4240-4249.
-
Obleser, J. & Kayser, C. (2019). "Neural entrainment and attentional selection in the listening brain." Trends in Cognitive Sciences 23(11):913-926. DOI: 10.1016/j.tics.2019.08.004
Edge Cases and Limitations¶
Single Trial¶
With \(N_{\text{trials}} = 1\), ITPC = 1 at every time point (trivially — the single trial always agrees with itself). This produces a false positive. Minimum recommended: \(N_{\text{trials}} \geq 10\).
Very Short Pause Window¶
If pause_indices contains only 1-2 indices, the persistence
estimate has high variance. Recommend at least 10 pause indices.
Target ≈ Control Frequency¶
When \(f_{\text{target}} \approx f_{\text{control}}\), the phase rescaling produces nearly identical phases → specificity ≈ 1. Use control frequencies at least 30% different from target.
Negative or Zero Frequencies¶
Both target_freq and control_freq must be positive. If either
is ≤ 0, evaluation raises ValueError before backend dispatch.
Integration with Other SPO Modules¶
With Fluctara Audio Engine¶
The EVS monitor is the verification backend for Fluctara:
# Fluctara generates stimulus → EEG recorded → phases extracted
result = evs_monitor.evaluate(phases_trials, pause_idx, stim_freq, ctrl_freq)
if result.is_entrained:
fluctara.maintain_stimulus()
else:
fluctara.increase_amplitude()
With Sleep Staging¶
Entrainment efficacy varies by sleep stage: - Wake/N1: Best entrainment response - N2/N3: Reduced (endogenous slow oscillations compete) - REM: Variable (depends on cortical activation)
With SSGF Geometry Control¶
If entrainment verification fails despite strong coupling, the SSGF engine may need to adjust the coupling topology to facilitate frequency-specific synchronisation.
Troubleshooting¶
Issue: is_entrained Always False¶
Diagnosis: Check which criterion fails: - Low ITPC → phases are inconsistent across trials (too much noise) - Low persistence → entrainment is stimulus-locked, not endogenous - Low specificity → entrainment is broadband, not frequency-specific
Issue: Specificity is Infinity¶
Diagnosis: Control ITPC is near zero (< 1e-12) while target ITPC is positive. This is valid — it means strong frequency-specific entrainment with no broadband component.
Issue: High ITPC but Specificity ≈ 1¶
Diagnosis: The phase coherence is broadband (affects all frequencies equally). This suggests a stimulus artefact, not genuine neural entrainment.
Solution: Verify the phase extraction pipeline. Ensure bandpass filtering is applied before phase computation. Check for DC offset or saturation in the recording.
Issue: Persistence Higher Than Mean ITPC¶
Diagnosis: This can occur when the stimulus is disruptive (reduces ITPC during stimulation) but the endogenous oscillator resumes coherent oscillation during the pause. This is a valid but unusual pattern — it indicates "rebound entrainment."
Solution: This is not an error. The system is genuinely entrained but the measurement is capturing the post-stimulus recovery.
Mathematical Appendix: ITPC Statistical Significance¶
For random (unentraining) phases uniformly distributed on \([0, 2\pi)\), the expected ITPC follows a Rayleigh distribution:
The critical ITPC for \(p < 0.05\) significance with \(N\) trials:
| \(N_{\text{trials}}\) | \(z_{\text{crit}}\) (\(p < 0.05\)) |
|---|---|
| 10 | 0.547 |
| 20 | 0.387 |
| 50 | 0.245 |
| 100 | 0.173 |
The default threshold \(\tau_{\text{itpc}} = 0.6\) exceeds the \(p < 0.05\) critical value for all \(N \geq 10\), ensuring statistical rigour. For stricter testing, use \(p < 0.01\) thresholds or Bonferroni correction for multiple frequency bins.