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)