88obj.run()
99"""
1010
11- from abc import abstractmethod
11+ from abc import abstractmethod , ABC
1212from typing import Callable
13+ from eegnb .devices .eeg import EEG
1314from psychopy import prefs
1415from psychopy .visual .rift import Rift
1516#change the pref libraty to PTB and set the latency mode to high precision
2627from eegnb import generate_save_fn
2728
2829
29- class BaseExperiment :
30+ class BaseExperiment ( ABC ) :
3031
3132 def __init__ (self , exp_name , duration , eeg , save_fn , n_trials : int , iti : float , soa : float , jitter : float ,
32- use_vr = False , use_fullscr = True ):
33+ use_vr = False , use_fullscr = True , stereoscopic = False ):
3334 """ Initializer for the Base Experiment Class
3435
3536 Args:
37+ exp_name (str): Name of the experiment
38+ duration (float): Duration of the experiment in seconds
39+ eeg: EEG device object for recording
40+ save_fn (str): Save filename function for data
3641 n_trials (int): Number of trials/stimulus
3742 iti (float): Inter-trial interval
3843 soa (float): Stimulus on arrival
3944 jitter (float): Random delay between stimulus
4045 use_vr (bool): Use VR for displaying stimulus
46+ use_fullscr (bool): Use fullscreen mode
47+ stereoscopic (bool): Use stereoscopic rendering for VR
4148 """
4249
4350 self .exp_name = exp_name
44- self .instruction_text = """\n Welcome to the {} experiment!\n Stay still, focus on the centre of the screen, and try not to blink. \n This block will run for %s seconds.\n
45- Press spacebar to continue. \n """ .format (self .exp_name )
51+ self .instruction_text = None
4652 self .duration = duration
47- self .eeg = eeg
53+ self .eeg : EEG = eeg
4854 self .save_fn = save_fn
4955 self .n_trials = n_trials
5056 self .iti = iti
5157 self .soa = soa
5258 self .jitter = jitter
5359 self .use_vr = use_vr
60+ self .stereoscopic = stereoscopic
5461 if use_vr :
5562 # VR interface accessible by specific experiment classes for customizing and using controllers.
56- self .rift : Rift = visual .Rift (monoscopic = True , headLocked = True )
63+ self .rift : Rift = visual .Rift (monoscopic = not stereoscopic , headLocked = True )
64+ # eye for presentation
65+ if stereoscopic :
66+ self .left_eye_x_pos = 0.2
67+ self .right_eye_x_pos = - 0.2
68+ else :
69+ self .left_eye_x_pos = 0
70+ self .right_eye_x_pos = 0
71+
5772 self .use_fullscr = use_fullscr
5873 self .window_size = [1600 ,800 ]
5974
75+ # Initializing the record duration and the marker names
76+ self .record_duration = np .float32 (self .duration )
77+ self .markernames = [1 , 2 ]
78+
79+ # Setting up the trial and parameter list
80+ self .parameter = np .random .binomial (1 , 0.5 , self .n_trials )
81+ self .trials = DataFrame (dict (parameter = self .parameter , timestamp = np .zeros (self .n_trials )))
82+
6083 @abstractmethod
6184 def load_stimulus (self ):
6285 """
@@ -78,17 +101,20 @@ def present_stimulus(self, idx : int):
78101 """
79102 raise NotImplementedError
80103
81- def setup (self , instructions = True ):
104+ def present_iti (self ):
105+ """
106+ Method that presents the inter-trial interval display for the specific experiment.
82107
83- # Initializing the record duration and the marker names
84- self . record_duration = np . float32 ( self . duration )
85- self . markernames = [ 1 , 2 ]
86-
87- # Setting up the trial and parameter list
88- self . parameter = np . random . binomial ( 1 , 0.5 , self . n_trials )
89- self .trials = DataFrame ( dict ( parameter = self . parameter , timestamp = np . zeros ( self . n_trials )) )
108+ This method defines what is shown on the screen during the period between stimuli.
109+ It could be a blank screen, a fixation cross, or any other appropriate display.
110+
111+ This is an optional method - the default implementation simply flips the window with no additional content.
112+ Subclasses can override this method to provide custom ITI graphics.
113+ """
114+ self .window . flip ( )
90115
91- # Setting up Graphics
116+ def setup (self , instructions = True ):
117+ # Setting up Graphics
92118 self .window = (
93119 self .rift if self .use_vr
94120 else visual .Window (self .window_size , monitor = "testMonitor" , units = "deg" , fullscr = self .use_fullscr ))
@@ -98,7 +124,7 @@ def setup(self, instructions=True):
98124
99125 # Show Instruction Screen if not skipped by the user
100126 if instructions :
101- self .show_instructions ()
127+ return self .show_instructions ()
102128
103129 # Checking for EEG to setup the EEG stream
104130 if self .eeg :
@@ -113,7 +139,8 @@ def setup(self, instructions=True):
113139 print (
114140 f"No path for a save file was passed to the experiment. Saving data to { self .save_fn } "
115141 )
116-
142+ return True
143+
117144 def show_instructions (self ):
118145 """
119146 Method that shows the instructions for the specific Experiment
@@ -122,24 +149,30 @@ def show_instructions(self):
122149 """
123150
124151 # Splitting instruction text into lines
125- self .instruction_text = self .instruction_text % self .duration
152+ if self .instruction_text is None :
153+ self .instruction_text = """\n Welcome to the {} experiment!\n Stay still, focus on the centre of the screen, and try not to blink. \n This block will run for %s seconds.\n
154+ Press spacebar to continue. \n """ .format (self .exp_name ) % self .duration
126155
127156 # Disabling the cursor during display of instructions
128157 self .window .mouseVisible = False
129158
130159 # clear/reset any old key/controller events
131- self .__clear_user_input ()
160+ self ._clear_user_input ()
132161
133162 # Waiting for the user to press the spacebar or controller button or trigger to start the experiment
134- while not self .__user_input ('start' ):
163+ while not self ._user_input ('start' ):
135164 # Displaying the instructions on the screen
136165 text = visual .TextStim (win = self .window , text = self .instruction_text , color = [- 1 , - 1 , - 1 ])
137- self .__draw (lambda : self .__draw_instructions (text ))
166+ self ._draw (lambda : self .__draw_instructions (text ))
138167
139168 # Enabling the cursor again
140169 self .window .mouseVisible = True
141170
142- def __user_input (self , input_type ):
171+ if self ._user_input ('cancel' ):
172+ return False
173+ return True
174+
175+ def _user_input (self , input_type ):
143176 if input_type == 'start' :
144177 key_input = 'spacebar'
145178 vr_inputs = [
@@ -156,6 +189,9 @@ def __user_input(self, input_type):
156189 ('Xbox' , 'B' , None )
157190 ]
158191
192+ else :
193+ raise Exception (f'Invalid input_type: { input_type } ' )
194+
159195 if len (event .getKeys (keyList = key_input )) > 0 :
160196 return True
161197
@@ -193,10 +229,16 @@ def get_vr_input(self, vr_controller, button=None, trigger=False):
193229 return False
194230
195231 def __draw_instructions (self , text ):
196- text .draw ()
232+ if self .use_vr and self .stereoscopic :
233+ for eye , x_pos in [("left" , self .left_eye_x_pos ), ("right" , self .right_eye_x_pos )]:
234+ self .window .setBuffer (eye )
235+ text .pos = (x_pos , 0 )
236+ text .draw ()
237+ else :
238+ text .draw ()
197239 self .window .flip ()
198240
199- def __draw (self , present_stimulus : Callable ):
241+ def _draw (self , present_stimulus : Callable ):
200242 """
201243 Set the current eye position and projection for all given stimulus,
202244 then draw all stimulus and flip the window/buffer
@@ -207,7 +249,7 @@ def __draw(self, present_stimulus: Callable):
207249 self .window .setDefaultView ()
208250 present_stimulus ()
209251
210- def __clear_user_input (self ):
252+ def _clear_user_input (self ):
211253 event .getKeys ()
212254 self .clear_vr_input ()
213255
@@ -217,14 +259,61 @@ def clear_vr_input(self):
217259 """
218260 if self .use_vr :
219261 self .rift .updateInputState ()
262+
263+ def _run_trial_loop (self , start_time , duration ):
264+ """
265+ Run the trial presentation loop
266+
267+ This method handles the common trial presentation logic.
268+
269+ Args:
270+ start_time (float): Time when the trial loop started
271+ duration (float): Maximum duration of the trial loop in seconds
220272
221- def run (self , instructions = True ):
222- """ Do the present operation for a bunch of experiments """
273+ """
223274
224275 def iti_with_jitter ():
225276 return self .iti + np .random .rand () * self .jitter
226277
227- # Setup the experiment, alternatively could get rid of this line, something to think about
278+ # Initialize trial variables
279+ current_trial = trial_end_time = - 1
280+ trial_start_time = None
281+ rendering_trial = - 1
282+
283+ # Clear/reset user input buffer
284+ self ._clear_user_input ()
285+
286+ # Run the trial loop
287+ while (time () - start_time ) < duration :
288+ elapsed_time = time () - start_time
289+
290+ # Do not present stimulus until current trial begins(Adhere to inter-trial interval).
291+ if elapsed_time > trial_end_time :
292+ current_trial += 1
293+
294+ # Calculate timing for this trial
295+ trial_start_time = elapsed_time + iti_with_jitter ()
296+ trial_end_time = trial_start_time + self .soa
297+
298+ # Do not present stimulus after trial has ended(stimulus on arrival interval).
299+ if elapsed_time >= trial_start_time :
300+ # if current trial number changed present new stimulus.
301+ if current_trial > rendering_trial :
302+ # Stimulus presentation overwritten by specific experiment
303+ self ._draw (lambda : self .present_stimulus (current_trial ))
304+ rendering_trial = current_trial
305+ else :
306+ self ._draw (lambda : self .present_iti ())
307+
308+ if self ._user_input ('cancel' ):
309+ return False
310+
311+ return True
312+
313+ def run (self , instructions = True ):
314+ """ Run the experiment """
315+
316+ # Setup the experiment
228317 self .setup (instructions )
229318
230319 print ("Wait for the EEG-stream to start..." )
@@ -235,37 +324,11 @@ def iti_with_jitter():
235324
236325 print ("EEG Stream started" )
237326
238- # Run trial until a key is pressed or experiment duration has expired.
239- start = time ()
240- current_trial = current_trial_end = - 1
241- current_trial_begin = None
242-
243- # Current trial being rendered
244- rendering_trial = - 1
245-
246- # Clear/reset user input buffer
247- self .__clear_user_input ()
248-
249- while not self .__user_input ('cancel' ) and (time () - start ) < self .record_duration :
250-
251- current_experiment_seconds = time () - start
252- # Do not present stimulus until current trial begins(Adhere to inter-trial interval).
253- if current_trial_end < current_experiment_seconds :
254- current_trial += 1
255- current_trial_begin = current_experiment_seconds + iti_with_jitter ()
256- current_trial_end = current_trial_begin + self .soa
257-
258- # Do not present stimulus after trial has ended(stimulus on arrival interval).
259- elif current_trial_begin < current_experiment_seconds :
260-
261- # if current trial number changed get new choice of image.
262- if rendering_trial < current_trial :
263- # Some form of presenting the stimulus - sometimes order changed in lower files like ssvep
264- # Stimulus presentation overwritten by specific experiment
265- self .__draw (lambda : self .present_stimulus (current_trial ))
266- rendering_trial = current_trial
267- else :
268- self .__draw (lambda : self .window .flip ())
327+ # Record experiment until a key is pressed or duration has expired.
328+ record_start_time = time ()
329+
330+ # Run the trial loop
331+ self ._run_trial_loop (record_start_time , self .record_duration )
269332
270333 # Clearing the screen for the next trial
271334 event .clearEvents ()
0 commit comments