From 1634e81277c1edb79183591d5b49fccdc553299d Mon Sep 17 00:00:00 2001 From: Parvfect Date: Mon, 30 May 2022 18:38:30 +0100 Subject: [PATCH 1/7] First commit --- eegnb/experiments/Experment.py | 79 ++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 eegnb/experiments/Experment.py diff --git a/eegnb/experiments/Experment.py b/eegnb/experiments/Experment.py new file mode 100644 index 00000000..872c32d4 --- /dev/null +++ b/eegnb/experiments/Experment.py @@ -0,0 +1,79 @@ + + + +class Experiment: + + def __init_(): + + + def present(duration=120, eeg: EEG=None, save_fn=None, n_trials=2010, exp_name=""): + """ Do the present operation for a bunch of experiments """ + + # Setup Trial list -> Common in most + parameter = np.random.binomial(1, 0.5, n_trials) + trials = DataFrame(dict(parameter=parameter, timestamp=np.zeros(n_trials))) + + # Setup Graphics + mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) + """ Does specific thing within Experiment Type to load stimulus data """ + + # Show Instruction Screen + show_instructions(duration=duration) + + # Start EEG Stream, wait for signal to settle, and then pull timestamp for start point + if eeg: + if save_fn is None: # If no save_fn passed, generate a new unnamed save file + random_id = random.randint(1000,10000) + save_fn = generate_save_fn(eeg.device_name, "visual_n170", random_id, random_id, "unnamed") + print( + f"No path for a save file was passed to the experiment. Saving data to {save_fn}" + ) + eeg.start(save_fn, duration=record_duration + 5) + + start = time() + + # Iterate through the events + for ii, trial in trials.iterrows(): + + # Intertrial interval + core.wait(iti + np.random.rand() * jitter) + + # Some form of presenting the stimulus - sometimes order changed in lower files like ssvep + + # Push sample + if eeg: + timestamp = time() + if eeg.backend == "muselsl": + marker = [markernames[label]] + else: + marker = markernames[label] + eeg.push_sample(marker=marker, timestamp=timestamp) + + # Offset + mywin.flip() + if len(event.getKeys()) > 0 or (time() - start) > record_duration: + break + event.clearEvents() + + + # Close the EEG stream + if eeg: + eeg.stop() + + def show_instructions(instruction_text): + + instruction_text = instruction_text % duration + + # graphics + mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) + + mywin.mouseVisible = False + + # Instructions + text = visual.TextStim(win=mywin, text=instruction_text, color=[-1, -1, -1]) + text.draw() + mywin.flip() + event.waitKeys(keyList="space") + + mywin.mouseVisible = True + mywin.close() \ No newline at end of file From d48093adef7094815ea4625b4f6bb1f5c477db9a Mon Sep 17 00:00:00 2001 From: Parvfect Date: Mon, 30 May 2022 18:43:52 +0100 Subject: [PATCH 2/7] Second commit --- eegnb/experiments/Experment.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/eegnb/experiments/Experment.py b/eegnb/experiments/Experment.py index 872c32d4..51ddd18e 100644 --- a/eegnb/experiments/Experment.py +++ b/eegnb/experiments/Experment.py @@ -1,5 +1,20 @@ +""" +Right now the quesitons are simple + +1. Do we want to have as much code as possible in the master class, somewhat like a main function that works with generic types +being passed in? EG. Experiment class has all the code, specific functions, variables, events and stimuli are passed in that are +called according to the stage of the cycle + +2. Do we want to just have a common set of shared data members in the form of a data class as per Issue 76? + +3. Do we want to split the main piece of code into a lot of functions like settng up trials, graphics, etc? + +4. How different are the next experiments that are going to be incorporated be? Will they be able to stick to such a protocol? + +""" + class Experiment: From f05f1c4919c5f635242c0c8cb574e259ed84a0e8 Mon Sep 17 00:00:00 2001 From: Parvfect Date: Sun, 5 Jun 2022 00:25:17 +0100 Subject: [PATCH 3/7] Modifications --- .../{Experment.py => Experiment.py} | 47 ++++++++++++------- eegnb/experiments/visual_n170/n170.py | 40 ++++++++++++++-- 2 files changed, 66 insertions(+), 21 deletions(-) rename eegnb/experiments/{Experment.py => Experiment.py} (68%) diff --git a/eegnb/experiments/Experment.py b/eegnb/experiments/Experiment.py similarity index 68% rename from eegnb/experiments/Experment.py rename to eegnb/experiments/Experiment.py index 51ddd18e..af32453f 100644 --- a/eegnb/experiments/Experment.py +++ b/eegnb/experiments/Experiment.py @@ -18,32 +18,46 @@ class Experiment: - def __init_(): - + def __init_(self): + + # Should have overwriting property for different experiments + self.instruction_text = """\nWelcome to the {} experiment!\nStay still, focus on the centre of the screen, and try not to blink. \nThis block will run for %s seconds.\n + Press spacebar to continue. \n""".format(exp_name) + + + def setup(self): - def present(duration=120, eeg: EEG=None, save_fn=None, n_trials=2010, exp_name=""): - """ Do the present operation for a bunch of experiments """ - - # Setup Trial list -> Common in most - parameter = np.random.binomial(1, 0.5, n_trials) - trials = DataFrame(dict(parameter=parameter, timestamp=np.zeros(n_trials))) + # Setup Trial list -> Common in most (csv in Unicorn) + self.parameter = np.random.binomial(1, 0.5, n_trials) + self.trials = DataFrame(dict(parameter=parameter, timestamp=np.zeros(n_trials))) # Setup Graphics mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) """ Does specific thing within Experiment Type to load stimulus data """ - + + # Needs to be overwritten by specific experiment + self.stim = self.load_stimulus() + # Show Instruction Screen - show_instructions(duration=duration) + self.show_instructions(duration=duration) - # Start EEG Stream, wait for signal to settle, and then pull timestamp for start point - if eeg: - if save_fn is None: # If no save_fn passed, generate a new unnamed save file + # Establish save function + if self.save_fn is None: # If no save_fn passed, generate a new unnamed save file random_id = random.randint(1000,10000) - save_fn = generate_save_fn(eeg.device_name, "visual_n170", random_id, random_id, "unnamed") + self.save_fn = generate_save_fn(eeg.device_name, experiement_id, random_id, random_id, "unnamed") print( f"No path for a save file was passed to the experiment. Saving data to {save_fn}" ) - eeg.start(save_fn, duration=record_duration + 5) + + + + def present(self, duration=120, eeg: EEG=None, save_fn=None, n_trials=2010, exp_name=""): + """ Do the present operation for a bunch of experiments """ + + + # Start EEG Stream, wait for signal to settle, and then pull timestamp for start point + if eeg: + eeg.start(save_fn, duration=record_duration + 5) start = time() @@ -75,9 +89,10 @@ def present(duration=120, eeg: EEG=None, save_fn=None, n_trials=2010, exp_name=" if eeg: eeg.stop() + def show_instructions(instruction_text): - instruction_text = instruction_text % duration + self.instruction_text = self.instruction_text % duration # graphics mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) diff --git a/eegnb/experiments/visual_n170/n170.py b/eegnb/experiments/visual_n170/n170.py index 3f9822bd..a12ca74d 100644 --- a/eegnb/experiments/visual_n170/n170.py +++ b/eegnb/experiments/visual_n170/n170.py @@ -18,9 +18,40 @@ from eegnb.devices.eeg import EEG from eegnb.stimuli import FACE_HOUSE + + + __title__ = "Visual N170" +def load_stimulus(): + def load_image(fn): + return visual.ImageStim(win=mywin, image=fn) + faces = list(map(load_image, glob(os.path.join(FACE_HOUSE, "faces", "*_3.jpg")))) + houses = list(map(load_image, glob(os.path.join(FACE_HOUSE, "houses", "*.3.jpg")))) + stim = [houses, faces] + return stim + +instruction_text = """ + Welcome to the N170 experiment! + + Stay still, focus on the centre of the screen, and try not to blink. + + This block will run for %s seconds. + + Press spacebar to continue. + + """ + + +if __name__ == "__main__": + + test = Experiment() + test.instruction_text = instruction_text + test.load_stimulus = load_stimulus + test.run() + + def present(duration=120, eeg: EEG=None, save_fn=None, n_trials = 2010, iti = 0.4, soa = 0.3, jitter = 0.2): @@ -39,9 +70,6 @@ def load_image(fn): # Setup graphics mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) - faces = list(map(load_image, glob(os.path.join(FACE_HOUSE, "faces", "*_3.jpg")))) - houses = list(map(load_image, glob(os.path.join(FACE_HOUSE, "houses", "*.3.jpg")))) - stim = [houses, faces] # Show the instructions screen show_instructions(duration) @@ -63,6 +91,7 @@ def load_image(fn): # Inter trial interval core.wait(iti + np.random.rand() * jitter) + # Select and display image label = trials["image_type"].iloc[ii] image = choice(faces if label == 1 else houses) @@ -97,7 +126,7 @@ def load_image(fn): def show_instructions(duration): instruction_text = """ - Welcome to the N170 experiment! + Welcome to the {} experiment! Stay still, focus on the centre of the screen, and try not to blink. @@ -105,7 +134,8 @@ def show_instructions(duration): Press spacebar to continue. - """ + """.format(self.experiment_id) + instruction_text = instruction_text % duration # graphics From 38993e6a5cf7c0e9f4b35556be8c5cb70a117e73 Mon Sep 17 00:00:00 2001 From: Parvfect Date: Sun, 5 Jun 2022 00:47:33 +0100 Subject: [PATCH 4/7] Lol --- eegnb/experiments/Experiment_readme.txt | 0 eegnb/experiments/visual_p300/p300.py | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 eegnb/experiments/Experiment_readme.txt diff --git a/eegnb/experiments/Experiment_readme.txt b/eegnb/experiments/Experiment_readme.txt new file mode 100644 index 00000000..e69de29b diff --git a/eegnb/experiments/visual_p300/p300.py b/eegnb/experiments/visual_p300/p300.py index b8176205..7d6f5853 100644 --- a/eegnb/experiments/visual_p300/p300.py +++ b/eegnb/experiments/visual_p300/p300.py @@ -12,6 +12,29 @@ __title__ = "Visual P300" +instruction_text = """ + Welcome to the P300 experiment! + + Stay still, focus on the centre of the screen, and try not to blink. + + This block will run for %s seconds. + + Press spacebar to continue. + + """ + +def load_stimulus(): + pass + +def present_stimulus(): + pass + + +if __name__ == "__main__": + + test = Experiment() + + def present(duration=120, eeg=None, save_fn=None): n_trials = 2010 From ba01229a93040daf916ca3c61908811b180996bc Mon Sep 17 00:00:00 2001 From: Parvfect Date: Sun, 5 Jun 2022 00:47:55 +0100 Subject: [PATCH 5/7] Lol --- eegnb/experiments/Experiment_readme.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/eegnb/experiments/Experiment_readme.txt b/eegnb/experiments/Experiment_readme.txt index e69de29b..acaa44b0 100644 --- a/eegnb/experiments/Experiment_readme.txt +++ b/eegnb/experiments/Experiment_readme.txt @@ -0,0 +1,13 @@ + + +Looking for a general implementation structure where base class implements and passes the following functions, + +def load_stimulus() -> stim (some form of dd array) + +def present_stimulus() -> given trial details does specific thing for experiment + +** Slight issue is that a lot of parameters will have to be passed which is not the best in practice + +Stuff that can be overwritten in general ... +instruction_text +parameter/trial From 42d3f2634d01e25e5f5e2fd618c70284b055848c Mon Sep 17 00:00:00 2001 From: Parvfect Date: Thu, 9 Jun 2022 21:43:20 +0100 Subject: [PATCH 6/7] Incorporated N170 and p300, looking good for a PR --- eegnb/experiments/visual_n170/n170.py | 143 +++++--------------------- 1 file changed, 23 insertions(+), 120 deletions(-) diff --git a/eegnb/experiments/visual_n170/n170.py b/eegnb/experiments/visual_n170/n170.py index a12ca74d..90897756 100644 --- a/eegnb/experiments/visual_n170/n170.py +++ b/eegnb/experiments/visual_n170/n170.py @@ -17,137 +17,40 @@ from eegnb import generate_save_fn from eegnb.devices.eeg import EEG from eegnb.stimuli import FACE_HOUSE - - - - -__title__ = "Visual N170" +from Experiment import Experiment def load_stimulus(): - def load_image(fn): - return visual.ImageStim(win=mywin, image=fn) + + load_image = lambda fn: visual.ImageStim(win=mywin, image=fn) + faces = list(map(load_image, glob(os.path.join(FACE_HOUSE, "faces", "*_3.jpg")))) houses = list(map(load_image, glob(os.path.join(FACE_HOUSE, "houses", "*.3.jpg")))) - stim = [houses, faces] - return stim - -instruction_text = """ - Welcome to the N170 experiment! - - Stay still, focus on the centre of the screen, and try not to blink. - - This block will run for %s seconds. - Press spacebar to continue. + return [houses, faces] - """ +def present_stimulus(trials, ii, eeg, markernames): + + label = trials["image_type"].iloc[ii] + image = choice(faces if label == 1 else houses) + image.draw() + # Push sample + if eeg: + timestamp = time() + if eeg.backend == "muselsl": + marker = [markernames[label]] + else: + marker = markernames[label] + eeg.push_sample(marker=marker, timestamp=timestamp) + if __name__ == "__main__": - test = Experiment() + test = Experiment("Visual N170") test.instruction_text = instruction_text test.load_stimulus = load_stimulus - test.run() - - -def present(duration=120, eeg: EEG=None, save_fn=None, - n_trials = 2010, iti = 0.4, soa = 0.3, jitter = 0.2): - - record_duration = np.float32(duration) - markernames = [1, 2] - - # Setup trial list - image_type = np.random.binomial(1, 0.5, n_trials) - trials = DataFrame(dict(image_type=image_type, timestamp=np.zeros(n_trials))) - - def load_image(fn): - return visual.ImageStim(win=mywin, image=fn) - - # start the EEG stream, will delay 5 seconds to let signal settle - - # Setup graphics - mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) - - - # Show the instructions screen - show_instructions(duration) - - if eeg: - if save_fn is None: # If no save_fn passed, generate a new unnamed save file - random_id = random.randint(1000,10000) - save_fn = generate_save_fn(eeg.device_name, "visual_n170", random_id, random_id, "unnamed") - print( - f"No path for a save file was passed to the experiment. Saving data to {save_fn}" - ) - eeg.start(save_fn, duration=record_duration + 5) - - # Start EEG Stream, wait for signal to settle, and then pull timestamp for start point - start = time() - - # Iterate through the events - for ii, trial in trials.iterrows(): - # Inter trial interval - core.wait(iti + np.random.rand() * jitter) - - - # Select and display image - label = trials["image_type"].iloc[ii] - image = choice(faces if label == 1 else houses) - image.draw() - - # Push sample - if eeg: - timestamp = time() - if eeg.backend == "muselsl": - marker = [markernames[label]] - else: - marker = markernames[label] - eeg.push_sample(marker=marker, timestamp=timestamp) - - mywin.flip() - - # offset - core.wait(soa) - mywin.flip() - if len(event.getKeys()) > 0 or (time() - start) > record_duration: - break - - event.clearEvents() - - # Cleanup - if eeg: - eeg.stop() - - mywin.close() - - -def show_instructions(duration): - - instruction_text = """ - Welcome to the {} experiment! - - Stay still, focus on the centre of the screen, and try not to blink. - - This block will run for %s seconds. - - Press spacebar to continue. - - """.format(self.experiment_id) - - instruction_text = instruction_text % duration - - # graphics - mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) - - mywin.mouseVisible = False - - # Instructions - text = visual.TextStim(win=mywin, text=instruction_text, color=[-1, -1, -1]) - text.draw() - mywin.flip() - event.waitKeys(keyList="space") + test.present_stimulus = present_stimulus + test.setup() + test.present() - mywin.mouseVisible = True - mywin.close() From 76c5036abc012074e770ea5fe3f0f0accd4f37a3 Mon Sep 17 00:00:00 2001 From: Parvfect Date: Thu, 9 Jun 2022 21:51:33 +0100 Subject: [PATCH 7/7] ssvep update --- eegnb/experiments/Experiment.py | 69 ++++++------- eegnb/experiments/visual_p300/p300.py | 129 ++++-------------------- eegnb/experiments/visual_ssvep/ssvep.py | 104 ++++++------------- 3 files changed, 81 insertions(+), 221 deletions(-) diff --git a/eegnb/experiments/Experiment.py b/eegnb/experiments/Experiment.py index af32453f..30164acc 100644 --- a/eegnb/experiments/Experiment.py +++ b/eegnb/experiments/Experiment.py @@ -1,39 +1,43 @@ +""" +Initial run of the Experiment Class Refactor base class +Derived classes have to set a few things in major: +1. load_stimulus function : returns an array of stimuli +2. present_stimulus function : presents the stimuli and pushes eeg data back and forth as needed +Additional parameters can be set from the derived class as per the initializer """ -Right now the quesitons are simple - -1. Do we want to have as much code as possible in the master class, somewhat like a main function that works with generic types -being passed in? EG. Experiment class has all the code, specific functions, variables, events and stimuli are passed in that are -called according to the stage of the cycle - -2. Do we want to just have a common set of shared data members in the form of a data class as per Issue 76? - -3. Do we want to split the main piece of code into a lot of functions like settng up trials, graphics, etc? - -4. How different are the next experiments that are going to be incorporated be? Will they be able to stick to such a protocol? - -""" - class Experiment: - def __init_(self): + def __init_(self, exp_name): + """ Anything that must be passed as a minimum for the experiment should be initialized here """ - # Should have overwriting property for different experiments + """ Dk if this overwrites the class variable or is worth doing + if we just assume they will overwrite """ + + self.exp_name= exp_name self.instruction_text = """\nWelcome to the {} experiment!\nStay still, focus on the centre of the screen, and try not to blink. \nThis block will run for %s seconds.\n Press spacebar to continue. \n""".format(exp_name) - + self.duration=120 + self.eeg:EEG=None + self.save_fn=None + self.n_trials=2010 + self.iti = 0.4 + self.soa = 0.3 + self.jitter = 0.2 def setup(self): + self.record_duration = np.float32(self.duration) + self.markernames = [1, 2] + # Setup Trial list -> Common in most (csv in Unicorn) self.parameter = np.random.binomial(1, 0.5, n_trials) self.trials = DataFrame(dict(parameter=parameter, timestamp=np.zeros(n_trials))) # Setup Graphics mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) - """ Does specific thing within Experiment Type to load stimulus data """ # Needs to be overwritten by specific experiment self.stim = self.load_stimulus() @@ -49,58 +53,45 @@ def setup(self): f"No path for a save file was passed to the experiment. Saving data to {save_fn}" ) - - - def present(self, duration=120, eeg: EEG=None, save_fn=None, n_trials=2010, exp_name=""): + def present(self): """ Do the present operation for a bunch of experiments """ - # Start EEG Stream, wait for signal to settle, and then pull timestamp for start point if eeg: - eeg.start(save_fn, duration=record_duration + 5) + eeg.start(self.save_fn, duration=self.record_duration + 5) start = time() # Iterate through the events - for ii, trial in trials.iterrows(): + for ii, trial in self.trials.iterrows(): # Intertrial interval - core.wait(iti + np.random.rand() * jitter) + core.wait(self.iti + np.random.rand() * self.jitter) # Some form of presenting the stimulus - sometimes order changed in lower files like ssvep - - # Push sample - if eeg: - timestamp = time() - if eeg.backend == "muselsl": - marker = [markernames[label]] - else: - marker = markernames[label] - eeg.push_sample(marker=marker, timestamp=timestamp) + self.present_stimulus(self.trials, ii, self.eeg, self.markernames) # Offset mywin.flip() - if len(event.getKeys()) > 0 or (time() - start) > record_duration: + if len(event.getKeys()) > 0 or (time() - start) > self.record_duration: break event.clearEvents() - # Close the EEG stream if eeg: eeg.stop() - def show_instructions(instruction_text): + def show_instructions(self): self.instruction_text = self.instruction_text % duration # graphics mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) - mywin.mouseVisible = False # Instructions - text = visual.TextStim(win=mywin, text=instruction_text, color=[-1, -1, -1]) + text = visual.TextStim(win=mywin, text=self.instruction_text, color=[-1, -1, -1]) text.draw() mywin.flip() event.waitKeys(keyList="space") diff --git a/eegnb/experiments/visual_p300/p300.py b/eegnb/experiments/visual_p300/p300.py index 7d6f5853..2d8e0c46 100644 --- a/eegnb/experiments/visual_p300/p300.py +++ b/eegnb/experiments/visual_p300/p300.py @@ -10,126 +10,39 @@ from eegnb import generate_save_fn from eegnb.stimuli import CAT_DOG -__title__ = "Visual P300" -instruction_text = """ - Welcome to the P300 experiment! - - Stay still, focus on the centre of the screen, and try not to blink. - - This block will run for %s seconds. - - Press spacebar to continue. - - """ - def load_stimulus(): - pass - -def present_stimulus(): - pass - - -if __name__ == "__main__": - - test = Experiment() - - - -def present(duration=120, eeg=None, save_fn=None): - n_trials = 2010 - iti = 0.4 - soa = 0.3 - jitter = 0.2 - record_duration = np.float32(duration) - markernames = [1, 2] - - # Setup trial list - image_type = np.random.binomial(1, 0.5, n_trials) - trials = DataFrame(dict(image_type=image_type, timestamp=np.zeros(n_trials))) - - def load_image(fn): - return visual.ImageStim(win=mywin, image=fn) - + load_image = lambda fn: visual.ImageStim(win=mywin, image=fn) # Setup graphics mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) - targets = list(map(load_image, glob(os.path.join(CAT_DOG, "target-*.jpg")))) nontargets = list(map(load_image, glob(os.path.join(CAT_DOG, "nontarget-*.jpg")))) - stim = [nontargets, targets] - - # Show instructions - show_instructions(duration=duration) - - # start the EEG stream, will delay 5 seconds to let signal settle - if eeg: - if save_fn is None: # If no save_fn passed, generate a new unnamed save file - save_fn = generate_save_fn(eeg.device_name, "visual_p300", "unnamed") - print( - f"No path for a save file was passed to the experiment. Saving data to {save_fn}" - ) - eeg.start(save_fn, duration=record_duration) - - # Iterate through the events - start = time() - for ii, trial in trials.iterrows(): - # Inter trial interval - core.wait(iti + np.random.rand() * jitter) - - # Select and display image - label = trials["image_type"].iloc[ii] - image = choice(targets if label == 1 else nontargets) - image.draw() - - # Push sample - if eeg: - timestamp = time() - if eeg.backend == "muselsl": - marker = [markernames[label]] - else: - marker = markernames[label] - eeg.push_sample(marker=marker, timestamp=timestamp) - - mywin.flip() + + return [nontargets, targets] - # offset - core.wait(soa) - mywin.flip() - if len(event.getKeys()) > 0 or (time() - start) > record_duration: - break +def present_stimulus(trials, ii, eeg, markernames): - event.clearEvents() + label = trials["image_type"].iloc[ii] + image = choice(targets if label == 1 else nontargets) + image.draw() - # Cleanup + # Push sample if eeg: - eeg.stop() - mywin.close() + timestamp = time() + if eeg.backend == "muselsl": + marker = [markernames[label]] + else: + marker = markernames[label] + eeg.push_sample(marker=marker, timestamp=timestamp) +if __name__ == "__main__": -def show_instructions(duration): - - instruction_text = """ - Welcome to the P300 experiment! - - Stay still, focus on the centre of the screen, and try not to blink. - - This block will run for %s seconds. - - Press spacebar to continue. + test = Experiment("Visual P300") + test.instruction_text = instruction_text + test.load_stimulus = load_stimulus + test.present_stimulus = present_stimulus + test.setup() + test.present() - """ - instruction_text = instruction_text % duration - - # graphics - mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) - - mywin.mouseVisible = False - # Instructions - text = visual.TextStim(win=mywin, text=instruction_text, color=[-1, -1, -1]) - text.draw() - mywin.flip() - event.waitKeys(keyList="space") - mywin.mouseVisible = True - mywin.close() diff --git a/eegnb/experiments/visual_ssvep/ssvep.py b/eegnb/experiments/visual_ssvep/ssvep.py index 8e6aa765..a3b480cc 100644 --- a/eegnb/experiments/visual_ssvep/ssvep.py +++ b/eegnb/experiments/visual_ssvep/ssvep.py @@ -8,24 +8,10 @@ from psychopy import visual, core, event from eegnb import generate_save_fn +from Experiment import Experiment -__title__ = "Visual SSVEP" - - -def present(duration=120, eeg=None, save_fn=None): - n_trials = 2010 - iti = 0.5 - soa = 3.0 - jitter = 0.2 - record_duration = np.float32(duration) - markernames = [1, 2] - - # Setup trial list - stim_freq = np.random.binomial(1, 0.5, n_trials) - trials = DataFrame(dict(stim_freq=stim_freq, timestamp=np.zeros(n_trials))) - - # Set up graphics - mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) +def load_stimulus(): + grating = visual.GratingStim(win=mywin, mask="circle", size=80, sf=0.2) grating_neg = visual.GratingStim( win=mywin, mask="circle", size=80, sf=0.2, phase=0.5 @@ -36,28 +22,7 @@ def present(duration=120, eeg=None, save_fn=None): # Generate the possible ssvep frequencies based on monitor refresh rate def get_possible_ssvep_freqs(frame_rate, stim_type="single"): - """Get possible SSVEP stimulation frequencies. - Utility function that returns the possible SSVEP stimulation - frequencies and on/off pattern based on screen refresh rate. - Args: - frame_rate (float): screen frame rate, in Hz - Keyword Args: - stim_type (str): type of stimulation - 'single'-> single graphic stimulus (the displayed object - appears and disappears in the background.) - 'reversal' -> pattern reversal stimulus (the displayed object - appears and is replaced by its opposite.) - Returns: - (dict): keys are stimulation frequencies (in Hz), and values are - lists of tuples, where each tuple is the number of (on, off) - periods of one stimulation cycle - For more info on stimulation patterns, see Section 2 of: - Danhua Zhu, Jordi Bieger, Gary Garcia Molina, and Ronald M. Aarts, - "A Survey of Stimulation Methods Used in SSVEP-Based BCIs," - Computational Intelligence and Neuroscience, vol. 2010, 12 pages, - 2010. - """ - + if stim_type == "single": max_period_nb = int(frame_rate / 6) periods = np.arange(max_period_nb) + 1 @@ -76,25 +41,7 @@ def get_possible_ssvep_freqs(frame_rate, stim_type="single"): return freqs def init_flicker_stim(frame_rate, cycle, soa): - """Initialize flickering stimulus. - Get parameters for a flickering stimulus, based on the screen refresh - rate and the desired stimulation cycle. - Args: - frame_rate (float): screen frame rate, in Hz - cycle (tuple or int): if tuple (on, off), represents the number of - 'on' periods and 'off' periods in one flickering cycle. This - supposes a "single graphic" stimulus, where the displayed object - appears and disappears in the background. - If int, represents the number of total periods in one cycle. - This supposes a "pattern reversal" stimulus, where the - displayed object appears and is replaced by its opposite. - soa (float): stimulus duration, in s - Returns: - (dict): dictionary with keys - 'cycle' -> tuple of (on, off) periods in a cycle - 'freq' -> stimulus frequency - 'n_cycles' -> number of cycles in one stimulus trial - """ + if isinstance(cycle, tuple): stim_freq = frame_rate / sum(cycle) n_cycles = int(soa * stim_freq) @@ -108,10 +55,6 @@ def init_flicker_stim(frame_rate, cycle, soa): # Set up stimuli frame_rate = np.round(mywin.getActualFrameRate()) # Frame rate, in Hz freqs = get_possible_ssvep_freqs(frame_rate, stim_type="reversal") - stim_patterns = [ - init_flicker_stim(frame_rate, 2, soa), - init_flicker_stim(frame_rate, 3, soa), - ] print( ( @@ -121,24 +64,35 @@ def init_flicker_stim(frame_rate, cycle, soa): ) ) - # Show the instructions screen - show_instructions(duration) + return [ + init_flicker_stim(frame_rate, 2, soa), + init_flicker_stim(frame_rate, 3, soa), + ] - # start the EEG stream, will delay 5 seconds to let signal settle - if eeg: - if save_fn is None: # If no save_fn passed, generate a new unnamed save file - save_fn = generate_save_fn(eeg.device_name, "visual_ssvep", "unnamed") - print( - f"No path for a save file was passed to the experiment. Saving data to {save_fn}" - ) - eeg.start(save_fn, duration=record_duration) +def present_stimulus(trials, ii, eeg, markernames): + pass + + +if __name__ == "__main__": + + test = Experiment("Visual SSVEP") + test.instruction_text = instruction_text + test.load_stimulus = load_stimulus + test.present_stimulus = present_stimulus + test.iti = 0.5 + test.soa = 3.0 + test.setup() + test.present() - # Iterate through trials - start = time() + +def present(duration=120, eeg=None, save_fn=None): + + for ii, trial in trials.iterrows(): # Intertrial interval core.wait(iti + np.random.rand() * jitter) + """ Unique """ # Select stimulus frequency ind = trials["stim_freq"].iloc[ii] @@ -162,6 +116,8 @@ def init_flicker_stim(frame_rate, cycle, soa): mywin.flip() grating_neg.setAutoDraw(False) + """ Unique ends """ + # offset mywin.flip() if len(event.getKeys()) > 0 or (time() - start) > record_duration: