Skip to content

Commit b16796b

Browse files
Merge pull request #303 from JohnGriffiths/eoec_exp
New resting-state eyes open/eyes-closed experiment
2 parents eaaf700 + e2606b7 commit b16796b

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

eegnb/experiments/rest/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+

eegnb/experiments/rest/eoec.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
2+
from typing import Optional
3+
from time import time
4+
5+
from psychopy import prefs
6+
7+
prefs.hardware["audioLib"] = "PTB"
8+
prefs.hardware["audioLatencyMode"] = 3
9+
10+
from psychopy import sound, visual
11+
from pylsl import StreamInfo, StreamOutlet
12+
13+
try: # pyserial is optional
14+
import serial
15+
except Exception: # pragma: no cover - handled gracefully
16+
serial = None
17+
18+
import numpy as np
19+
from pandas import DataFrame
20+
21+
from eegnb.devices.eeg import EEG
22+
from eegnb.experiments import Experiment
23+
24+
25+
class RestEyesOpenCloseAlternating(Experiment.BaseExperiment):
26+
"""
27+
Resting-state experiment with alternating eyes-open / eyes-closed blocks,
28+
and minimal visual and auditory cues to assist with task instructions.
29+
"""
30+
31+
def __init__(
32+
self,
33+
duration: Optional[float] = None,
34+
eeg: Optional[EEG] = None,
35+
save_fn: Optional[str] = None,
36+
block_duration: float = 60.0,
37+
n_cycles: int = 5,
38+
serial_port: Optional[str] = None,
39+
use_verbal_cues: bool = False,
40+
open_audio: Optional[str] = None,
41+
close_audio: Optional[str] = None
42+
):
43+
44+
exp_name = "Rest Eyes Open/Closed Alternating"
45+
if duration is None:
46+
duration = block_duration * 2 * n_cycles
47+
self.block_duration = block_duration
48+
self.n_cycles = n_cycles
49+
self.serial_port = serial_port
50+
self.use_verbal_cues = use_verbal_cues
51+
self.open_audio = open_audio
52+
self.close_audio = close_audio
53+
self.serial = None
54+
self.outlet = None
55+
self.open_sound = None
56+
self.close_sound = None
57+
super().__init__(
58+
exp_name,
59+
duration,
60+
eeg,
61+
save_fn,
62+
n_trials=2 * n_cycles,
63+
iti=0,
64+
soa=block_duration,
65+
jitter=0,
66+
)
67+
68+
69+
def load_stimulus(self):
70+
self.fixation = visual.TextStim(win=self.window, text="+", height=1.0)
71+
self.close_text = visual.TextStim(win=self.window, text="Close your eyes", height=1.0)
72+
return [self.fixation, self.close_text]
73+
74+
75+
def setup(self, instructions: bool = True):
76+
# recompute number of trials if duration was changed after init
77+
self.n_cycles = max(1, int(self.duration // (2 * self.block_duration)))
78+
self.n_trials = self.n_cycles * 2
79+
super().setup(instructions)
80+
81+
# overwrite trial sequence to alternate between open (0) and closed (1)
82+
parameter = np.tile([0, 1], self.n_cycles)
83+
self.trials = DataFrame(
84+
dict(parameter=parameter, timestamp=np.zeros(self.n_trials))
85+
)
86+
87+
# LSL outlet for markers
88+
info = StreamInfo("Markers", "Markers", 1, 0, "int32", "eyeclosure-baseline")
89+
self.outlet = StreamOutlet(info)
90+
91+
# serial connection for hardware triggers
92+
if self.serial_port and serial is not None:
93+
try:
94+
self.serial = serial.Serial(self.serial_port, 115200, timeout=1)
95+
except Exception: # pragma: no cover
96+
self.serial = None
97+
98+
# sounds for block transitions
99+
if self.use_verbal_cues and self.open_audio and self.close_audio:
100+
self.open_sound = sound.Sound(self.open_audio)
101+
self.close_sound = sound.Sound(self.close_audio)
102+
else:
103+
self.open_sound = sound.Sound(440, secs=0.2)
104+
self.close_sound = sound.Sound(330, secs=0.2)
105+
106+
107+
def present_stimulus(self, idx: int):
108+
label = self.trials["parameter"].iloc[idx] # 0 open, 1 closed
109+
if self.trials["timestamp"].iloc[idx] == 0:
110+
timestamp = time()
111+
self.trials.at[idx, "timestamp"] = timestamp
112+
self.outlet.push_sample([self.markernames[label]], timestamp)
113+
if self.eeg:
114+
marker = (
115+
[self.markernames[label]]
116+
if self.eeg.backend == "muselsl"
117+
else self.markernames[label]
118+
)
119+
self.eeg.push_sample(marker=marker, timestamp=timestamp)
120+
if self.serial:
121+
try:
122+
self.serial.write(bytes([self.markernames[label]]))
123+
except Exception: # pragma: no cover
124+
pass
125+
if label == 0:
126+
self.open_sound.play()
127+
else:
128+
self.close_sound.play()
129+
130+
if label == 0:
131+
self.fixation.draw()
132+
else:
133+
self.close_text.draw()
134+
self.window.flip()
135+
136+
def run(self, instructions: bool = True):
137+
try:
138+
super().run(instructions)
139+
finally:
140+
if self.serial:
141+
self.serial.close()
142+
143+
144+

0 commit comments

Comments
 (0)