File size: 2,484 Bytes
d37c079 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | """Synthetic 32-ch EEG source — lets us test the viewer and pipeline with no hardware.
Produces plausible-looking µV data: posterior alpha (~10 Hz), central mu, pink-ish
background, frontal eye-blinks, a bit of 50 Hz line noise, and one deliberately bad
channel so the quality panel has something to show.
"""
from __future__ import annotations
import numpy as np
from common.montage import CAP32_CHANNELS
_POSTERIOR = {"O1", "O2", "OZ", "PO3", "PO4", "P3", "P4", "P7", "P8", "PZ"}
_CENTRAL = {"C3", "C4", "CZ", "FC1", "FC2", "FC5", "FC6", "CP1", "CP2", "CP5", "CP6"}
_FRONTAL = {"FP1", "FP2", "AF3", "AF4"}
_BAD_CH = None # set to a channel name (e.g. "T7") to simulate a poor-contact electrode
class SynthCap:
def __init__(self, sfreq: float = 250.0, seed: int = 7):
self.sfreq = sfreq
self.ch = CAP32_CHANNELS
self.rng = np.random.default_rng(seed)
self.t = 0.0
# per-channel pink-noise state
self._b = np.zeros(len(self.ch))
def get_chunk(self, n: int) -> np.ndarray:
"""Return (32, n) µV."""
fs = self.sfreq
idx = np.arange(n)
t = self.t + idx / fs
out = np.zeros((len(self.ch), n))
for i, name in enumerate(self.ch):
# pink-ish background via leaky-integrated white noise (~8 µV)
w = self.rng.standard_normal(n)
b = np.empty(n)
prev = self._b[i]
for k in range(n):
prev = 0.97 * prev + 0.3 * w[k]
b[k] = prev
self._b[i] = prev
sig = 8.0 * b
# rhythms
if name in _POSTERIOR:
sig += 18.0 * np.sin(2 * np.pi * 10.0 * t + i) # strong alpha
elif name in _CENTRAL:
sig += 9.0 * np.sin(2 * np.pi * 11.0 * t + i * 0.5) # mu
elif name in _FRONTAL:
sig += 5.0 * np.sin(2 * np.pi * 9.5 * t + i)
# 50 Hz line noise (small)
sig += 1.5 * np.sin(2 * np.pi * 50.0 * t)
# occasional frontal blink
if name in _FRONTAL and self.rng.random() < 0.02:
sig += 80.0 * np.exp(-((idx - self.rng.integers(0, n)) ** 2) / (2 * (0.05 * fs) ** 2))
# bad channel: railed / very noisy
if name == _BAD_CH:
sig = 60.0 * self.rng.standard_normal(n) + 30.0 * np.sin(2 * np.pi * 50.0 * t)
out[i] = sig
self.t += n / fs
return out.astype(np.float32)
|