-
Notifications
You must be signed in to change notification settings - Fork 138
Experiment Class Refactor (update to #183), converting specific experiments to subclasses #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
1634e81
d48093a
f05f1c4
38993e6
ba01229
19ffcdc
42d3f26
76c5036
dd958d2
f208e08
5db55f3
efb1928
251d04e
7436444
ee7cca9
d73abe0
ee5ab09
cb7895b
4815d28
3ec461a
b1000bd
d9f53b2
dc97fb5
ef3dba0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| """ | ||
| Initial run of the Experiment Class Refactor base class | ||
| Specific experiments are implemented as sub classes that overload a load_stimulus and present_stimulus method | ||
| Running each experiment: | ||
| obj = VisualP300({parametrs}) | ||
| obj.present() | ||
| """ | ||
|
|
||
| from abc import ABC, abstractmethod | ||
| from psychopy import prefs | ||
| #change the pref libraty to PTB and set the latency mode to high precision | ||
| prefs.hardware['audioLib'] = 'PTB' | ||
| prefs.hardware['audioLatencyMode'] = 3 | ||
|
|
||
| import os | ||
| from time import time | ||
| from glob import glob | ||
| from random import choice | ||
| from optparse import OptionParser | ||
| import random | ||
|
|
||
| import numpy as np | ||
| from pandas import DataFrame | ||
| from psychopy import visual, core, event | ||
|
|
||
| from eegnb import generate_save_fn | ||
| from eegnb.devices.eeg import EEG | ||
|
|
||
| class Experiment: | ||
|
||
|
|
||
| def __init__(self, exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter): | ||
| """ Anything that must be passed as a minimum for the experiment should be initialized here """ | ||
|
|
||
| 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(self.exp_name) | ||
| self.duration = duration | ||
| self.eeg = eeg | ||
| self.save_fn = save_fn | ||
| self.n_trials = n_trials | ||
| self.iti = iti | ||
| self.soa = soa | ||
| self.jitter = jitter | ||
|
|
||
| @abstractmethod | ||
| def load_stimulus(self): | ||
| """ Needs to be overwritten by specific experiment """ | ||
| raise NotImplementedError | ||
|
|
||
| @abstractmethod | ||
| def present_stimulus(self): | ||
| raise NotImplementedError | ||
|
|
||
| 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, self.n_trials) | ||
| self.trials = DataFrame(dict(parameter=self.parameter, timestamp=np.zeros(self.n_trials))) | ||
|
|
||
| # Setup Graphics | ||
| self.mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) | ||
|
|
||
| # Needs to be overwritten by specific experiment | ||
| self.stim = self.load_stimulus() | ||
|
|
||
| # Show Instruction Screen | ||
| self.show_instructions() | ||
|
|
||
| if self.eeg: | ||
| if save_fn is None: # If no save_fn passed, generate a new unnamed save file | ||
| random_id = random.randint(1000,10000) | ||
| self.save_fn = generate_save_fn(self.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 {self.save_fn}" | ||
| ) | ||
|
|
||
| def present(self): | ||
|
||
| """ Do the present operation for a bunch of experiments """ | ||
|
|
||
| self.setup() | ||
|
||
|
|
||
| # Start EEG Stream, wait for signal to settle, and then pull timestamp for start point | ||
| if self.eeg: | ||
| self.eeg.start(self.save_fn, duration=self.record_duration + 5) | ||
|
|
||
| start = time() | ||
|
|
||
| # Iterate through the events | ||
| for ii, trial in self.trials.iterrows(): | ||
|
|
||
| # Intertrial interval | ||
| core.wait(self.iti + np.random.rand() * self.jitter) | ||
|
|
||
| # Some form of presenting the stimulus - sometimes order changed in lower files like ssvep | ||
| self.present_stimulus(ii) | ||
|
|
||
| # Offset | ||
| core.wait(self.soa) | ||
| self.mywin.flip() | ||
| if len(event.getKeys()) > 0 or (time() - start) > self.record_duration: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also as discussed, this part (of the original code) has always looked a bit ugly to me. The alternative is to pre-calculate all of the itis, soas, jitters in advance, and then it will be known before the loop starts what the duration will be, and so the decision of whether or it is the duration param or the completion of the trial set will be clear and not determined on the fly at the end. However, I suggest we have a stab at this small adjustment at a later point, it isn't the focus or priority of the current PR. |
||
| break | ||
| event.clearEvents() | ||
|
|
||
| # Close the EEG stream | ||
| if self.eeg: | ||
| self.eeg.stop() | ||
|
|
||
| # Close the window | ||
| self.mywin.close() | ||
|
|
||
|
|
||
| def show_instructions(self): | ||
|
|
||
| self.instruction_text = self.instruction_text % self.duration | ||
|
|
||
| # graphics | ||
| #mywin = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) | ||
| self.mywin.mouseVisible = False | ||
|
||
|
|
||
| # Instructions | ||
| text = visual.TextStim(win=self.mywin, text=self.instruction_text, color=[-1, -1, -1]) | ||
| text.draw() | ||
| self.mywin.flip() | ||
| event.waitKeys(keyList="space") | ||
|
|
||
| self.mywin.mouseVisible = True | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these safe to set globally? @JohnGriffiths
Feels like we set them like this in more places, in which case they would conflict.
Edit: Indeed, but looks like they all set the same values? Should prob be consolidated. All hits: https://github.com/NeuroTechX/eeg-notebooks/search?q=prefs.hardware