Skip to content

Commit f7d8c28

Browse files
committed
feat: add optode sync patch for monitor-based VEP timing
Adds use_optode=False constructor flag. When enabled on the monitor path, a 50x50px patch in the bottom-left corner alternates white/black with each checkerboard reversal. A photodiode on this corner produces a TTL pulse at actual photon time, equivalent to the compositor timestamps used on the VR path. Patch is suppressed in VR mode (compositor timestamps are used instead) and has no effect when use_optode=False, so existing sessions are unaffected.
1 parent 180275a commit f7d8c28

1 file changed

Lines changed: 25 additions & 1 deletion

File tree

eegnb/experiments/visual_vep/pattern_reversal_vep.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
class VisualPatternReversalVEP(BlockExperiment):
1515

1616
def __init__(self, display_refresh_rate: int, eeg: Optional[EEG] = None, save_fn=None,
17-
block_duration_seconds=50, block_trial_size: int=100, n_blocks: int=4, use_vr=False, use_fullscr=True):
17+
block_duration_seconds=50, block_trial_size: int=100, n_blocks: int=4, use_vr=False, use_fullscr=True,
18+
use_optode=False):
1819

1920
self.display_refresh_rate = display_refresh_rate
21+
self.use_optode = use_optode
2022
soa=0.5
2123
iti=0
2224
jitter=0
@@ -100,6 +102,21 @@ def load_stimulus(self) -> Dict[str, Any]:
100102
create_checkerboard = self.create_monitor_checkerboard
101103
size = (self.window_size[1], self.window_size[1])
102104

105+
# Optode sync patch: small white/black square in the bottom-left corner.
106+
# Alternates polarity with each checkerboard reversal so a photodiode
107+
# taped to this corner produces a TTL pulse on every stimulus onset.
108+
# Only created for the monitor path — VR uses compositor timestamps instead.
109+
if self.use_optode and not self.use_vr:
110+
patch_size = 50 # pixels
111+
x = -self.window.size[0] / 2 + patch_size / 2
112+
y = -self.window.size[1] / 2 + patch_size / 2
113+
self.optode_patch = visual.Rect(
114+
self.window, width=patch_size, height=patch_size,
115+
pos=(x, y), units='pix', fillColor='white'
116+
)
117+
else:
118+
self.optode_patch = None
119+
103120
# The surrounding / periphery needs to be dark when not using vr.
104121
# Also used for covering eye which is not being stimulated.
105122
self.black_background = visual.Rect(self.window,
@@ -215,6 +232,13 @@ def present_stimulus(self, idx: int):
215232
if self.use_vr:
216233
self.window.setBuffer(closed_eye)
217234
self.black_background.draw()
235+
236+
# Alternate sync patch polarity with each reversal so the photodiode
237+
# fires on every checkerboard flip, not just odd or even frames.
238+
if self.optode_patch is not None:
239+
self.optode_patch.fillColor = 'white' if checkerboard_frame == 0 else 'black'
240+
self.optode_patch.draw()
241+
218242
self.window.flip()
219243

220244
# Use compositor-reported predicted display time when available (VR path).

0 commit comments

Comments
 (0)