File size: 1,955 Bytes
eccd68f 676184b eccd68f | 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 | """Generate a deterministic synthetic MNE Raw fixture for EEG pipeline tests.
The fixture is committed to the repo alongside this script so test runs are
reproducible without re-running the script. Re-run only if the contract changes.
Channels: 4 EEG (Cz, Pz, O1, O2) + 1 EOG (EOG061).
Sampling rate: 256 Hz. Duration: 10 s.
Synthetic content: a 10 Hz alpha sine on each EEG channel, plus a 1.5 Hz EOG
"blink" injected on EOG061 and bleed-through on the frontal-most EEG channel
(Cz) so ICA has something to detect.
NOTE: byte-determinism of the .fif output is coupled to ``mne==1.7.1`` (pinned
in requirements.txt). If that pin is upgraded, re-run this script and commit
the rebuilt artifact alongside the dependency bump.
"""
from __future__ import annotations
from pathlib import Path
import mne
import numpy as np
def build() -> Path:
rng = np.random.default_rng(seed=42)
sfreq = 256.0
duration_s = 10.0
n_samples = int(sfreq * duration_s)
t = np.arange(n_samples) / sfreq
# Base alpha (10 Hz) + small white noise on every EEG channel.
eeg_alpha = np.sin(2 * np.pi * 10.0 * t)
eeg_noise = rng.standard_normal((4, n_samples)) * 1e-6
eeg = (eeg_alpha[None, :] * 1e-5) + eeg_noise
# EOG blink: low-frequency square-ish pulse train at ~1.5 Hz.
eog_pulse = (np.sin(2 * np.pi * 1.5 * t) > 0.95).astype(float) * 1e-4
# Bleed EOG into Cz (channel 0) so ICA finds an EOG-correlated component.
eeg[0] += 0.3 * eog_pulse
data = np.vstack([eeg, eog_pulse[None, :]]) # shape: (5, n_samples)
info = mne.create_info(
ch_names=["Cz", "Pz", "O1", "O2", "EOG061"],
sfreq=sfreq,
ch_types=["eeg", "eeg", "eeg", "eeg", "eog"],
)
raw = mne.io.RawArray(data, info, verbose="ERROR")
out = Path(__file__).parent / "eeg_sample.fif"
raw.save(out, overwrite=True, verbose="ERROR")
return out
if __name__ == "__main__":
p = build()
print(f"Wrote {p}")
|