From 6579d6ccc01810acfdd7c634c005007380eed3fa Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Thu, 2 Jun 2022 14:59:18 -0400 Subject: [PATCH 01/32] example test commit (#182) * example test commit * example edit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c35cafef..b91fd161 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ h5py>=3.1.0 pytest-shutil pyo>=1.0.3; platform_system == "Linux" -# This might try to build from source on linux (since there are no wheels for Linux on PyPI) +# This might try to build from source on linux (since there are no wheels for Linux on PyPI) . # You can pass `--find-links=https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04/` your `pip install` to use the prebuilt wheels at the link. wxPython>=4.0 ; platform_system == "Linux" click From ae4a5dbfb9239ee4ccebd59a2ab7a193a39f4d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 10 Jun 2022 10:47:49 +0200 Subject: [PATCH 02/32] ci: run test workflow on develop branch --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 185caf2d..53f057d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Test on: push: - branches: [ master ] + branches: [ master, develop ] pull_request: - branches: [ master ] + branches: [ master, develop ] jobs: test: From 5c83bbecdc904f3f9a51747e0e31c421e310150d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 5 May 2022 17:02:38 +0200 Subject: [PATCH 03/32] ci: add develop branch to job triggers --- .github/workflows/docs.yml | 8 ++++---- .github/workflows/test.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 48eaf939..f1f23d9d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,9 +2,9 @@ name: Docs on: push: - branches: [ master, 'dev/*' ] + branches: [ master, develop, 'dev/*' ] pull_request: - branches: [ master ] + branches: [ master, develop ] jobs: build: @@ -29,8 +29,8 @@ jobs: cd doc && make html - name: Deploy Docs uses: peaceiris/actions-gh-pages@v3 - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' # TODO: Deploy seperate develop-version of docs? with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: doc/_build/html - + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53f057d9..25c9d146 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: python -m pip install --upgrade pip wheel pip install . - name: Run eegnb install test - shell: bash + shell: bash pip install -U psychtoolbox # JG_ADD run: | if [ "$RUNNER_OS" == "Linux" ]; then From 32515fa5e1949b581eb3193f5535c77d1c05edea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 16 Jun 2022 16:04:31 +0200 Subject: [PATCH 04/32] ci: fix syntax issue in workflow --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 25c9d146..ccec3e05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,13 +60,13 @@ jobs: run: | python -m pip install --upgrade pip wheel pip install . + pip install -U psychtoolbox # JG_ADD + # EB: needed? - name: Run eegnb install test shell: bash - pip install -U psychtoolbox # JG_ADD run: | if [ "$RUNNER_OS" == "Linux" ]; then Xvfb :0 -screen 0 1024x768x24 -ac +extension GLX +render -noreset &> xvfb.log & - / export DISPLAY=:0 fi eegnb --help From 75e42ef09181a976eac4531a75e9282590a6345e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 16 Jun 2022 16:24:10 +0200 Subject: [PATCH 05/32] fix: fixed import (brainflow updated API) --- eegnb/devices/eeg.py | 57 +++++++++++++++++++++--------------------- eegnb/devices/utils.py | 14 +++++------ 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/eegnb/devices/eeg.py b/eegnb/devices/eeg.py index 6b22a377..13fdb83a 100644 --- a/eegnb/devices/eeg.py +++ b/eegnb/devices/eeg.py @@ -14,11 +14,17 @@ import numpy as np import pandas as pd -from brainflow import BoardShim, BoardIds, BrainFlowInputParams +from brainflow.board_shim import BoardShim, BoardIds, BrainFlowInputParams from muselsl import stream, list_muses, record, constants as mlsl_cnsts from pylsl import StreamInfo, StreamOutlet, StreamInlet, resolve_byprop -from eegnb.devices.utils import get_openbci_usb, create_stim_array,SAMPLE_FREQS,EEG_INDICES,EEG_CHANNELS +from eegnb.devices.utils import ( + get_openbci_usb, + create_stim_array, + SAMPLE_FREQS, + EEG_INDICES, + EEG_CHANNELS, +) logger = logging.getLogger(__name__) @@ -39,18 +45,19 @@ "notion2", "freeeeg32", "crown", - "museS_bfn", # bfn = brainflow with native bluetooth; - "museS_bfb", # bfb = brainflow with BLED dongle bluetooth + "museS_bfn", # bfn = brainflow with native bluetooth; + "museS_bfb", # bfb = brainflow with BLED dongle bluetooth "muse2_bfn", "muse2_bfb", "muse2016_bfn", - "muse2016_bfb" + "muse2016_bfb", ] class EEG: device_name: str stream_started: bool = False + def __init__( self, device=None, @@ -85,8 +92,8 @@ def initialize_backend(self): self.timestamp_channel = BoardShim.get_timestamp_channel(self.brainflow_id) elif self.backend == "muselsl": self._init_muselsl() - self._muse_get_recent() # run this at initialization to get some - # stream metadata into the eeg class + self._muse_get_recent() # run this at initialization to get some + # stream metadata into the eeg class def _get_backend(self, device_name): if device_name in brainflow_devices: @@ -126,7 +133,7 @@ def _start_muse(self, duration): print("will save to file: %s" % self.save_fn) self.recording = Process(target=record, args=(duration, self.save_fn)) self.recording.start() - + time.sleep(5) self.stream_started = True self.push_sample([99], timestamp=time.time()) @@ -137,7 +144,7 @@ def _stop_muse(self): def _muse_push_sample(self, marker, timestamp): self.muse_StreamOutlet.push_sample(marker, timestamp) - def _muse_get_recent(self, n_samples: int=256, restart_inlet: bool=False): + def _muse_get_recent(self, n_samples: int = 256, restart_inlet: bool = False): if self._muse_recent_inlet and not restart_inlet: inlet = self._muse_recent_inlet else: @@ -157,9 +164,8 @@ def _muse_get_recent(self, n_samples: int=256, restart_inlet: bool=False): self.info = info self.n_chans = n_chans - timeout = (n_samples/sfreq)+0.5 - samples, timestamps = inlet.pull_chunk(timeout=timeout, - max_samples=n_samples) + timeout = (n_samples / sfreq) + 0.5 + samples, timestamps = inlet.pull_chunk(timeout=timeout, max_samples=n_samples) samples = np.array(samples) timestamps = np.array(timestamps) @@ -260,13 +266,13 @@ def _init_brainflow(self): elif self.device_name == "muse2_bfn": self.brainflow_id = BoardIds.MUSE_2_BOARD.value - + elif self.device_name == "muse2_bfb": self.brainflow_id = BoardIds.MUSE_2_BLED_BOARD.value elif self.device_name == "muse2016_bfn": self.brainflow_id = BoardIds.MUSE_2016_BOARD.value - + elif self.device_name == "muse2016_bfb": self.brainflow_id = BoardIds.MUSE_2016_BLED_BOARD.value @@ -291,11 +297,13 @@ def _start_brainflow(self): # only start stream if non exists if not self.stream_started: self.board.start_stream() - + self.stream_started = True # wait for signal to settle - if (self.device_name.find("cyton") != -1) or (self.device_name.find("ganglion") != -1): + if (self.device_name.find("cyton") != -1) or ( + self.device_name.find("ganglion") != -1 + ): # wait longer for openbci cyton / ganglion sleep(10) else: @@ -314,21 +322,19 @@ def _stop_brainflow(self): # Create a column for the stimuli to append to the EEG data stim_array = create_stim_array(timestamps, self.markers) - timestamps = timestamps[ ..., None ] - + timestamps = timestamps[..., None] + # Add an additional dimension so that shapes match total_data = np.append(timestamps, eeg_data, 1) # Append the stim array to data. - total_data = np.append(total_data, stim_array, 1) - + total_data = np.append(total_data, stim_array, 1) + # Subtract five seconds of settling time from beginning total_data = total_data[5 * self.sfreq :] data_df = pd.DataFrame(total_data, columns=["timestamps"] + ch_names + ["stim"]) data_df.to_csv(self.save_fn, index=False) - - def _brainflow_extract(self, data): """ Formats the data returned from brainflow to get @@ -357,14 +363,12 @@ def _brainflow_extract(self, data): eeg_data = data[:, BoardShim.get_eeg_channels(self.brainflow_id)] timestamps = data[:, BoardShim.get_timestamp_channel(self.brainflow_id)] - return ch_names,eeg_data,timestamps - + return ch_names, eeg_data, timestamps def _brainflow_push_sample(self, marker): last_timestamp = self.board.get_current_board_data(1)[self.timestamp_channel][0] self.markers.append([marker, last_timestamp]) - def _brainflow_get_recent(self, n_samples=256): # initialize brainflow if not set @@ -405,7 +409,6 @@ def start(self, fn, duration=None): elif self.backend == "muselsl": self._start_muse(duration) - def push_sample(self, marker, timestamp): """ Universal method for pushing a marker and its timestamp to store alongside the EEG data. @@ -445,6 +448,4 @@ def get_recent(self, n_samples: int = 256): sorted_cols = sorted(df.columns) df = df[sorted_cols] - return df - diff --git a/eegnb/devices/utils.py b/eegnb/devices/utils.py index 2c085340..8e01ad9a 100644 --- a/eegnb/devices/utils.py +++ b/eegnb/devices/utils.py @@ -3,14 +3,14 @@ import platform import serial -from brainflow import BoardShim, BoardIds +from brainflow.board_shim import BoardShim, BoardIds # Default channel names for the various EEG devices. EEG_CHANNELS = { - "muse2016": ['TP9', 'AF7', 'AF8', 'TP10', 'Right AUX'], - "muse2": ['TP9', 'AF7', 'AF8', 'TP10', 'Right AUX'], - "museS": ['TP9', 'AF7', 'AF8', 'TP10', 'Right AUX'], + "muse2016": ["TP9", "AF7", "AF8", "TP10", "Right AUX"], + "muse2": ["TP9", "AF7", "AF8", "TP10", "Right AUX"], + "museS": ["TP9", "AF7", "AF8", "TP10", "Right AUX"], "muse2016_bfn": BoardShim.get_eeg_names(BoardIds.MUSE_2016_BOARD.value), "muse2016_bfb": BoardShim.get_eeg_names(BoardIds.MUSE_2016_BLED_BOARD.value), "muse2_bfn": BoardShim.get_eeg_names(BoardIds.MUSE_2_BOARD.value), @@ -26,7 +26,7 @@ "notion1": BoardShim.get_eeg_names(BoardIds.NOTION_1_BOARD.value), "notion2": BoardShim.get_eeg_names(BoardIds.NOTION_2_BOARD.value), "crown": BoardShim.get_eeg_names(BoardIds.CROWN_BOARD.value), - "freeeeg32": [f'eeg_{i}' for i in range(0,32)], + "freeeeg32": [f"eeg_{i}" for i in range(0, 32)], } BRAINFLOW_CHANNELS = { @@ -62,9 +62,9 @@ "muse2016": 256, "muse2": 256, "museS": 256, - "muse2016_bfn": BoardShim.get_sampling_rate(BoardIds.MUSE_2016_BOARD.value), + "muse2016_bfn": BoardShim.get_sampling_rate(BoardIds.MUSE_2016_BOARD.value), "muse2016_bfb": BoardShim.get_sampling_rate(BoardIds.MUSE_2016_BLED_BOARD.value), - "muse2_bfn": BoardShim.get_sampling_rate(BoardIds.MUSE_2_BOARD.value), + "muse2_bfn": BoardShim.get_sampling_rate(BoardIds.MUSE_2_BOARD.value), "muse2_bfb": BoardShim.get_sampling_rate(BoardIds.MUSE_2_BLED_BOARD.value), "museS_bfn": BoardShim.get_sampling_rate(BoardIds.MUSE_S_BOARD.value), "museS_bfb": BoardShim.get_sampling_rate(BoardIds.MUSE_S_BLED_BOARD.value), From f139ab5ff5f0bb96f47698d475a1a5dcb047c5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 16 Jun 2022 16:45:28 +0200 Subject: [PATCH 06/32] build(deps): locked pylsl==1.10.5 (#187) --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b91fd161..2ad031b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ seaborn==0.9.0 pyriemann>=0.2.7 jupyter muselsl>=2.0.2 +pylsl==1.10.5 # due to https://github.com/NeuroTechX/eeg-notebooks/issues/187 brainflow>=4.8.2 gdown matplotlib>=3.3.3 @@ -24,7 +25,7 @@ wxPython>=4.0 ; platform_system == "Linux" click # can be removed once minimum version is Python 3.7 -dataclasses; python_version == '3.6' +dataclasses; python_version == '3.6' # pywinhook needs some special treatment since there are only wheels on PyPI for Python 3.7-3.8, and building requires special tools (swig, VS C++ tools) # See issue: https://github.com/NeuroTechX/eeg-notebooks/issues/29 From e4e7d8a21750d7270523e09e7b76ad1a5bbb7d31 Mon Sep 17 00:00:00 2001 From: Parv Agarwal <65726543+Parvfect@users.noreply.github.com> Date: Wed, 10 Aug 2022 17:35:41 +0100 Subject: [PATCH 07/32] Experiment Class Refactor (update to #183), converting specific experiments to subclasses (#184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First commit * Second commit * Modifications * Lol * Lol * Incorporated N170 and p300, looking good for a PR * ssvep update * Implementing subclasses instead of loose functions * fix: fixed import (brainflow updated API) * Playing around still * Fixing import errors * Adding abstractmethod decorators * Still working on the import error * Guess what's finally working * Comments and naming ticks * More comments * Live coding demonstration * ssvep adapted * Adapting Auditory Oddball * changing save_fn to self.save_fun * This maybe the last big change * utils file changed, changes work through cli Co-authored-by: Erik Bjäreholt --- eegnb/cli/utils.py | 28 ++- eegnb/experiments/Experiment.py | 168 +++++++++++++ eegnb/experiments/Experiment_readme.txt | 13 + eegnb/experiments/__init__.py | 4 + eegnb/experiments/auditory_oddball/aob.py | 185 +++++---------- eegnb/experiments/visual_n170/n170.py | 133 +++-------- eegnb/experiments/visual_p300/p300.py | 129 +++------- eegnb/experiments/visual_ssvep/ssvep.py | 274 +++++++--------------- eegnb/experiments/visual_vep/vep.py | 20 +- 9 files changed, 438 insertions(+), 516 deletions(-) create mode 100644 eegnb/experiments/Experiment.py create mode 100644 eegnb/experiments/Experiment_readme.txt diff --git a/eegnb/cli/utils.py b/eegnb/cli/utils.py index 00a769c5..322a16dd 100644 --- a/eegnb/cli/utils.py +++ b/eegnb/cli/utils.py @@ -7,24 +7,26 @@ from eegnb.devices.eeg import EEG -from eegnb.experiments.visual_n170 import n170 -from eegnb.experiments.visual_p300 import p300 -from eegnb.experiments.visual_ssvep import ssvep +from eegnb.experiments import VisualN170 +from eegnb.experiments import VisualP300 +from eegnb.experiments import VisualSSVEP +from eegnb.experiments import AuditoryOddball from eegnb.experiments.visual_cueing import cueing from eegnb.experiments.visual_codeprose import codeprose -from eegnb.experiments.auditory_oddball import aob, diaconescu +from eegnb.experiments.auditory_oddball import diaconescu from eegnb.experiments.auditory_ssaep import ssaep, ssaep_onefreq +# New Experiment Class structure has a different initilization, to be noted experiments = { - "visual-N170": n170, - "visual-P300": p300, - "visual-SSVEP": ssvep, + "visual-N170": VisualN170(), + "visual-P300": VisualP300(), + "visual-SSVEP": VisualSSVEP(), "visual-cue": cueing, "visual-codeprose": codeprose, "auditory-SSAEP orig": ssaep, "auditory-SSAEP onefreq": ssaep_onefreq, - "auditory-oddball orig": aob, + "auditory-oddball orig": AuditoryOddball(), "auditory-oddball diaconescu": diaconescu, } @@ -42,7 +44,15 @@ def run_experiment( ): if experiment in experiments: module = experiments[experiment] - module.present(duration=record_duration, eeg=eeg_device, save_fn=save_fn) # type: ignore + + # Condition added for different run types of old and new experiment class structure + if experiment == "visual-N170" or experiment == "visual-P300" or experiment == "visual-SSVEP" or experiment == "auditory-oddball orig": + module.duration = record_duration + module.eeg = eeg_device + module.save_fn = save_fn + module.run() + else: + module.present(duration=record_duration, eeg=eeg_device, save_fn=save_fn) # type: ignore else: print("\nError: Unknown experiment '{}'".format(experiment)) print("\nExperiment can be one of:") diff --git a/eegnb/experiments/Experiment.py b/eegnb/experiments/Experiment.py new file mode 100644 index 00000000..35da046a --- /dev/null +++ b/eegnb/experiments/Experiment.py @@ -0,0 +1,168 @@ +""" +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({parameters}) +obj.run() +""" + +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 BaseExperiment: + + def __init__(self, exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter): + """ Initializer for the Base Experiment Class """ + + 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): + """ + Method that loads the stimulus for the specific experiment, overwritten by the specific experiment + Returns the stimulus object in the form of [{stim1},{stim2},...] + Throws error if not overwritten in the specific experiment + """ + raise NotImplementedError + + @abstractmethod + def present_stimulus(self, idx : int): + """ + Method that presents the stimulus for the specific experiment, overwritten by the specific experiment + Displays the stimulus on the screen + Pushes EEG Sample if EEG is enabled + Throws error if not overwritten in the specific experiment + + idx : Trial index for the current trial + """ + raise NotImplementedError + + def setup(self, instructions=True): + + # Initializing the record duration and the marker names + self.record_duration = np.float32(self.duration) + self.markernames = [1, 2] + + # Setting up the trial and parameter list + self.parameter = np.random.binomial(1, 0.5, self.n_trials) + self.trials = DataFrame(dict(parameter=self.parameter, timestamp=np.zeros(self.n_trials))) + + # Setting up Graphics + self.window = visual.Window([1600, 900], monitor="testMonitor", units="deg", fullscr=True) + + # Loading the stimulus from the specific experiment, throws an error if not overwritten in the specific experiment + self.stim = self.load_stimulus() + + # Show Instruction Screen if not skipped by the user + if instructions: + self.show_instructions() + + # Checking for EEG to setup the EEG stream + if self.eeg: + # If no save_fn passed, generate a new unnamed save file + if self.save_fn is None: + # Generating a random int for the filename + random_id = random.randint(1000,10000) + # Generating save function + 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 show_instructions(self): + """ + Method that shows the instructions for the specific Experiment + In the usual case it is not overwritten, the instruction text can be overwritten by the specific experiment + No parameters accepted, can be skipped through passing a False while running the Experiment + """ + + # Splitting instruction text into lines + self.instruction_text = self.instruction_text % self.duration + + # Disabling the cursor during display of instructions + self.window.mouseVisible = False + + # Displaying the instructions on the screen + text = visual.TextStim(win=self.window, text=self.instruction_text, color=[-1, -1, -1]) + text.draw() + self.window.flip() + + # Waiting for the user to press the spacebar to start the experiment + event.waitKeys(keyList="space") + + # Enabling the cursor again + self.window.mouseVisible = True + + def run(self, instructions=True): + """ Do the present operation for a bunch of experiments """ + + # Setup the experiment, alternatively could get rid of this line, something to think about + self.setup(instructions) + + print("Wait for the EEG-stream to start...") + + # 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) + + print("EEG Stream started") + + start = time() + + # Iterate through the events + for ii, trial in self.trials.iterrows(): + + # Intertrial interval + core.wait(self.iti + np.random.rand() * self.jitter) + + # Stimulus presentation overwritten by specific experiment + self.present_stimulus(ii, trial) + + # Offset + core.wait(self.soa) + self.window.flip() + + # Exiting the loop condition, looks ugly and needs to be fixed + if len(event.getKeys()) > 0 or (time() - start) > self.record_duration: + break + + # Clearing the screen for the next trial + event.clearEvents() + + # Closing the EEG stream + if self.eeg: + self.eeg.stop() + + # Closing the window + self.window.close() + + + \ No newline at end of file diff --git a/eegnb/experiments/Experiment_readme.txt b/eegnb/experiments/Experiment_readme.txt new file mode 100644 index 00000000..acaa44b0 --- /dev/null +++ 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 diff --git a/eegnb/experiments/__init__.py b/eegnb/experiments/__init__.py index e69de29b..e17326ff 100644 --- a/eegnb/experiments/__init__.py +++ b/eegnb/experiments/__init__.py @@ -0,0 +1,4 @@ +from .visual_n170.n170 import VisualN170 +from .visual_p300.p300 import VisualP300 +from .visual_ssvep.ssvep import VisualSSVEP +from .auditory_oddball.aob import AuditoryOddball \ No newline at end of file diff --git a/eegnb/experiments/auditory_oddball/aob.py b/eegnb/experiments/auditory_oddball/aob.py index d9d28896..32ccd1b2 100644 --- a/eegnb/experiments/auditory_oddball/aob.py +++ b/eegnb/experiments/auditory_oddball/aob.py @@ -3,156 +3,83 @@ from psychopy import visual, core, event, sound from time import time +from eegnb.devices.eeg import EEG +from eegnb.experiments import Experiment -__title__ = "Auditory oddball (orig)" +class AuditoryOddball(Experiment.BaseExperiment): + + def __init__(self, duration=120, eeg: EEG=None, save_fn=None, n_trials = 2010, iti = 0.3, soa = 0.2, jitter = 0.2, secs=0.2, volume=0.8, random_state=42, s1_freq="C", s2_freq="D", s1_octave=5, s2_octave=6): + """ -def present( - save_fn = None, - eeg=None, - duration=120, - n_trials=2010, - iti=0.3, - soa=0.2, - jitter=0.2, - secs=0.2, - volume=0.8, - random_state=42, - s1_freq="C", - s2_freq="D", - s1_octave=5, - s2_octave=6, -): + Auditory Oddball Experiment + =========================== - """ + Unique Parameters: + ----------- - Auditory Oddball Experiment - =========================== + secs - duration of the sound in seconds (default 0.2) + volume - volume of the sounds in [0,1] (default 0.8) - Parameters: - ----------- + random_state - random seed (default 42) - duration - duration of the recording in seconds (default 10) + s1_freq - frequency of first tone + s2_freq - frequency of second tone - n_trials - number of trials (default 10) + s1_octave - octave of first tone + s2_octave - octave of second tone - iti - intertrial interval (default 0.3) + """ - soa - stimulus onset asynchrony, = interval between end of stimulus - and next trial (default 0.2) + exp_name = "Auditory Oddball" + super().__init__(exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter) + self.secs = secs + self.volume = volume + self.random_state = random_state + self.s1_freq = s1_freq + self.s2_freq = s2_freq + self.s1_octave = s1_octave + self.s2_octave = s2_octave - jitter - jitter in the intertrial intervals (default 0.2) + def load_stimulus(self): + """ Loads the Stimulus """ + + # Set up trial parameters + np.random.seed(self.random_state) + + # Initialize stimuli + aud1, aud2 = sound.Sound(self.s1_freq, octave=self.s1_octave, secs=self.secs), sound.Sound(self.s2_freq, octave=self.s2_octave, secs=self.secs) + aud1.setVolume(self.volume) + aud2.setVolume(self.volume) + self.auds = [aud1, aud2] - secs - duration of the sound in seconds (default 0.2) + # Setup trial list + sound_ind = np.random.binomial(1, 0.25, self.n_trials) + itis = self.iti + np.random.rand(self.n_trials) * self.jitter + self.trials = DataFrame(dict(sound_ind=sound_ind, iti=itis)) + self.trials["soa"] = self.soa + self.trials["secs"] = self.secs - volume - volume of the sounds in [0,1] (default 0.8) + self.fixation = visual.GratingStim(win=self.window, size=0.2, pos=[0, 0], sf=0, rgb=[1, 0, 0]) + self.fixation.setAutoDraw(True) + self.window.flip() - random_state - random seed (default 42) - - s1_freq - frequency of first tone - s2_freq - frequency of second tone - - s1_octave - octave of first tone - s2_octave - octave of second tone - - """ - - # Set up trial parameters - np.random.seed(random_state) - markernames = [1, 2] - record_duration = np.float32(duration) - - # Initialize stimuli - aud1 = sound.Sound(s1_freq, octave=s1_octave, secs=secs) - aud1.setVolume(volume) - aud2 = sound.Sound(s2_freq, octave=s2_octave, secs=secs) - aud2.setVolume(volume) - auds = [aud1, aud2] - - # Setup trial list - sound_ind = np.random.binomial(1, 0.25, n_trials) - itis = iti + np.random.rand(n_trials) * jitter - trials = DataFrame(dict(sound_ind=sound_ind, iti=itis)) - trials["soa"] = soa - trials["secs"] = secs - - # Setup graphics - mywin = visual.Window( - [1920, 1080], monitor="testMonitor", units="deg", fullscr=True - ) - fixation = visual.GratingStim(win=mywin, size=0.2, pos=[0, 0], sf=0, rgb=[1, 0, 0]) - fixation.setAutoDraw(True) - mywin.flip() - - # Show the instructions screen - show_instructions(10) - - # Start EEG Stream, wait for signal to settle, and then pull timestamp for start point - if eeg: - eeg.start(save_fn, duration=record_duration) - start = time() - - # Iterate through the events - for ii, trial in trials.iterrows(): - - # Intertrial interval - core.wait(trial["iti"]) + return + + def present_stimulus(self, idx : int, trial): + """ Presents the Stimulus """ # Select and play sound ind = int(trial["sound_ind"]) - auds[ind].stop() - auds[ind].play() + self.auds[ind].stop() + self.auds[ind].play() # Push sample - if eeg: + if self.eeg: timestamp = time() - marker = [markernames[ind]] + marker = [self.markernames[ind]] marker = list(map(int, marker)) + self.eeg.push_sample(marker=marker, timestamp=timestamp) - eeg.push_sample(marker=marker, timestamp=timestamp) - - mywin.flip() - - # Offset - core.wait(soa) - if len(event.getKeys()) > 0: - break - if (time() - start) > record_duration: - break - event.clearEvents() - - # Cleanup - if eeg: - eeg.stop() - - mywin.close() - - -def show_instructions(duration): - - instruction_text = """ - Welcome to the aMMN 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. - - """ - 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_n170/n170.py b/eegnb/experiments/visual_n170/n170.py index 3f9822bd..0ff0dbed 100644 --- a/eegnb/experiments/visual_n170/n170.py +++ b/eegnb/experiments/visual_n170/n170.py @@ -1,3 +1,5 @@ +""" eeg-notebooks/eegnb/experiments/visual_n170/n170.py """ + from psychopy import prefs #change the pref libraty to PTB and set the latency mode to high precision prefs.hardware['audioLib'] = 'PTB' @@ -14,110 +16,49 @@ from pandas import DataFrame from psychopy import visual, core, event -from eegnb import generate_save_fn from eegnb.devices.eeg import EEG from eegnb.stimuli import FACE_HOUSE +from eegnb.experiments import Experiment -__title__ = "Visual N170" +class VisualN170(Experiment.BaseExperiment): -def present(duration=120, eeg: EEG=None, save_fn=None, + def __init__(self, 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) - - 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) - - 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) + # Set experiment name + exp_name = "Visual N170" + # Calling the super class constructor to initialize the experiment variables + super(VisualN170, self).__init__(exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter) + + def load_stimulus(self): + + # Loading Images from the folder + load_image = lambda fn: visual.ImageStim(win=self.window, image=fn) + + # Setting up images for the stimulus + self.faces = list(map(load_image, glob(os.path.join(FACE_HOUSE, "faces", "*_3.jpg")))) + self.houses = list(map(load_image, glob(os.path.join(FACE_HOUSE, "houses", "*.3.jpg")))) + + # Return the list of images as a stimulus object + return [self.houses, self.faces] + + def present_stimulus(self, idx : int, trial): + + # Get the label of the trial + label = self.trials["parameter"].iloc[idx] + # Get the image to be presented + image = choice(self.faces if label == 1 else self.houses) + # Draw the image image.draw() - # Push sample - if eeg: + # Pushing the sample to the EEG + if self.eeg: timestamp = time() - if eeg.backend == "muselsl": - marker = [markernames[label]] + if self.eeg.backend == "muselsl": + marker = [self.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 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. - - """ - 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() + marker = self.markernames[label] + self.eeg.push_sample(marker=marker, timestamp=timestamp) + + self.window.flip() \ No newline at end of file diff --git a/eegnb/experiments/visual_p300/p300.py b/eegnb/experiments/visual_p300/p300.py index b8176205..33f6f3e2 100644 --- a/eegnb/experiments/visual_p300/p300.py +++ b/eegnb/experiments/visual_p300/p300.py @@ -1,112 +1,51 @@ + +""" eeg-notebooks/eegnb/experiments/visual_p300/p300.py """ + 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.stimuli import CAT_DOG +from eegnb.experiments import Experiment +from eegnb.devices.eeg import EEG -__title__ = "Visual P300" - - -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) - - # 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) +class VisualP300(Experiment.BaseExperiment): + + def __init__(self, duration=120, eeg: EEG=None, save_fn=None, + n_trials = 2010, iti = 0.4, soa = 0.3, jitter = 0.2): + + exp_name = "Visual P300" + super().__init__(exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter) + + def load_stimulus(self): + + load_image = lambda fn: visual.ImageStim(win=self.window, image=fn) + + self.targets = list(map(load_image, glob(os.path.join(CAT_DOG, "target-*.jpg")))) + self.nontargets = list(map(load_image, glob(os.path.join(CAT_DOG, "nontarget-*.jpg")))) + + return [self.nontargets, self.targets] + + def present_stimulus(self, idx:int, trial): + + label = self.trials["parameter"].iloc[idx] + image = choice(self.targets if label == 1 else self.nontargets) image.draw() # Push sample - if eeg: + if self.eeg: timestamp = time() - if eeg.backend == "muselsl": - marker = [markernames[label]] + if self.eeg.backend == "muselsl": + marker = [self.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 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. - - """ - 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") + marker = self.markernames[label] + self.eeg.push_sample(marker=marker, timestamp=timestamp) - mywin.mouseVisible = True - mywin.close() + self.window.flip() \ No newline at end of file diff --git a/eegnb/experiments/visual_ssvep/ssvep.py b/eegnb/experiments/visual_ssvep/ssvep.py index 8e6aa765..b30baffd 100644 --- a/eegnb/experiments/visual_ssvep/ssvep.py +++ b/eegnb/experiments/visual_ssvep/ssvep.py @@ -1,3 +1,5 @@ + +from eegnb.experiments import Experiment import os from time import time from glob import glob @@ -7,199 +9,105 @@ from pandas import DataFrame from psychopy import visual, core, event -from eegnb import generate_save_fn -__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) - 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 - ) - fixation = visual.GratingStim( - win=mywin, size=0.2, pos=[0, 0], sf=0.2, color=[1, 0, 0], autoDraw=True - ) - - # 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. - """ - - max_period_nb = int(frame_rate / 6) - periods = np.arange(max_period_nb) + 1 - - if stim_type == "single": - freqs = dict() - for p1 in periods: - for p2 in periods: - f = frame_rate / (p1 + p2) - try: - freqs[f].append((p1, p2)) - except: - freqs[f] = [(p1, p2)] - elif stim_type == "reversal": - freqs = {frame_rate / p: [(p, p)] for p in periods[::-1]} - - 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) - else: - stim_freq = frame_rate / cycle - cycle = (cycle, cycle) - n_cycles = int(soa * stim_freq) / 2 - - return {"cycle": cycle, "freq": stim_freq, "n_cycles": n_cycles} - - # 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( - ( - "Flickering frequencies (Hz): {}\n".format( - [stim_patterns[0]["freq"], stim_patterns[1]["freq"]] - ) - ) - ) +from eegnb.devices.eeg import EEG +from eegnb import generate_save_fn - # Show the instructions screen - show_instructions(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_ssvep", "unnamed") - print( - f"No path for a save file was passed to the experiment. Saving data to {save_fn}" +class VisualSSVEP(Experiment.BaseExperiment): + + def __init__(self, duration=120, eeg: EEG=None, save_fn=None, n_trials = 2010, iti = 0.5, soa = 3.0, jitter = 0.2): + + exp_name = "Visual SSVEP" + super().__init__(exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter) + + def load_stimulus(self): + + self.grating = visual.GratingStim(win=self.window, mask="circle", size=80, sf=0.2) + + self.grating_neg = visual.GratingStim(win=self.window, mask="circle", size=80, sf=0.2, phase=0.5) + + fixation = visual.GratingStim(win=self.window, size=0.2, pos=[0, 0], sf=0.2, color=[1, 0, 0], autoDraw=True) + + # Generate the possible ssvep frequencies based on monitor refresh rate + def get_possible_ssvep_freqs(frame_rate, stim_type="single"): + + max_period_nb = int(frame_rate / 6) + periods = np.arange(max_period_nb) + 1 + + if stim_type == "single": + freqs = dict() + for p1 in periods: + for p2 in periods: + f = frame_rate / (p1 + p2) + try: + freqs[f].append((p1, p2)) + except: + freqs[f] = [(p1, p2)] + + elif stim_type == "reversal": + freqs = {frame_rate / p: [(p, p)] for p in periods[::-1]} + + return freqs + + def init_flicker_stim(frame_rate, cycle, soa): + + if isinstance(cycle, tuple): + stim_freq = frame_rate / sum(cycle) + n_cycles = int(soa * stim_freq) + + else: + stim_freq = frame_rate / cycle + cycle = (cycle, cycle) + n_cycles = int(soa * stim_freq) / 2 + + return {"cycle": cycle, "freq": stim_freq, "n_cycles": n_cycles} + + # Set up stimuli + frame_rate = np.round(self.window.getActualFrameRate()) # Frame rate, in Hz + freqs = get_possible_ssvep_freqs(frame_rate, stim_type="reversal") + self.stim_patterns = [ + init_flicker_stim(frame_rate, 2, self.soa), + init_flicker_stim(frame_rate, 3, self.soa), + ] + + print( + ( + "Flickering frequencies (Hz): {}\n".format( + [self.stim_patterns[0]["freq"], self.stim_patterns[1]["freq"]] + ) ) - eeg.start(save_fn, duration=record_duration) + ) - # Iterate through trials - start = time() - for ii, trial in trials.iterrows(): - # Intertrial interval - core.wait(iti + np.random.rand() * jitter) + return [ + init_flicker_stim(frame_rate, 2, self.soa), + init_flicker_stim(frame_rate, 3, self.soa), + ] + def present_stimulus(self, idx, trial): + # Select stimulus frequency - ind = trials["stim_freq"].iloc[ii] + ind = self.trials["parameter"].iloc[idx] # Push sample - if eeg: + if self.eeg: timestamp = time() - if eeg.backend == "muselsl": - marker = [markernames[ind]] + if self.eeg.backend == "muselsl": + marker = [self.markernames[ind]] else: - marker = markernames[ind] - eeg.push_sample(marker=marker, timestamp=timestamp) + marker = self.markernames[ind] + self.eeg.push_sample(marker=marker, timestamp=timestamp) # Present flickering stim - for _ in range(int(stim_patterns[ind]["n_cycles"])): - grating.setAutoDraw(True) - for _ in range(int(stim_patterns[ind]["cycle"][0])): - mywin.flip() - grating.setAutoDraw(False) - grating_neg.setAutoDraw(True) - for _ in range(stim_patterns[ind]["cycle"][1]): - mywin.flip() - grating_neg.setAutoDraw(False) - - # offset - 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 SSVEP 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. - - Warning: This experiment contains flashing lights and may induce a seizure. Discretion is advised. - - """ - 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() + for _ in range(int(self.stim_patterns[ind]["n_cycles"])): + self.grating.setAutoDraw(True) + for _ in range(int(self.stim_patterns[ind]["cycle"][0])): + self.window.flip() + self.grating.setAutoDraw(False) + self.grating_neg.setAutoDraw(True) + for _ in range(self.stim_patterns[ind]["cycle"][1]): + self.window.flip() + self.grating_neg.setAutoDraw(False) + pass + + self.window.flip() \ No newline at end of file diff --git a/eegnb/experiments/visual_vep/vep.py b/eegnb/experiments/visual_vep/vep.py index 0da5d8fd..1acd1b11 100644 --- a/eegnb/experiments/visual_vep/vep.py +++ b/eegnb/experiments/visual_vep/vep.py @@ -1,9 +1,21 @@ -import numpy as np -from pandas import DataFrame -from psychopy import visual, core, event from time import time, strftime, gmtime -from optparse import OptionParser from pylsl import StreamInfo, StreamOutlet +from eegnb.experiments.Experiment import Experiment + + +class VisualVEP(Experiment): + + def __init__(self, duration=120, eeg: EEG=None, save_fn=None, + n_trials = 2000, iti = 0.2, soa = 0.2, jitter = 0.1): + + exp_name = "Visual VEP" + super().__init__(exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter) + + def load_stimulus(): + pass + + def present_stimulus(): + pass def present(duration=120): From 05bdf62073dc1673611dcbc7e73258e1412de2f0 Mon Sep 17 00:00:00 2001 From: Parvfect Date: Mon, 5 Sep 2022 02:34:36 +0100 Subject: [PATCH 08/32] Submodule added for gsoc --- .gitmodules | 3 +++ GSOC-eeg-notebooks | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 GSOC-eeg-notebooks diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..efe8fc68 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "GSOC-eeg-notebooks"] + path = GSOC-eeg-notebooks + url = https://github.com/Parvfect/GSOC-eeg-notebooks diff --git a/GSOC-eeg-notebooks b/GSOC-eeg-notebooks new file mode 160000 index 00000000..e0514d73 --- /dev/null +++ b/GSOC-eeg-notebooks @@ -0,0 +1 @@ +Subproject commit e0514d73aa3d4a68561f25876dfb3a09e0e6a493 From 67147400f0e14e32cc2b83ffb9ab1574a298a792 Mon Sep 17 00:00:00 2001 From: Parv Agarwal <65726543+Parvfect@users.noreply.github.com> Date: Sun, 16 Oct 2022 20:31:18 +0100 Subject: [PATCH 09/32] Adding pipelines for cli analysis (#202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * started pipelines function * almost working simple function equivalents of nb scripts * fix: fixed import (brainflow updated API) * sqc fixes for unicorn (#176) * Ignore pushes * Trying to create a cli * Stepping through the problem * First commit * Fixing pause in signal quality check * Fixing Signal quality check problem * fix the technical debt * Save path done for automated saving pdf * I feel amazing * Almost through * Update eegnb/cli/__main__.py Co-authored-by: Erik Bjäreholt * Trying to create cli but it's being really painful * Extra word cli error * Changed example handling * Pain * Adding whole datapath * Finally fixed cli * hmm * Looking good * added hyperlink * Having some issues with detecting css and image deltetion * Just the css now * Fixed the css linking problem though it's a weird soln * Automated running, still fnames problem * Hahahah embedded images in html * Improving code * Okay now * Look at that * Almost there just the two figures now * Now * Added attrdict to do with cli error Co-authored-by: John Griffiths Co-authored-by: Erik Bjäreholt Co-authored-by: John Griffiths --- eegnb/analysis/analysis_report.html | 39 +++ eegnb/analysis/analysis_report.py | 95 +++++++ .../experiment_descriptions/visual-N170.txt | 2 + .../experiment_descriptions/visual-P300.txt | 2 + eegnb/analysis/pipelines.py | 243 ++++++++++++++++++ eegnb/analysis/report.html | 60 +++++ eegnb/analysis/styling.css | 34 +++ eegnb/analysis/utils.py | 81 +++--- eegnb/cli/__main__.py | 50 +++- eegnb/cli/introprompt.py | 61 ++++- requirements.txt | 3 + testing.py | 12 + 12 files changed, 641 insertions(+), 41 deletions(-) create mode 100644 eegnb/analysis/analysis_report.html create mode 100644 eegnb/analysis/analysis_report.py create mode 100644 eegnb/analysis/experiment_descriptions/visual-N170.txt create mode 100644 eegnb/analysis/experiment_descriptions/visual-P300.txt create mode 100644 eegnb/analysis/pipelines.py create mode 100644 eegnb/analysis/report.html create mode 100644 eegnb/analysis/styling.css create mode 100644 testing.py diff --git a/eegnb/analysis/analysis_report.html b/eegnb/analysis/analysis_report.html new file mode 100644 index 00000000..55dda918 --- /dev/null +++ b/eegnb/analysis/analysis_report.html @@ -0,0 +1,39 @@ + + + + + Analysis Report + + + +
+

Analysis Report

+

+ Experiment Name: {}
+ Subject Id: {}
+ Session Id: {}
+ EEG Device: {}
+ Drop Percentage: {}

+ This is an analysis report for the experiment.
For more information about the experiment, please visit the documentation +

+
+
+

Raw Epoch

+

+ The raw epoch is shown below. The raw epoch is the data that is recorded from the EEG headset. The raw epoch is then processed to remove noise and artifacts. +

+ Raw Epoch +
+
+

Stimulus Response

+

+ The stimulus response is shown below. The stimulus response is the data that is recorded from the EEG headset after removing noise and artifacts. +

+ Stimulus Response +
+ + \ No newline at end of file diff --git a/eegnb/analysis/analysis_report.py b/eegnb/analysis/analysis_report.py new file mode 100644 index 00000000..9df3b88d --- /dev/null +++ b/eegnb/analysis/analysis_report.py @@ -0,0 +1,95 @@ + +# Generating html using Python + +from airium import Airium +from typing import Dict +import os +import eegnb +import base64 + +a = Airium() + +def get_experiment_information(experiment:str): + analysis_save_path = os.path.join(os.path.dirname(eegnb.__file__), "analysis") + file_path = os.path.join(analysis_save_path, "experiment_descriptions") + + with open(os.path.join(file_path, experiment + ".txt"), 'r') as f: + experiment_text = f.readlines() + + return experiment_text + +def get_img_string(image_save_path): + """ Returns image as string to embed into the html report """ + return base64.b64encode(open(image_save_path, "rb").read()).decode() + +def get_html(experimental_parameters: Dict): + + # add variable to store the link + analysis_save_path = os.path.join(os.path.dirname(eegnb.__file__), "analysis") + css_path = os.path.join(analysis_save_path, "styling.css") + eeg_device, experiment, subject, session, example, drop_percentage, epochs_chosen = experimental_parameters.values() + + erp_image_path = os.path.join(os.getcwd(), "erp_plot.png") + pos_image_path = os.path.join(os.getcwd(), "power_spectrum.png") + + experiment_text = get_experiment_information(experiment) + + + """ Possibility of unique experiment text - decision to be made """ + #experiment_text = "" + #with open('experiment_descriptions/{}.txt'.format(experiment), 'r') as f: + # experiment_text = f.readlines() + + a('') + with a.html(): + with a.head(): + a.link(href=css_path, rel='stylesheet', type="text/css") + a.title(_t="Analysis Report") + + with a.body(): + + # Navigation bar + with a.div(klass="topnav"): + a.a(_t="Description", href="#Description") + a.a(_t="Raw Epoch", href="#Raw Epoch") + a.a(_t="Stimulus Response", href="#Stimulus Response") + + # Description + with a.div(id="Description"): + a.h1(_t="Analysis Report") + with a.p(): + a("Experiment Name: {}
".format(experiment)) + + if example: + a("Example File
") + else: + a("Subject Id: {}
".format(subject)) + a("Session Id: {}
".format(session)) + + a("EEG Device: {}
".format(eeg_device)) + a('This is an analysis report for the experiment.
For more information about the experiment, please visit the documentation

') + a("{}
".format(experiment_text[0])) + a("{}
".format(experiment_text[1])) + + # Raw Epoch + with a.div(id="Raw Epoch"): + a.h2(_t="Raw Epoch") + with a.p(): + a("The power spectrum of the raw epoch is displayed below. The raw epoch is then processed to remove noise and artifacts.") + a.img(src="data:image/png;base64, {}".format(get_img_string(pos_image_path)), alt="Raw Epoch") + + # Stimulus Response + with a.div(id="Stimulus Response"): + a.h2(_t="Stimulus Response") + with a.p(): + a("The stimulus response is shown below. The stimulus response is the amplitude response at the specific timescales where the response to the stimulus can be detected.
") + a("Epochs chosen: {}
".format(epochs_chosen)) + a("Drop Percentage: {} %

".format(round(drop_percentage,2))) + a.img(src="data:image/png;base64, {}".format(get_img_string(erp_image_path)), alt="Stimulus Response") + + # Delete the images + os.remove(erp_image_path) + os.remove(pos_image_path) + + # Return the html + return str(a) diff --git a/eegnb/analysis/experiment_descriptions/visual-N170.txt b/eegnb/analysis/experiment_descriptions/visual-N170.txt new file mode 100644 index 00000000..ec9c24a7 --- /dev/null +++ b/eegnb/analysis/experiment_descriptions/visual-N170.txt @@ -0,0 +1,2 @@ +The N170 is a large negative event-related potential (ERP) component that occurs after the detection of faces, but not objects, scrambled faces, or other body parts such as hands. +In the experiment we aim to detect the N170 using faces and houses as our stimuli. \ No newline at end of file diff --git a/eegnb/analysis/experiment_descriptions/visual-P300.txt b/eegnb/analysis/experiment_descriptions/visual-P300.txt new file mode 100644 index 00000000..d756becd --- /dev/null +++ b/eegnb/analysis/experiment_descriptions/visual-P300.txt @@ -0,0 +1,2 @@ +The P300 is a positive event-related potential (ERP) that occurs around 300ms after perceiving a novel or unexpected stimulus. It is most commonly elicited through ‘oddball’ experimental paradigms, where a certain subtype of stimulus is presented rarely amidst a background of another more common type of stimulus. +In the experiment, we aimed to elicit P300 response using a visual oddball stimulation. \ No newline at end of file diff --git a/eegnb/analysis/pipelines.py b/eegnb/analysis/pipelines.py new file mode 100644 index 00000000..a4604531 --- /dev/null +++ b/eegnb/analysis/pipelines.py @@ -0,0 +1,243 @@ +""" + +CLI Pipeline for Analysis of EEGNB Recorded Data + +To do: +1. Beautify analysis pdf +2. Handle cli automated errors for report creation + +Usage: + +For Recorded Data: + +from eegnb.analysis.pipelines import create_analysis_report() +create_analysis_report(experiment, eegdevice, subject, session, filepath)s + +For Example Datasets: + +from eegnb.analysis.pipelines import example_analysis_report() +example_analysis_report() + +""" + +# Some standard pythonic imports +import os +from collections import OrderedDict +import warnings +import matplotlib.pyplot as plt +from datetime import datetime +import numpy as np +from typing import Dict + +warnings.filterwarnings('ignore') + +# MNE functions +from mne import Epochs,find_events, create_info +from mne.io import RawArray + +# EEG-Notebooks functions +from eegnb import generate_save_fn +from eegnb.analysis.utils import load_data,plot_conditions, load_csv_as_raw, fix_musemissinglines +from eegnb.analysis.analysis_report import get_html +from eegnb.datasets import fetch_dataset +from eegnb.devices.utils import EEG_INDICES, SAMPLE_FREQS +from pathlib import Path + +DATA_DIR = os.path.join(os.path.expanduser("~/"), ".eegnb", "data") +eegdevice, experiment_name, subject_id, session_nb, example_flag = None, None, None, None, False + +def load_eeg_data(experiment, subject=1, session=1, device_name='muse2016_bfn', tmin=-0.1, tmax=0.6, baseline=None, + reject={'eeg': 5e-5}, preload=True, verbose=1, + picks=[0,1,2,3], event_id = OrderedDict(House=1,Face=2), fnames=None, example=False): + """ + Loads EEG data from the specified experiment, subject, session, and device. + Returns the raw and epochs objects. + + Procedure + 1. Loads the data using file names and retrives if not already present + 2. Epochs the data + 3. Computes the ERP + 4. Returns the raw and ERP objects + + Parameters + ---------- + experiment : Experiment Name + subject : Subject ID of performed experiment + session : Session ID of performed experiment + device_name : Device used for performed experiment + tmin : Start time of the epochs in seconds, relative to the time-locked event. + tmax : End time of the epochs in seconds, relative to the time-locked event. + baseline : Not very sure..? + reject : Rejection parameters for the epochs. + preload : If True, preload the epochs into memory. + verbose : If True, print out messages. + picks : Channels to include in the analysis. + event_id : Dictionary of event_id's for the epochs + fnames : File names of the experiment data, if not passed, example files are used + """ + + # If not using the example dataset, load the data from the specified experiment using load_csv_as_raw + if not example: + + # Obataining the specific parameters to load the data into MNE object + sfreq = SAMPLE_FREQS[device_name] + ch_ind = EEG_INDICES[device_name] + + # Generate file names if not passed + if fnames is None: + raw = load_data(subject_id=subject, session_nb=session, experiment=experiment, device_name=device_name, site="local", data_dir=os.path.join(os.path.expanduser('~/'),'.eegnb', 'data')) + + else: + # Replace Ch names has arbitarily been set to None + if device_name in ["muse2016", "muse2", "museS"]: + raw = load_csv_as_raw([fnames], sfreq=sfreq, ch_ind=ch_ind, aux_ind=[5], replace_ch_names=None, verbose=verbose) + else: + raw = load_csv_as_raw([fnames], sfreq=sfreq, ch_ind=ch_ind, replace_ch_names=None, verbose=verbose) + + # Getting the subject and session + subject, session = fnames.split('_')[1], fnames.split('_')[2] + + # If using the example dataset, load the data from the example dataset + else: + subject, session = 1, 1 + + # Loading Data + eegnb_data_path = os.path.join(os.path.expanduser('~/'),'.eegnb', 'data') + experiment_data_path = os.path.join(eegnb_data_path, experiment, 'eegnb_examples') + + # If dataset hasn't been downloaded yet, download it + if not os.path.isdir(experiment_data_path): + fetch_dataset(data_dir=eegnb_data_path, experiment=experiment, site='eegnb_examples') + + raw = load_data(1,1, + experiment=experiment, site='eegnb_examples', device_name=device_name, + data_dir = eegnb_data_path) + + # Filtering the data under a certain frequency range + raw.filter(1,30, method='iir') + + # Visualising the power spectrum + fig = raw.plot_psd(fmin=1, fmax=30, show=False) + + # Saving the figure so it can be accessed by the pdf creation. Automatically deleted when added to the pdf. + plt.tight_layout() + plt.savefig("power_spectrum.png") + plt.show(block=False) + plt.pause(10) + plt.close() + + # Epoching + # Create an array containing the timestamps and type of each stimulus (i.e. face or house) + events = find_events(raw) + + # Create an MNE Epochs object representing all the epochs around stimulus presentation + epochs = Epochs(raw, events=events, event_id=event_id, + tmin=tmin, tmax=tmax, baseline=baseline, + reject=reject, preload=preload, + verbose=verbose, picks=picks) + + print('sample drop %: ', (1 - len(epochs.events)/len(events)) * 100) + print(len(epochs.events), 'events found') + print(epochs) + + experimental_parameters = {"eeg_device": device_name, "experiment_name": experiment, "subject_id": subject, "session_nb": session, "example_flag": example, "drop_percent": (1 - len(epochs.events)/len(events)) * 100, "epochs_chosen": len(epochs.events)} + + return epochs, experimental_parameters + + +def make_erp_plot(epochs, experimental_parameters:Dict, conditions=OrderedDict(House=[1],Face=[2]), ci=97.5, n_boot=1000, title='', + diff_waveform=None, #(1, 2)) + channel_order=[1,0,2,3]): + """ + Plots the ERP for the specified conditions. + + Parameters + ---------- + epochs : MNE Epochs object + conditions : OrderedDict holding the conditions to plot + ci: confidence interval + n_boot: number of bootstrap samples + title: title of the plot + diff_waveform: tuple of two integers indicating the channels to compare + channel_order: list of integers indicating the order of the channels to plot + """ + + fig, ax = plot_conditions(epochs, conditions=conditions, + ci=97.5, n_boot=1000, title='', + diff_waveform=None, #(1, 2)) + channel_order=[1,0,2,3]) # reordering of epochs.ch_names according to [[0,2],[1,3]] of subplot axes + + # Autoscaling the y axis to a tight fit to the ERP + for i in [0,1,2,3]: ax[i].autoscale(tight=True) + + # Saving the figure so it can be accessed by the pdf creation. Automatically deleted when added to the pdf. + # Makes sure that the axis labels are not cut out + plt.tight_layout() + plt.savefig("erp_plot.png") + plt.show(block=False) + plt.pause(10) + plt.close() + + # Creating the pdf, needs to be discussed whether we want to call it here or seperately. + create_pdf(experimental_parameters) + +def create_pdf(experimental_parameters:Dict): + """Creates analysis report using the power spectrum and ERP plots that are saved in the directory""" + + # Unpack the experimental parameters + eegdevice, experiment, subject, session, example, drop_percentage, epochs_chosen = experimental_parameters.values() + + # Getting the directory where the report should be saved + save_dir = get_save_directory(experiment=experiment, eegdevice=eegdevice, subject=subject, session=session, example=example, label="analysis") + + #get whole filepath + filepath = os.path.join(save_dir, 'analysis_report_{}.html'.format(datetime.now().strftime("%d-%m-%Y_%H-%M-%S"))) + + # Get the report + report_html = get_html(experimental_parameters) + + # Save html file + with open(filepath, 'w') as f: + f.write(report_html) + + # Informing the user that the report has been saved + print('Analysis report saved to {}\n'.format(filepath)) + print("Open the report by clicking the following link: {}{}".format("file:///", filepath)) + +def get_save_directory(experiment, eegdevice, subject, session, example, label): + """ Returns save directory as a String for the analysis report """ + + if not example: + site='local' + else: + site='eegnb_examples' + + # Getting the directory where the analysis report should be saved + save_path = os.path.join(os.path.expanduser("~/"),'.eegnb', label) + save_path = os.path.join(save_path, experiment, site, eegdevice, "subject{}".format(subject), "session{}".format(session)) + + # Creating the directory if it doesn't exist + if not os.path.isdir(save_path): + os.makedirs(save_path) + + return save_path + +def create_analysis_report_(experiment, eegdevice, subject=None, session=None, data_path=None, bluemuse_file_fix=False): + """ Interface with the erp plot function, basically cli type instructions """ + + # Prompt user to enter options and then take inputs and do the necessary + epochs, experimental_parameters = load_eeg_data(experiment=experiment, subject=subject, session=session, device_name=eegdevice, example=False, fnames=data_path) + make_erp_plot(epochs, experimental_parameters) + +def example_analysis_report(): + """ Example of how to use the analysis report function """ + + experiment = ["visual-N170", "visual-P300"] + experiment_choice = experiment[int(input("Choose an experiment: {} 0 or 1\n".format(experiment)))] + + if experiment_choice == "visual-N170": + epochs, experimental_parameters = load_eeg_data(experiment_choice, example=True) + make_erp_plot(epochs, experimental_parameters) + else: + epochs, experimental_parameters = load_eeg_data('visual-P300', device_name='muse2016', event_id={'Non-Target': 1, 'Target': 2}, example=True) + make_erp_plot(epochs, experimental_parameters, conditions=OrderedDict(NonTarget=[1],Target=[2])) \ No newline at end of file diff --git a/eegnb/analysis/report.html b/eegnb/analysis/report.html new file mode 100644 index 00000000..b0f91f77 --- /dev/null +++ b/eegnb/analysis/report.html @@ -0,0 +1,60 @@ + + + + + + + Analysis Report + + + + + + +
+

+ Analysis Report +

+

+ Experiment Name:
+ Subject Id:
+ Session Number:
+ EEG Headset:
+ Drop Percentage:
+

+ +

+ This is an analysis report for the experiment.
+ For more information about the experiment, please visit the documentation. +

+
+ + +
+

+ Raw Epoch +

+

+ The raw epoch is shown below. The raw epoch is the data that is recorded from the EEG headset. The raw epoch is then processed to remove noise and artifacts. +

+ Raw Epoch +
+ + +
+

+ Stimulus Response +

+

+ The stimulus response is shown below. The stimulus response is the data that is recorded from the EEG headset after the raw epoch has been processed. The stimulus response is then used to calculate the power spectrum. +

+ Stimulus Response +
+ + + diff --git a/eegnb/analysis/styling.css b/eegnb/analysis/styling.css new file mode 100644 index 00000000..4b000843 --- /dev/null +++ b/eegnb/analysis/styling.css @@ -0,0 +1,34 @@ +/* Add a black background color to the top navigation */ +.topnav { + background-color: #333; + overflow: hidden; + } + + /* Style the links inside the navigation bar */ + .topnav a { + float: left; + color: #f2f2f2; + text-align: center; + padding: 14px 16px; + text-decoration: none; + font-size: 17px; + } + + /* Change the color of links on hover */ + .topnav a:hover { + background-color: #ddd; + color: black; + } + + /* Add a color to the active/current link */ + .topnav a.active { + background-color: #04AA6D; + color: white; + } + + /* Centre the images */ + img { + display: block; + margin-left: auto; + margin-right: auto; + } \ No newline at end of file diff --git a/eegnb/analysis/utils.py b/eegnb/analysis/utils.py index bd67baa9..d0ca7c58 100644 --- a/eegnb/analysis/utils.py +++ b/eegnb/analysis/utils.py @@ -5,11 +5,15 @@ from collections import OrderedDict from glob import glob from typing import Union, List, Dict +from collections import Iterable from time import sleep, time from numpy.core.fromnumeric import std +import keyboard +import os import pandas as pd import numpy as np +import matplotlib.pyplot as plt import seaborn as sns from mne import create_info, concatenate_raws from mne.io import RawArray @@ -22,8 +26,6 @@ from eegnb.devices.eeg import EEG from eegnb.devices.utils import EEG_INDICES, SAMPLE_FREQS - - # this should probably not be done here sns.set_context("talk") sns.set_style("white") @@ -32,6 +34,22 @@ logger = logging.getLogger(__name__) +# Empirically determined lower and upper bounds of +# acceptable temporal standard deviations +# for different EEG devices tested by us +openbci_devices = ['ganglion', 'ganglion_wifi', 'cyton', 'cyton_wifi', 'cyton_daisy_wifi'] +muse_devices = ['muse' + model + sfx for model in ['2016', '2', 'S'] for sfx in ['', '_bfn', '_bfb']] +neurosity_devices = ['notion1', 'notion2', 'crown'] +gtec_devices = ['unicorn'] +alltesteddevices = openbci_devices + muse_devices + neurosity_devices + gtec_devices +thres_stds = {} +for device in alltesteddevices: + if device in openbci_devices: thres_stds[device] = [1,9] + elif device in muse_devices: thres_stds[device] = [1,18] + elif device in neurosity_devices: thres_stds[device] = [1,15] + elif device in gtec_devices: thres_stds[device] = [1,15] + + def load_csv_as_raw( fnames: List[str], sfreq: float, @@ -68,6 +86,7 @@ def load_csv_as_raw( n_aux = 0 raw = [] + for fn in fnames: # Read the file data = pd.read_csv(fn) @@ -91,7 +110,7 @@ def load_csv_as_raw( # create MNE object info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq, verbose=1) raw.append(RawArray(data=data, info=info, verbose=verbose)) - + raws = concatenate_raws(raw, verbose=verbose) montage = make_standard_montage("standard_1005") raws.set_montage(montage) @@ -445,25 +464,14 @@ def check_report(eeg: EEG, n_times: int=60, pause_time=5, thres_std_low=None, th # If no upper and lower std thresholds set in function call, # set thresholds based on the following per-device name defaults - if thres_std_high is None: - if eeg.device_name in ["ganglion", "ganglion_wifi", "cyton", - "cyton_wifi", "cyton_daisy", "cyton_daisy_wifi"]: - thres_std_high = 9 - elif eeg.device_name in ["notion1", "notion2", "crown"]: - thres_std_high = 15 - elif 'muse' in eeg.device_name: - thres_std_high = 18 - + edn = eeg.device_name + flag = False if thres_std_low is None: - - if 'muse' in eeg.device_name: - thres_std_low = 1 - - elif eeg.device_name in ["ganglion", "ganglion_wifi", "cyton", - "cyton_wifi", "cyton_daisy", "cyton_daisy_wifi", - "notion1", "notion2", "crown"]: - thres_std_low = 1 - + if edn in thres_stds.keys(): + thres_std_low = thres_stds[edn][0] + if thres_std_high is None: + if edn in thres_stds.keys(): + thres_std_high = thres_stds[edn][1] print("\n\nRunning signal quality check...") print(f"Accepting threshold stdev between: {thres_std_low} - {thres_std_high}") @@ -493,7 +501,6 @@ def check_report(eeg: EEG, n_times: int=60, pause_time=5, thres_std_low=None, th print("\nSignal quality:") print(indicators) - bad_channels = [k for k, v in std_series.iteritems() if v < thres_std_low or v > thres_std_high ] if bad_channels: print(f"Bad channels: {', '.join(bad_channels)}") @@ -510,26 +517,26 @@ def check_report(eeg: EEG, n_times: int=60, pause_time=5, thres_std_low=None, th if (loop_index+1) % n_inarow == 0: print(f"\n\nLooks like you still have {len(bad_channels)} bad channels after {loop_index+1} tries\n") - prompt_start = time() - continue_sigqual = input("\nChecks will resume in %s seconds...Press 'c' (and ENTER key) if you want to stop adjusting for better quality.\n" %pause_time) - while time() < prompt_start + 5: - if continue_sigqual == 'c': - break - if continue_sigqual == 'c': - print("\nStopping signal quality checks!") - break - - sleep(pause_time) - - - + prompt_time = time() + print(f"Starting next cycle in 5 seconds, press C and enter to cancel") + while time() < prompt_time + 5: + if keyboard.is_pressed('c'): + print("\nStopping signal quality checks!") + flag = True + break + if flag: + break + def fix_musemissinglines(orig_f,new_f=''): - if new_f == '': new_f = orig_f.replace('.csv', '_fml.csv') + #if new_f == '': new_f = orig_f.replace('.csv', '_fml.csv') + + # Overwriting + new_f = orig_f print('writing fixed file to %s' %new_f) - # Read oriignal file + # Read original file F = open(orig_f, 'r') Ls = F.readlines() diff --git a/eegnb/cli/__main__.py b/eegnb/cli/__main__.py index 6e57fbce..b924fc50 100644 --- a/eegnb/cli/__main__.py +++ b/eegnb/cli/__main__.py @@ -6,10 +6,12 @@ import shutil from eegnb.datasets.datasets import zip_data_folders -from .introprompt import intro_prompt +from .introprompt import intro_prompt, analysis_intro_prompt from .utils import run_experiment +from eegnb import generate_save_fn from eegnb.devices.eeg import EEG from eegnb.analysis.utils import check_report +from eegnb.analysis.pipelines import load_eeg_data, make_erp_plot, create_analysis_report_, example_analysis_report @click.group(name="eegnb") @@ -35,6 +37,7 @@ def runexp( outfname: str = None, prompt: bool = False, dosigqualcheck = True, + generatereport = True ): """ Run experiment. @@ -59,6 +62,8 @@ def runexp( if prompt: eeg, experiment, recdur, outfname = intro_prompt() else: + # Random values for outfile for now + outfname = generate_save_fn(eegdevice, experiment,7, 7) if eegdevice == "ganglion": # if the ganglion is chosen a MAC address should also be provided eeg = EEG(device=eegdevice, mac_addr=macaddr) @@ -73,16 +78,55 @@ def askforsigqualcheck(): "Sorry, didn't recognize answer. " askforsigqualcheck() + def askforreportcheck(): + generatereport = input("\n\nGenerate Report? (Y/n): \n").lower() != "n" + if dosigqualcheck: askforsigqualcheck() - + + if generatereport: + askforreportcheck() run_experiment(experiment, eeg, recdur, outfname) print(f"\n\n\nExperiment complete! Recorded data is saved @ {outfname}") + if generatereport: + # Error of filenames being multiple etc, needs to be handled + create_analysis_report(experiment=experiment, device_name=eegdevice, fnames=outfname) +@main.command() +@click.option("-ex", "--experiment", help="Experiment to run") +@click.option("-ed", "--eegdevice", help="EEG device to use") +@click.option("-sub", "--subject", help="Subject ID") +@click.option("-sess", "--session", help="Session number") +@click.option("-fp", "--filepath", help="Filepath to save data") +@click.option( + "-ip", "--prompt", help="Use interactive prompt to ask for parameters", is_flag=True +) +def create_analysis_report( + experiment: str, + eegdevice: str = None, + subject: str = None, + session: str = None, + filepath:str = None, + prompt: bool = False, +): + """ + Create analysis report of recorded data + """ + + if prompt: + example = input("Do you want to load an example experiment? (y/n)\n") + print() + if example == 'y': + example_analysis_report() + return + else: + experiment, eegdevice, subject, session, filepath = analysis_intro_prompt() + create_analysis_report_(experiment, eegdevice, subject, session, filepath) + @main.command() @click.option("-ed", "--eegdevice", help="EEG device to use", required=True) @@ -107,6 +151,8 @@ def checksigqual(eegdevice: str): # valuess in the function definition ] + + @main.command() @click.option("-ex", "--experiment", help="Experiment to zip", required=False) @click.option( diff --git a/eegnb/cli/introprompt.py b/eegnb/cli/introprompt.py index 6eaf90c9..647651bb 100644 --- a/eegnb/cli/introprompt.py +++ b/eegnb/cli/introprompt.py @@ -25,7 +25,7 @@ def device_prompt() -> EEG: "ganglion": "OpenBCI Ganglion", "cyton": "OpenBCI Cyton", "cyton_daisy": "OpenBCI Cyton + Daisy", - "unicord": "G.Tec Unicorn", + "unicorn": "G.Tec Unicorn", "brainbit": "BrainBit", "notion1": "Notion 1", "notion2": "Notion 2", @@ -96,7 +96,7 @@ def exp_prompt(runorzip:str='run') -> str: ) ) - exp_idx = int(input("\nEnter Experiment Selection: ")) + exp_idx = int(input("\nEnter Experiment Selection: \n")) exp_selection = list(experiments.keys())[exp_idx] print(f"Selected experiment: {exp_selection} \n") @@ -157,6 +157,63 @@ def intro_prompt() -> Tuple[EEG, str, int, str]: return eeg_device, exp_selection, duration, str(save_fn) +def analysis_device_prompt(): + + boards = { + "none": "None", + "muse2016": "Muse (2016)", + "muse2": "Muse 2", + "museS": "Muse S", + "muse2016_bfn": "Muse 2016 - brainflow, native bluetooth", + "muse2016_bfb": "Muse 2016 - brainflow, BLED bluetooth dongle", + "muse2_bfn": "Muse 2 - brainflow, native bluetooth", + "muse2_bfb": "Muse 2 - brainflow, BLED bluetooth dongle", + "museS_bfn": "Muse S - brainflow, native bluetooth", + "museS_bfb": "Muse S - brainflow, BLED bluetooth dongle", + "ganglion": "OpenBCI Ganglion", + "cyton": "OpenBCI Cyton", + "cyton_daisy": "OpenBCI Cyton + Daisy", + "unicorn": "G.Tec Unicorn", + "brainbit": "BrainBit", + "notion1": "Notion 1", + "notion2": "Notion 2", + "crown": "Crown", + "synthetic": "Synthetic", + "freeeeg32": "FreeEEG32", + } + + print("Please enter the integer value corresponding to your EEG device: \n") + print("\n".join(f"[{i:2}] {board}" for i, board in enumerate(boards.values()))) + + board_idx = int(input("\nEnter Board Selection: \n")) + + # Board_codes are the actual names to be passed to the EEG class + board_code = list(boards.keys())[board_idx] + return board_code + +def analysis_intro_prompt(): + + # check if user has filepath + print("Welcome to NeurotechX EEG Notebooks\n") + print("Do you have a filepath to a .csv file you would like to analyze? \n") + print("[1] Yes \n") + print("[0] No \n") + file_idx = int(input("Enter selection: \n")) + if file_idx == 1: + print("Please enter the filepath to the .csv file you would like to analyze. \n") + filepath = input("Enter filepath: \n") + subject, session = None, None + else: + subject = int(input("Enter subject ID#: \n")) + session = int(input("Enter session #: \n")) + filepath = None + + eegdevice = analysis_device_prompt() + experiment = exp_prompt() + + return experiment, eegdevice, subject, session, filepath + + def intro_prompt_zip() -> Tuple[str,str]: """This function handles the user prompts for inputting information for zipping their function.""" diff --git a/requirements.txt b/requirements.txt index 2ad031b2..c85486eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,9 @@ pyserial>=3.5 h5py>=3.1.0 pytest-shutil pyo>=1.0.3; platform_system == "Linux" +keyboard==0.13.5 +airium>=0.1.0 +attrdict>=2.0.1 # This might try to build from source on linux (since there are no wheels for Linux on PyPI) . # You can pass `--find-links=https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04/` your `pip install` to use the prebuilt wheels at the link. diff --git a/testing.py b/testing.py new file mode 100644 index 00000000..fff46054 --- /dev/null +++ b/testing.py @@ -0,0 +1,12 @@ + + +import matplotlib.pyplot as plt +import numpy as np +from eegnb.analysis.pipelines import load_eeg_data, make_erp_plot +from eegnb.analysis.utils import fix_musemissinglines + +file_path = r"C:\Users\Parv\.eegnb\data\visual-N170\local\muse2\subject0001\session004\recording_2022-08-15-19.09.37.csv" + +raw, epochs = load_eeg_data(experiment='visual-N170', subject=1, session=3, device_name='muse2', example=False) +make_erp_plot(epochs) + From 3f3281c37c20418ee817959e0741a46f2d3e9675 Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Sun, 16 Oct 2022 17:04:01 -0400 Subject: [PATCH 10/32] added more options for site args; improved function names; removed some redundant lines (#209) --- eegnb/analysis/pipelines.py | 27 +++++++++++++++------------ eegnb/analysis/utils.py | 30 ++++++++++++++++++++---------- eegnb/cli/__main__.py | 11 ++++++----- eegnb/cli/introprompt.py | 10 ++++++++-- 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/eegnb/analysis/pipelines.py b/eegnb/analysis/pipelines.py index a4604531..aa67d953 100644 --- a/eegnb/analysis/pipelines.py +++ b/eegnb/analysis/pipelines.py @@ -46,8 +46,9 @@ DATA_DIR = os.path.join(os.path.expanduser("~/"), ".eegnb", "data") eegdevice, experiment_name, subject_id, session_nb, example_flag = None, None, None, None, False + def load_eeg_data(experiment, subject=1, session=1, device_name='muse2016_bfn', tmin=-0.1, tmax=0.6, baseline=None, - reject={'eeg': 5e-5}, preload=True, verbose=1, + reject={'eeg': 5e-5}, preload=True, verbose=1, site='local', picks=[0,1,2,3], event_id = OrderedDict(House=1,Face=2), fnames=None, example=False): """ Loads EEG data from the specified experiment, subject, session, and device. @@ -85,9 +86,9 @@ def load_eeg_data(experiment, subject=1, session=1, device_name='muse2016_bfn', # Generate file names if not passed if fnames is None: - raw = load_data(subject_id=subject, session_nb=session, experiment=experiment, device_name=device_name, site="local", data_dir=os.path.join(os.path.expanduser('~/'),'.eegnb', 'data')) - + raw = load_data(subject=subject, session=session, experiment=experiment, device_name=device_name, site=site, data_dir=DATA_DIR) else: + # Replace Ch names has arbitarily been set to None if device_name in ["muse2016", "muse2", "museS"]: raw = load_csv_as_raw([fnames], sfreq=sfreq, ch_ind=ch_ind, aux_ind=[5], replace_ch_names=None, verbose=verbose) @@ -99,19 +100,17 @@ def load_eeg_data(experiment, subject=1, session=1, device_name='muse2016_bfn', # If using the example dataset, load the data from the example dataset else: - subject, session = 1, 1 # Loading Data - eegnb_data_path = os.path.join(os.path.expanduser('~/'),'.eegnb', 'data') - experiment_data_path = os.path.join(eegnb_data_path, experiment, 'eegnb_examples') + experiment_data_path = os.path.join(DATA_DIR, experiment, 'eegnb_examples') # If dataset hasn't been downloaded yet, download it if not os.path.isdir(experiment_data_path): - fetch_dataset(data_dir=eegnb_data_path, experiment=experiment, site='eegnb_examples') + fetch_dataset(data_dir=DATA_DIR,experiment=experiment, site='eegnb_examples') - raw = load_data(1,1, + raw = load_data(subject = subject, session = session, experiment=experiment, site='eegnb_examples', device_name=device_name, - data_dir = eegnb_data_path) + data_dir = DATA_DIR) # Filtering the data under a certain frequency range raw.filter(1,30, method='iir') @@ -181,6 +180,7 @@ def make_erp_plot(epochs, experimental_parameters:Dict, conditions=OrderedDict(H # Creating the pdf, needs to be discussed whether we want to call it here or seperately. create_pdf(experimental_parameters) + def create_pdf(experimental_parameters:Dict): """Creates analysis report using the power spectrum and ERP plots that are saved in the directory""" @@ -204,6 +204,7 @@ def create_pdf(experimental_parameters:Dict): print('Analysis report saved to {}\n'.format(filepath)) print("Open the report by clicking the following link: {}{}".format("file:///", filepath)) + def get_save_directory(experiment, eegdevice, subject, session, example, label): """ Returns save directory as a String for the analysis report """ @@ -222,13 +223,15 @@ def get_save_directory(experiment, eegdevice, subject, session, example, label): return save_path -def create_analysis_report_(experiment, eegdevice, subject=None, session=None, data_path=None, bluemuse_file_fix=False): + +def analysis_report(experiment, eegdevice, subject=None, session=None, site='local', data_path=None, bluemuse_file_fix=False): """ Interface with the erp plot function, basically cli type instructions """ # Prompt user to enter options and then take inputs and do the necessary - epochs, experimental_parameters = load_eeg_data(experiment=experiment, subject=subject, session=session, device_name=eegdevice, example=False, fnames=data_path) + epochs, experimental_parameters = load_eeg_data(experiment=experiment, subject=subject, session=session, site=site, device_name=eegdevice, example=False, fnames=data_path) make_erp_plot(epochs, experimental_parameters) + def example_analysis_report(): """ Example of how to use the analysis report function """ @@ -240,4 +243,4 @@ def example_analysis_report(): make_erp_plot(epochs, experimental_parameters) else: epochs, experimental_parameters = load_eeg_data('visual-P300', device_name='muse2016', event_id={'Non-Target': 1, 'Target': 2}, example=True) - make_erp_plot(epochs, experimental_parameters, conditions=OrderedDict(NonTarget=[1],Target=[2])) \ No newline at end of file + make_erp_plot(epochs, experimental_parameters, conditions=OrderedDict(NonTarget=[1],Target=[2])) diff --git a/eegnb/analysis/utils.py b/eegnb/analysis/utils.py index d0ca7c58..32a8691f 100644 --- a/eegnb/analysis/utils.py +++ b/eegnb/analysis/utils.py @@ -77,6 +77,13 @@ def load_csv_as_raw( (mne.io.RawArray): concatenation of the specified filenames into a single Raw object. """ + + + print('\n\nLoading these files: \n') + for f in fnames: print(f + '\n') + print('\n\n') + + ch_ind = copy.deepcopy(ch_ind) n_eeg = len(ch_ind) if aux_ind is not None: @@ -119,8 +126,8 @@ def load_csv_as_raw( def load_data( - subject_id: Union[str, int], - session_nb: Union[str, int], + subject: Union[str, int], + session: Union[str, int], device_name: str, experiment: str, replace_ch_names=None, @@ -142,9 +149,9 @@ def load_data( given at the time of recording. Args: - subject_id (int or str): subject number. If 'all', load all + subject (int or str): subject number. If 'all', load all subjects. - session_nb (int or str): session number. If 'all', load all + session (int or str): session number. If 'all', load all sessions. device_name (str): name of device. For a list of supported devices, see eegnb.analysis.utils.SAMPLE_FREQS. @@ -165,17 +172,20 @@ def load_data( (mne.io.RawArray): loaded EEG """ - subject_str = "*" if subject_id == "all" else f"subject{subject_id:04}" - session_str = "*" if session_nb == "all" else f"session{session_nb:03}" + subject_str = "*" if subject == "all" else f"subject{subject:04}" + session_str = "*" if session == "all" else f"session{session:03}" if site == "all": site = "*" - data_path = ( - _get_recording_dir(device_name, experiment, subject_str, session_str, site, data_dir) - / "*.csv" - ) + recdir = _get_recording_dir(device_name, experiment, subject_str, session_str, site)#, data_dir) + data_path = os.path.join(data_dir, recdir, "*.csv") + fnames = glob(str(data_path)) + if len(fnames) == 0: + raise Exception("No filenames found in folder: %s" %data_path) + + sfreq = SAMPLE_FREQS[device_name] ch_ind = EEG_INDICES[device_name] diff --git a/eegnb/cli/__main__.py b/eegnb/cli/__main__.py index b924fc50..9b727a03 100644 --- a/eegnb/cli/__main__.py +++ b/eegnb/cli/__main__.py @@ -11,7 +11,7 @@ from eegnb import generate_save_fn from eegnb.devices.eeg import EEG from eegnb.analysis.utils import check_report -from eegnb.analysis.pipelines import load_eeg_data, make_erp_plot, create_analysis_report_, example_analysis_report +from eegnb.analysis.pipelines import load_eeg_data, make_erp_plot, analysis_report, example_analysis_report @click.group(name="eegnb") @@ -101,15 +101,16 @@ def askforreportcheck(): @click.option("-ed", "--eegdevice", help="EEG device to use") @click.option("-sub", "--subject", help="Subject ID") @click.option("-sess", "--session", help="Session number") +@click.option("-site", "--site", help="Site/Study Name") @click.option("-fp", "--filepath", help="Filepath to save data") -@click.option( - "-ip", "--prompt", help="Use interactive prompt to ask for parameters", is_flag=True +@click.option("-ip", "--prompt", help="Use interactive prompt to ask for parameters", is_flag=True ) def create_analysis_report( experiment: str, eegdevice: str = None, subject: str = None, session: str = None, + site: str = None, filepath:str = None, prompt: bool = False, ): @@ -124,8 +125,8 @@ def create_analysis_report( example_analysis_report() return else: - experiment, eegdevice, subject, session, filepath = analysis_intro_prompt() - create_analysis_report_(experiment, eegdevice, subject, session, filepath) + experiment, eegdevice, subject, session, site, filepath = analysis_intro_prompt() + analysis_report(experiment, eegdevice, subject, session, site, filepath) @main.command() diff --git a/eegnb/cli/introprompt.py b/eegnb/cli/introprompt.py index 647651bb..6f8aa488 100644 --- a/eegnb/cli/introprompt.py +++ b/eegnb/cli/introprompt.py @@ -8,6 +8,7 @@ eegnb_sites = ['eegnb_examples', 'grifflab_dev', 'jadinlab_home'] + def device_prompt() -> EEG: # define the names of the available boards # boards is a mapping from board code to board description @@ -102,6 +103,7 @@ def exp_prompt(runorzip:str='run') -> str: return exp_selection + def site_prompt(experiment:str) -> str: experiment_dir=os.path.join(DATA_DIR,experiment) @@ -124,6 +126,7 @@ def site_prompt(experiment:str) -> str: print("Selected Folder : {} \n".format(site)) return site + def intro_prompt() -> Tuple[EEG, str, int, str]: """This function handles the user prompts for inputting information about the session they wish to record.""" print("Welcome to NeurotechX EEG Notebooks\n") @@ -157,6 +160,7 @@ def intro_prompt() -> Tuple[EEG, str, int, str]: return eeg_device, exp_selection, duration, str(save_fn) + def analysis_device_prompt(): boards = { @@ -191,6 +195,7 @@ def analysis_device_prompt(): board_code = list(boards.keys())[board_idx] return board_code + def analysis_intro_prompt(): # check if user has filepath @@ -202,16 +207,17 @@ def analysis_intro_prompt(): if file_idx == 1: print("Please enter the filepath to the .csv file you would like to analyze. \n") filepath = input("Enter filepath: \n") - subject, session = None, None + subject, session, site = None, None, None else: subject = int(input("Enter subject ID#: \n")) session = int(input("Enter session #: \n")) + site = str(input("Enter site name: \n")) filepath = None eegdevice = analysis_device_prompt() experiment = exp_prompt() - return experiment, eegdevice, subject, session, filepath + return experiment, eegdevice, subject, session, site, filepath From 79652af421adda327ffb733bedbf8fc632c62bd7 Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Sun, 16 Oct 2022 19:40:59 -0400 Subject: [PATCH 11/32] fix subject num parsing bug --- eegnb/analysis/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eegnb/analysis/utils.py b/eegnb/analysis/utils.py index 32a8691f..21feb74c 100644 --- a/eegnb/analysis/utils.py +++ b/eegnb/analysis/utils.py @@ -172,10 +172,11 @@ def load_data( (mne.io.RawArray): loaded EEG """ - subject_str = "*" if subject == "all" else f"subject{subject:04}" - session_str = "*" if session == "all" else f"session{session:03}" - if site == "all": - site = "*" + subject_int = int(subject) + session_int = int(session) + + subject_str = "*" if subject == "all" else f"subject{subject_int:04}" + session_str = "*" if session == "all" else f"session{session_int:03}" recdir = _get_recording_dir(device_name, experiment, subject_str, session_str, site)#, data_dir) data_path = os.path.join(data_dir, recdir, "*.csv") From a62280ac11fa6ef69be9294f6921b10fd894d11f Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Mon, 17 Oct 2022 02:21:55 -0400 Subject: [PATCH 12/32] analysis report function improvements for openbci cyton and gtec unicorn devices --- eegnb/analysis/pipelines.py | 31 ++++++++++++++++++++----------- eegnb/analysis/utils.py | 26 ++++++++++++-------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/eegnb/analysis/pipelines.py b/eegnb/analysis/pipelines.py index aa67d953..cfd9f9f3 100644 --- a/eegnb/analysis/pipelines.py +++ b/eegnb/analysis/pipelines.py @@ -49,7 +49,7 @@ def load_eeg_data(experiment, subject=1, session=1, device_name='muse2016_bfn', tmin=-0.1, tmax=0.6, baseline=None, reject={'eeg': 5e-5}, preload=True, verbose=1, site='local', - picks=[0,1,2,3], event_id = OrderedDict(House=1,Face=2), fnames=None, example=False): + picks=None, event_id = OrderedDict(House=1,Face=2), fnames=None, example=False): """ Loads EEG data from the specified experiment, subject, session, and device. Returns the raw and epochs objects. @@ -90,10 +90,13 @@ def load_eeg_data(experiment, subject=1, session=1, device_name='muse2016_bfn', else: # Replace Ch names has arbitarily been set to None - if device_name in ["muse2016", "muse2", "museS"]: - raw = load_csv_as_raw([fnames], sfreq=sfreq, ch_ind=ch_ind, aux_ind=[5], replace_ch_names=None, verbose=verbose) + if device_name in ["muse2016", "muse2", "museS"]: + aux_ind=[5] else: - raw = load_csv_as_raw([fnames], sfreq=sfreq, ch_ind=ch_ind, replace_ch_names=None, verbose=verbose) + aux_ind=None + + raw = load_csv_as_raw([fnames], sfreq=sfreq, ch_ind=ch_ind, aux_ind=aux_ind, + replace_ch_names=None, verbose=verbose) # Getting the subject and session subject, session = fnames.split('_')[1], fnames.split('_')[2] @@ -130,6 +133,9 @@ def load_eeg_data(experiment, subject=1, session=1, device_name='muse2016_bfn', events = find_events(raw) # Create an MNE Epochs object representing all the epochs around stimulus presentation + if picks is None: + picks = range(len(ch_ind)) + epochs = Epochs(raw, events=events, event_id=event_id, tmin=tmin, tmax=tmax, baseline=baseline, reject=reject, preload=preload, @@ -144,9 +150,7 @@ def load_eeg_data(experiment, subject=1, session=1, device_name='muse2016_bfn', return epochs, experimental_parameters -def make_erp_plot(epochs, experimental_parameters:Dict, conditions=OrderedDict(House=[1],Face=[2]), ci=97.5, n_boot=1000, title='', - diff_waveform=None, #(1, 2)) - channel_order=[1,0,2,3]): +def make_erp_plot(epochs, experimental_parameters:Dict, conditions=OrderedDict(House=[1],Face=[2]), ci=97.5, n_boot=1000, title='',diff_waveform=None,channel_order=None): """ Plots the ERP for the specified conditions. @@ -160,14 +164,19 @@ def make_erp_plot(epochs, experimental_parameters:Dict, conditions=OrderedDict(H diff_waveform: tuple of two integers indicating the channels to compare channel_order: list of integers indicating the order of the channels to plot """ + + if 'muse' in experimental_parameters['eeg_device']: channel_order = [1,0,2,3] # reordering of epochs.ch_names according to [[0,2],[1,3]] of subplot axes + nchan = len(epochs.ch_names) + if channel_order is None: channel_order = range(0,nchan) fig, ax = plot_conditions(epochs, conditions=conditions, ci=97.5, n_boot=1000, title='', diff_waveform=None, #(1, 2)) - channel_order=[1,0,2,3]) # reordering of epochs.ch_names according to [[0,2],[1,3]] of subplot axes + channel_order=channel_order, + channel_count=nchan) # Autoscaling the y axis to a tight fit to the ERP - for i in [0,1,2,3]: ax[i].autoscale(tight=True) + for i in range(nchan): ax[i].autoscale(tight=True) # [0,1,2,3,4] # Saving the figure so it can be accessed by the pdf creation. Automatically deleted when added to the pdf. # Makes sure that the axis labels are not cut out @@ -224,11 +233,11 @@ def get_save_directory(experiment, eegdevice, subject, session, example, label): return save_path -def analysis_report(experiment, eegdevice, subject=None, session=None, site='local', data_path=None, bluemuse_file_fix=False): +def analysis_report(experiment, eegdevice, subject=None, session=None, site='local', data_path=None, bluemuse_file_fix=False,reject={'eeg': 5e-05}): """ Interface with the erp plot function, basically cli type instructions """ # Prompt user to enter options and then take inputs and do the necessary - epochs, experimental_parameters = load_eeg_data(experiment=experiment, subject=subject, session=session, site=site, device_name=eegdevice, example=False, fnames=data_path) + epochs, experimental_parameters = load_eeg_data(experiment=experiment, subject=subject, session=session, site=site, device_name=eegdevice, example=False, fnames=data_path, reject=reject) make_erp_plot(epochs, experimental_parameters) diff --git a/eegnb/analysis/utils.py b/eegnb/analysis/utils.py index 21feb74c..119500d8 100644 --- a/eegnb/analysis/utils.py +++ b/eegnb/analysis/utils.py @@ -57,6 +57,7 @@ def load_csv_as_raw( aux_ind=None, replace_ch_names=None, verbose=1, + resp_on_missing='warn' ) -> RawArray: """Load CSV files into an MNE Raw object. @@ -120,7 +121,7 @@ def load_csv_as_raw( raws = concatenate_raws(raw, verbose=verbose) montage = make_standard_montage("standard_1005") - raws.set_montage(montage) + raws.set_montage(montage,on_missing=resp_on_missing) return raws @@ -134,8 +135,8 @@ def load_data( verbose=1, site="local", data_dir=None, - inc_chans=None, -) -> RawArray: + inc_chans=None + ) -> RawArray: """Load CSV files from the /data directory into a Raw object. This is a utility function that simplifies access to eeg-notebooks @@ -195,22 +196,19 @@ def load_data( ch_ind = inc_chans if device_name in ["muse2016", "muse2", "museS"]: - return load_csv_as_raw( - fnames, - sfreq=sfreq, - ch_ind=ch_ind, - aux_ind=[5], - replace_ch_names=replace_ch_names, - verbose=verbose, - ) + aux_ind = [5] else: - return load_csv_as_raw( + aux_ind = None + + res = load_csv_as_raw( fnames, sfreq=sfreq, ch_ind=ch_ind, + aux_ind=aux_ind, replace_ch_names=replace_ch_names, - verbose=verbose, - ) + verbose=verbose) + + return res def plot_conditions( From e7bee1bda237744df7836327fc19bdbfbc46cf1e Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Tue, 18 Oct 2022 13:07:53 -0400 Subject: [PATCH 13/32] run exp fix --- eegnb/cli/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eegnb/cli/__main__.py b/eegnb/cli/__main__.py index 9b727a03..6a59af38 100644 --- a/eegnb/cli/__main__.py +++ b/eegnb/cli/__main__.py @@ -91,9 +91,9 @@ def askforreportcheck(): print(f"\n\n\nExperiment complete! Recorded data is saved @ {outfname}") - if generatereport: - # Error of filenames being multiple etc, needs to be handled - create_analysis_report(experiment=experiment, device_name=eegdevice, fnames=outfname) + #if generatereport: + # # Error of filenames being multiple etc, needs to be handled + # create_analysis_report(experiment=experiment, device_name=eegdevice, fnames=outfname) @main.command() From e6e92b92cf0fb3fba3e0c5bbc7ea8be448e614c9 Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Thu, 20 Oct 2022 11:14:55 -0400 Subject: [PATCH 14/32] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c85486eb..6b347d40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ pyo>=1.0.3; platform_system == "Linux" keyboard==0.13.5 airium>=0.1.0 attrdict>=2.0.1 +attrdict3 # This might try to build from source on linux (since there are no wheels for Linux on PyPI) . # You can pass `--find-links=https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04/` your `pip install` to use the prebuilt wheels at the link. From 30fb77c26b3121532487ca3c53537b2f33aff6b3 Mon Sep 17 00:00:00 2001 From: Ben Pettit Date: Thu, 27 Oct 2022 14:33:15 +1000 Subject: [PATCH 15/32] fixes to get docs building by github action (#210) * fixes to get docs building by github action * reverted some changes * Update 01r__ssvep_viz.py Co-authored-by: John Griffiths --- .github/workflows/docs.yml | 11 ++++++----- .gitignore | 3 +++ .../visual_cueing/01r__cueing_singlesub_analysis.py | 4 ++-- examples/visual_cueing/02r__cueing_group_analysis.py | 4 ++-- .../04r__cueing_group_analysis_winter2019.py | 4 ++-- examples/visual_cueing/CueingAnalysis_Colab.ipynb | 4 ++-- .../visual_cueing/CueingGroupAnalysis_Colab.ipynb | 4 ++-- .../CueingGroupAnalysis_Colab_Winter2019.ipynb | 4 ++-- examples/visual_cueing/cueing.ipynb | 4 ++-- examples/visual_cueing/cueing_group_analysis.ipynb | 4 ++-- examples/visual_cueing/cueing_loop.ipynb | 4 ++-- examples/visual_ssvep/01r__ssvep_viz.py | 1 + 12 files changed, 28 insertions(+), 23 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f1f23d9d..67ad74d8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,20 +8,21 @@ on: jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip wheel + python -m pip install attrdict # Install wxPython wheels since they are distribution-specific and therefore not on PyPI # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 wxPython + pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04 wxPython pip install . - name: Build docs diff --git a/.gitignore b/.gitignore index 7d84d133..d6bf9a8f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ examples/visual_cueing/*.csv .coverage coverage.xml htmlcov + +# PyCharm +.idea/ \ No newline at end of file diff --git a/examples/visual_cueing/01r__cueing_singlesub_analysis.py b/examples/visual_cueing/01r__cueing_singlesub_analysis.py index fcee0f83..9ba11022 100644 --- a/examples/visual_cueing/01r__cueing_singlesub_analysis.py +++ b/examples/visual_cueing/01r__cueing_singlesub_analysis.py @@ -132,7 +132,7 @@ # Left Cue tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, n_cycles=wave_cycles, return_itc=True) -tfr = tfr.apply_baseline([-1,-.5],mode='mean') +tfr = tfr.apply_baseline((-1,-.5),mode='mean') tfr.plot(picks=[0], mode='logratio', title='TP9 - Ipsi'); tfr.plot(picks=[1], mode='logratio', @@ -143,7 +143,7 @@ # Right Cue tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, n_cycles=wave_cycles, return_itc=True) -tfr = tfr.apply_baseline([-1,-.5],mode='mean') +tfr = tfr.apply_baseline((-1,-.5),mode='mean') tfr.plot(picks=[0], mode='logratio', title='TP9 - Contra'); tfr.plot(picks=[1], mode='logratio', diff --git a/examples/visual_cueing/02r__cueing_group_analysis.py b/examples/visual_cueing/02r__cueing_group_analysis.py index 9053412a..535d8db3 100644 --- a/examples/visual_cueing/02r__cueing_group_analysis.py +++ b/examples/visual_cueing/02r__cueing_group_analysis.py @@ -123,7 +123,7 @@ # Left Cue tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, n_cycles=wave_cycles, return_itc=True) - tfr = tfr.apply_baseline([-1,-.5],mode='mean') + tfr = tfr.apply_baseline((-1,-.5),mode='mean') #tfr.plot(picks=[0], mode='logratio', # title='TP9 - Ipsi'); #tfr.plot(picks=[3], mode='logratio', @@ -134,7 +134,7 @@ # Right Cue tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, n_cycles=wave_cycles, return_itc=True) - tfr = tfr.apply_baseline([-1,-.5],mode='mean') + tfr = tfr.apply_baseline((-1,-.5),mode='mean') #tfr.plot(picks=[0], mode='logratio', # title='TP9 - Contra'); #tfr.plot(picks=[3], mode='logratio', diff --git a/examples/visual_cueing/04r__cueing_group_analysis_winter2019.py b/examples/visual_cueing/04r__cueing_group_analysis_winter2019.py index 8c2fb9cc..50c382c8 100644 --- a/examples/visual_cueing/04r__cueing_group_analysis_winter2019.py +++ b/examples/visual_cueing/04r__cueing_group_analysis_winter2019.py @@ -136,14 +136,14 @@ # Left Cue tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, n_cycles=wave_cycles, return_itc=True) - tfr = tfr.apply_baseline([-1,-.5],mode='mean') + tfr = tfr.apply_baseline((-1,-.5),mode='mean') power_Ipsi_TP9 = tfr.data[0,:,:] power_Contra_TP10 = tfr.data[1,:,:] # Right Cue tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, n_cycles=wave_cycles, return_itc=True) - tfr = tfr.apply_baseline([-1,-.5],mode='mean') + tfr = tfr.apply_baseline((-1,-.5),mode='mean') power_Contra_TP9 = tfr.data[0,:,:] power_Ipsi_TP10 = tfr.data[1,:,:] diff --git a/examples/visual_cueing/CueingAnalysis_Colab.ipynb b/examples/visual_cueing/CueingAnalysis_Colab.ipynb index 47bfd1b2..5a5712c1 100644 --- a/examples/visual_cueing/CueingAnalysis_Colab.ipynb +++ b/examples/visual_cueing/CueingAnalysis_Colab.ipynb @@ -434,7 +434,7 @@ "# Left Cue\n", "tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - "tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + "tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", "tfr.plot(picks=[0], mode='logratio', \n", " title='TP9 - Ipsi');\n", "tfr.plot(picks=[1], mode='logratio', \n", @@ -445,7 +445,7 @@ "# Right Cue\n", "tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - "tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + "tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", "tfr.plot(picks=[0], mode='logratio', \n", " title='TP9 - Contra');\n", "tfr.plot(picks=[1], mode='logratio', \n", diff --git a/examples/visual_cueing/CueingGroupAnalysis_Colab.ipynb b/examples/visual_cueing/CueingGroupAnalysis_Colab.ipynb index 1b7c9172..1d3d25dc 100644 --- a/examples/visual_cueing/CueingGroupAnalysis_Colab.ipynb +++ b/examples/visual_cueing/CueingGroupAnalysis_Colab.ipynb @@ -215,7 +215,7 @@ " # Left Cue\n", " tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - " tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + " tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", " #tfr.plot(picks=[0], mode='logratio', \n", " # title='TP9 - Ipsi');\n", " #tfr.plot(picks=[3], mode='logratio', \n", @@ -226,7 +226,7 @@ " # Right Cue\n", " tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - " tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + " tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", " #tfr.plot(picks=[0], mode='logratio', \n", " # title='TP9 - Contra');\n", " #tfr.plot(picks=[3], mode='logratio', \n", diff --git a/examples/visual_cueing/CueingGroupAnalysis_Colab_Winter2019.ipynb b/examples/visual_cueing/CueingGroupAnalysis_Colab_Winter2019.ipynb index 390ec855..30f8af16 100644 --- a/examples/visual_cueing/CueingGroupAnalysis_Colab_Winter2019.ipynb +++ b/examples/visual_cueing/CueingGroupAnalysis_Colab_Winter2019.ipynb @@ -215,14 +215,14 @@ " # Left Cue\n", " tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - " tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + " tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", " power_Ipsi_TP9 = tfr.data[0,:,:]\n", " power_Contra_TP10 = tfr.data[1,:,:]\n", "\n", " # Right Cue\n", " tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - " tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + " tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", " power_Contra_TP9 = tfr.data[0,:,:]\n", " power_Ipsi_TP10 = tfr.data[1,:,:]\n", "\n", diff --git a/examples/visual_cueing/cueing.ipynb b/examples/visual_cueing/cueing.ipynb index 0406e41e..ad9f3fb6 100644 --- a/examples/visual_cueing/cueing.ipynb +++ b/examples/visual_cueing/cueing.ipynb @@ -529,7 +529,7 @@ "# Left Cue\n", "tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - "tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + "tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", "tfr.plot(picks=[0], mode='logratio', \n", " title='TP9 - Ipsi');\n", "tfr.plot(picks=[1], mode='logratio', \n", @@ -540,7 +540,7 @@ "# Right Cue\n", "tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - "tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + "tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", "tfr.plot(picks=[0], mode='logratio', \n", " title='TP9 - Contra');\n", "tfr.plot(picks=[1], mode='logratio', \n", diff --git a/examples/visual_cueing/cueing_group_analysis.ipynb b/examples/visual_cueing/cueing_group_analysis.ipynb index bd781d91..44dcbc79 100755 --- a/examples/visual_cueing/cueing_group_analysis.ipynb +++ b/examples/visual_cueing/cueing_group_analysis.ipynb @@ -225,7 +225,7 @@ " # Left Cue\n", " tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - " tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + " tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", " #tfr.plot(picks=[0], mode='logratio', \n", " # title='TP9 - Ipsi');\n", " #tfr.plot(picks=[3], mode='logratio', \n", @@ -236,7 +236,7 @@ " # Right Cue\n", " tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - " tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + " tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", " #tfr.plot(picks=[0], mode='logratio', \n", " # title='TP9 - Contra');\n", " #tfr.plot(picks=[3], mode='logratio', \n", diff --git a/examples/visual_cueing/cueing_loop.ipynb b/examples/visual_cueing/cueing_loop.ipynb index 969c08d0..8894dcfe 100644 --- a/examples/visual_cueing/cueing_loop.ipynb +++ b/examples/visual_cueing/cueing_loop.ipynb @@ -601,7 +601,7 @@ " # Left Cue\n", " tfr, itc = tfr_morlet(epochs['LeftCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - " tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + " tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", " #tfr.plot(picks=[0], mode='logratio', \n", " # title='TP9 - Ipsi');\n", " #tfr.plot(picks=[3], mode='logratio', \n", @@ -612,7 +612,7 @@ " # Right Cue\n", " tfr, itc = tfr_morlet(epochs['RightCue'], freqs=frequencies, \n", " n_cycles=wave_cycles, return_itc=True)\n", - " tfr = tfr.apply_baseline([-1,-.5],mode='mean')\n", + " tfr = tfr.apply_baseline((-1,-.5),mode='mean')\n", " #tfr.plot(picks=[0], mode='logratio', \n", " # title='TP9 - Contra');\n", " #tfr.plot(picks=[3], mode='logratio', \n", diff --git a/examples/visual_ssvep/01r__ssvep_viz.py b/examples/visual_ssvep/01r__ssvep_viz.py index 91d9ad50..d812681c 100644 --- a/examples/visual_ssvep/01r__ssvep_viz.py +++ b/examples/visual_ssvep/01r__ssvep_viz.py @@ -59,6 +59,7 @@ experiment='visual-SSVEP', site='eegnb_examples', device_name='muse2016', data_dir = eegnb_data_path, replace_ch_names={'Right AUX': 'POz'}) +raw.set_channel_types({'POz': 'eeg'}) ################################################################################################### # Visualize the power spectrum From 678346378001c442ff19d4040da8ad9feb1b6dcd Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Thu, 1 Dec 2022 14:52:02 -0500 Subject: [PATCH 16/32] Update README.rst small commit to test doc build workflow on this branch --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f64d676d..285f2516 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ EEG-Notebooks EEG-Notebooks is a collection of classic EEG experiments, implemented in Python 3 and Jupyter notebooks. The experimental protocols and analyses are quite generic, but are primarily taylored for low-budget / consumer EEG hardware such as the InteraXon MUSE and OpenBCI Cyton. The goal is to make cognitive neuroscience and neurotechnology more accessible, affordable, and scalable. -- **For an intro talk on the eeg-notebooks project see:** `JG's Brainhack Ontario 2020 presentation `_. +- **For an intro talk on the eeg-notebooks project see:** `JG's Brainhack Ontario presentation `_. - **For documentation see:** `documentation site `_. - **For code see:** `github site `_. - **For instructions on running experiments see:** `running experiments `_. From 3b1585fa237da609650504d1b832c208ecebd776 Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Thu, 19 Jan 2023 10:41:23 -0500 Subject: [PATCH 17/32] removing gsoc submodule --- GSOC-eeg-notebooks | 1 - 1 file changed, 1 deletion(-) delete mode 160000 GSOC-eeg-notebooks diff --git a/GSOC-eeg-notebooks b/GSOC-eeg-notebooks deleted file mode 160000 index e0514d73..00000000 --- a/GSOC-eeg-notebooks +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e0514d73aa3d4a68561f25876dfb3a09e0e6a493 From 478fa49b782e2c0f6ddc50f6ef66cd15ca3d11d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 3 Mar 2023 15:03:12 +0100 Subject: [PATCH 18/32] ci: update python to 3.8, update wxPython, update setup-python action --- .github/workflows/test.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccec3e05..58d93909 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,22 +14,25 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - python_version: [3.7] + python_version: ['3.8'] include: - - os: ubuntu-latest - python_version: 3.8 # Experimental: Python 3.9 # Works fine, commented out because mostly covered (at least installing/building deps) by the typecheck job # See issue: https://github.com/NeuroTechX/eeg-notebooks/issues/50 #- os: ubuntu-latest # python_version: 3.9 + # Check 3.10 for future-proofing + - os: ubuntu-latest + python_version: '3.10' + steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python_version }} + # Not needed if pywinhook is installed from wheels #- name: Install swig # if: "startsWith(runner.os, 'windows')" @@ -37,6 +40,7 @@ jobs: # (New-Object System.Net.WebClient).DownloadFile("http://prdownloads.sourceforge.net/swig/swigwin-4.0.1.zip","swigwin-4.0.1.zip"); # Expand-Archive .\swigwin-4.0.1.zip .; # echo "$((Get-Item .).FullName)/swigwin-4.0.1" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Install APT dependencies if: "startsWith(runner.os, 'Linux')" run: | @@ -52,8 +56,9 @@ jobs: python -m pip install --upgrade pip wheel # Install wxPython wheels since they are distribution-specific and therefore not on PyPI + # This could possibly be bumped to 22.04 even. # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 wxPython + pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04 wxPython pip install . - name: Install MacOS/Windows dependencies @@ -110,7 +115,7 @@ jobs: # Install wxPython wheels since they are distribution-specific and therefore not on PyPI # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 wxPython + pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04 wxPython pip install . - name: Install MacOS/Windows dependencies From 3bd12a7cb5d618d4237f63c514aed436613fdd8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 3 Mar 2023 15:08:43 +0100 Subject: [PATCH 19/32] ci: pin ubuntu versions to 22.04, updated wxPython urls --- .github/workflows/docs.yml | 4 ++-- .github/workflows/test.yml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 67ad74d8..eb09e561 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python @@ -22,7 +22,7 @@ jobs: # Install wxPython wheels since they are distribution-specific and therefore not on PyPI # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04 wxPython + pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython pip install . - name: Build docs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58d93909..ae7f9302 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: ['ubuntu-22.04', windows-latest, macOS-latest] python_version: ['3.8'] include: # Experimental: Python 3.9 @@ -23,7 +23,7 @@ jobs: # python_version: 3.9 # Check 3.10 for future-proofing - - os: ubuntu-latest + - os: ubuntu-22.04 python_version: '3.10' steps: @@ -56,11 +56,11 @@ jobs: python -m pip install --upgrade pip wheel # Install wxPython wheels since they are distribution-specific and therefore not on PyPI - # This could possibly be bumped to 22.04 even. # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04 wxPython + pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython pip install . + - name: Install MacOS/Windows dependencies run: | python -m pip install --upgrade pip wheel @@ -90,7 +90,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: ['ubuntu-22.04'] python_version: [3.9] steps: @@ -115,7 +115,7 @@ jobs: # Install wxPython wheels since they are distribution-specific and therefore not on PyPI # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04 wxPython + pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython pip install . - name: Install MacOS/Windows dependencies From a4fe823baa712ba9ca8872378db379c81e876005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 3 Mar 2023 15:18:29 +0100 Subject: [PATCH 20/32] ci: bumped psychopy to 2023.1.0 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 568058b3..e21ed8aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ # Main repo requirements -psychopy==2020.2.3 +psychopy==2023.1.0 psychtoolbox scikit-learn>=0.23.2 pandas>=1.1.4 numpy>=1.19.4 mne>=0.20.8 -seaborn==0.9.0 +seaborn>=0.9.0 pyriemann>=0.2.7 jupyter muselsl>=2.0.2 From c2c5d57aae346bf220777ef42919631efde72426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 3 Mar 2023 15:23:26 +0100 Subject: [PATCH 21/32] build(deps): set upper supported numpy version to 1.23.x --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e21ed8aa..ddd23eff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ psychopy==2023.1.0 psychtoolbox scikit-learn>=0.23.2 pandas>=1.1.4 -numpy>=1.19.4 +numpy>=1.19.4,<1.24 # due to outdated libs not changing the names after: https://github.com/numpy/numpy/pull/22607 mne>=0.20.8 seaborn>=0.9.0 pyriemann>=0.2.7 From adf5e289cd0c212a792a4e259667369fea39c586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 3 Mar 2023 15:25:11 +0100 Subject: [PATCH 22/32] chore: applied no_implicit_optional --- eegnb/cli/__main__.py | 19 ++++++++++--------- eegnb/cli/utils.py | 3 ++- eegnb/experiments/auditory_oddball/aMMN.py | 3 ++- eegnb/experiments/auditory_oddball/aob.py | 3 ++- .../auditory_oddball/auditory_erp_arrayin.py | 4 +++- .../experiments/visual_codeprose/codeprose.py | 4 ++-- eegnb/experiments/visual_n170/n170.py | 3 ++- eegnb/experiments/visual_p300/p300.py | 3 ++- eegnb/experiments/visual_ssvep/ssvep.py | 3 ++- eegnb/experiments/visual_vep/vep.py | 3 ++- 10 files changed, 29 insertions(+), 19 deletions(-) diff --git a/eegnb/cli/__main__.py b/eegnb/cli/__main__.py index 6a59af38..9a196e8e 100644 --- a/eegnb/cli/__main__.py +++ b/eegnb/cli/__main__.py @@ -12,6 +12,7 @@ from eegnb.devices.eeg import EEG from eegnb.analysis.utils import check_report from eegnb.analysis.pipelines import load_eeg_data, make_erp_plot, analysis_report, example_analysis_report +from typing import Optional @click.group(name="eegnb") @@ -31,10 +32,10 @@ def main(): ) def runexp( experiment: str, - eegdevice: str = None, - macaddr: str = None, - recdur: float = None, - outfname: str = None, + eegdevice: Optional[str] = None, + macaddr: Optional[str] = None, + recdur: Optional[float] = None, + outfname: Optional[str] = None, prompt: bool = False, dosigqualcheck = True, generatereport = True @@ -107,11 +108,11 @@ def askforreportcheck(): ) def create_analysis_report( experiment: str, - eegdevice: str = None, - subject: str = None, - session: str = None, - site: str = None, - filepath:str = None, + eegdevice: Optional[str] = None, + subject: Optional[str] = None, + session: Optional[str] = None, + site: Optional[str] = None, + filepath: Optional[str] = None, prompt: bool = False, ): """ diff --git a/eegnb/cli/utils.py b/eegnb/cli/utils.py index 322a16dd..dfa10699 100644 --- a/eegnb/cli/utils.py +++ b/eegnb/cli/utils.py @@ -15,6 +15,7 @@ from eegnb.experiments.visual_codeprose import codeprose from eegnb.experiments.auditory_oddball import diaconescu from eegnb.experiments.auditory_ssaep import ssaep, ssaep_onefreq +from typing import Optional # New Experiment Class structure has a different initilization, to be noted @@ -40,7 +41,7 @@ def get_exp_desc(exp: str): def run_experiment( - experiment: str, eeg_device: EEG, record_duration: float = None, save_fn=None + experiment: str, eeg_device: EEG, record_duration: Optional[float] = None, save_fn=None ): if experiment in experiments: module = experiments[experiment] diff --git a/eegnb/experiments/auditory_oddball/aMMN.py b/eegnb/experiments/auditory_oddball/aMMN.py index 67305a69..cf809c0a 100644 --- a/eegnb/experiments/auditory_oddball/aMMN.py +++ b/eegnb/experiments/auditory_oddball/aMMN.py @@ -9,10 +9,11 @@ from psychopy import visual, core, event, sound from eegnb import generate_save_fn +from typing import Optional def present( - save_fn: str = None, + save_fn: Optional[str] = None, duration=120, stim_types=None, itis=None, diff --git a/eegnb/experiments/auditory_oddball/aob.py b/eegnb/experiments/auditory_oddball/aob.py index 32ccd1b2..1659100b 100644 --- a/eegnb/experiments/auditory_oddball/aob.py +++ b/eegnb/experiments/auditory_oddball/aob.py @@ -5,11 +5,12 @@ from time import time from eegnb.devices.eeg import EEG from eegnb.experiments import Experiment +from typing import Optional class AuditoryOddball(Experiment.BaseExperiment): - def __init__(self, duration=120, eeg: EEG=None, save_fn=None, n_trials = 2010, iti = 0.3, soa = 0.2, jitter = 0.2, secs=0.2, volume=0.8, random_state=42, s1_freq="C", s2_freq="D", s1_octave=5, s2_octave=6): + def __init__(self, duration=120, eeg: Optional[EEG]=None, save_fn=None, n_trials = 2010, iti = 0.3, soa = 0.2, jitter = 0.2, secs=0.2, volume=0.8, random_state=42, s1_freq="C", s2_freq="D", s1_octave=5, s2_octave=6): """ Auditory Oddball Experiment diff --git a/eegnb/experiments/auditory_oddball/auditory_erp_arrayin.py b/eegnb/experiments/auditory_oddball/auditory_erp_arrayin.py index 27a2e485..1100e49c 100644 --- a/eegnb/experiments/auditory_oddball/auditory_erp_arrayin.py +++ b/eegnb/experiments/auditory_oddball/auditory_erp_arrayin.py @@ -16,10 +16,12 @@ from eegnb import generate_save_fn from eegnb.devices.eeg import EEG +from typing import Optional + #from eegnb.stimuli import FACE_HOUSE -def present(eeg: EEG=None, save_fn=None, +def present(eeg: Optional[EEG]=None, save_fn=None, stim_types=None, itis=None, additional_labels={}, secs=0.07, volume=0.8,tone1_hz =440, tone2_hz = 528, do_fixation=True): diff --git a/eegnb/experiments/visual_codeprose/codeprose.py b/eegnb/experiments/visual_codeprose/codeprose.py index e1ec3912..ec849268 100644 --- a/eegnb/experiments/visual_codeprose/codeprose.py +++ b/eegnb/experiments/visual_codeprose/codeprose.py @@ -9,7 +9,7 @@ # TODO: Switch to using time_ns when Python 3.7 is the minimum version for eegnb from time import time, strftime, gmtime -from typing import List +from typing import Optional, List from pathlib import Path from dataclasses import dataclass, field from ...devices.eeg import EEG @@ -167,7 +167,7 @@ def run(window: visual.Window) -> pd.DataFrame: return pd.DataFrame(responses) -def fixate(window: visual.Window, text: str = None): +def fixate(window: visual.Window, text: Optional[str] = None): visual.TextStim( win=window, text=text, diff --git a/eegnb/experiments/visual_n170/n170.py b/eegnb/experiments/visual_n170/n170.py index 0ff0dbed..0cd27e39 100644 --- a/eegnb/experiments/visual_n170/n170.py +++ b/eegnb/experiments/visual_n170/n170.py @@ -19,11 +19,12 @@ from eegnb.devices.eeg import EEG from eegnb.stimuli import FACE_HOUSE from eegnb.experiments import Experiment +from typing import Optional class VisualN170(Experiment.BaseExperiment): - def __init__(self, duration=120, eeg: EEG=None, save_fn=None, + def __init__(self, duration=120, eeg: Optional[EEG]=None, save_fn=None, n_trials = 2010, iti = 0.4, soa = 0.3, jitter = 0.2): # Set experiment name diff --git a/eegnb/experiments/visual_p300/p300.py b/eegnb/experiments/visual_p300/p300.py index 33f6f3e2..a22ee893 100644 --- a/eegnb/experiments/visual_p300/p300.py +++ b/eegnb/experiments/visual_p300/p300.py @@ -15,10 +15,11 @@ from eegnb.stimuli import CAT_DOG from eegnb.experiments import Experiment from eegnb.devices.eeg import EEG +from typing import Optional class VisualP300(Experiment.BaseExperiment): - def __init__(self, duration=120, eeg: EEG=None, save_fn=None, + def __init__(self, duration=120, eeg: Optional[EEG]=None, save_fn=None, n_trials = 2010, iti = 0.4, soa = 0.3, jitter = 0.2): exp_name = "Visual P300" diff --git a/eegnb/experiments/visual_ssvep/ssvep.py b/eegnb/experiments/visual_ssvep/ssvep.py index b30baffd..21719b92 100644 --- a/eegnb/experiments/visual_ssvep/ssvep.py +++ b/eegnb/experiments/visual_ssvep/ssvep.py @@ -12,11 +12,12 @@ from eegnb.devices.eeg import EEG from eegnb import generate_save_fn +from typing import Optional class VisualSSVEP(Experiment.BaseExperiment): - def __init__(self, duration=120, eeg: EEG=None, save_fn=None, n_trials = 2010, iti = 0.5, soa = 3.0, jitter = 0.2): + def __init__(self, duration=120, eeg: Optional[EEG]=None, save_fn=None, n_trials = 2010, iti = 0.5, soa = 3.0, jitter = 0.2): exp_name = "Visual SSVEP" super().__init__(exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter) diff --git a/eegnb/experiments/visual_vep/vep.py b/eegnb/experiments/visual_vep/vep.py index 1acd1b11..fd28844e 100644 --- a/eegnb/experiments/visual_vep/vep.py +++ b/eegnb/experiments/visual_vep/vep.py @@ -1,11 +1,12 @@ from time import time, strftime, gmtime from pylsl import StreamInfo, StreamOutlet from eegnb.experiments.Experiment import Experiment +from typing import Optional class VisualVEP(Experiment): - def __init__(self, duration=120, eeg: EEG=None, save_fn=None, + def __init__(self, duration=120, eeg: Optional[EEG]=None, save_fn=None, n_trials = 2000, iti = 0.2, soa = 0.2, jitter = 0.1): exp_name = "Visual VEP" From 584272e493d0e1d2d8d4f170defbc88f57d9a80d Mon Sep 17 00:00:00 2001 From: Ore Ogundipe Date: Sat, 4 Mar 2023 13:20:19 -0800 Subject: [PATCH 23/32] update dependencies - for build (#220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * major update: merging develop to master (#217) * example test commit (#182) * example test commit * example edit * ci: run test workflow on develop branch * ci: add develop branch to job triggers * ci: fix syntax issue in workflow * fix: fixed import (brainflow updated API) * build(deps): locked pylsl==1.10.5 (#187) * Experiment Class Refactor (update to #183), converting specific experiments to subclasses (#184) * First commit * Second commit * Modifications * Lol * Lol * Incorporated N170 and p300, looking good for a PR * ssvep update * Implementing subclasses instead of loose functions * fix: fixed import (brainflow updated API) * Playing around still * Fixing import errors * Adding abstractmethod decorators * Still working on the import error * Guess what's finally working * Comments and naming ticks * More comments * Live coding demonstration * ssvep adapted * Adapting Auditory Oddball * changing save_fn to self.save_fun * This maybe the last big change * utils file changed, changes work through cli Co-authored-by: Erik Bjäreholt * Submodule added for gsoc * Adding pipelines for cli analysis (#202) * started pipelines function * almost working simple function equivalents of nb scripts * fix: fixed import (brainflow updated API) * sqc fixes for unicorn (#176) * Ignore pushes * Trying to create a cli * Stepping through the problem * First commit * Fixing pause in signal quality check * Fixing Signal quality check problem * fix the technical debt * Save path done for automated saving pdf * I feel amazing * Almost through * Update eegnb/cli/__main__.py Co-authored-by: Erik Bjäreholt * Trying to create cli but it's being really painful * Extra word cli error * Changed example handling * Pain * Adding whole datapath * Finally fixed cli * hmm * Looking good * added hyperlink * Having some issues with detecting css and image deltetion * Just the css now * Fixed the css linking problem though it's a weird soln * Automated running, still fnames problem * Hahahah embedded images in html * Improving code * Okay now * Look at that * Almost there just the two figures now * Now * Added attrdict to do with cli error Co-authored-by: John Griffiths Co-authored-by: Erik Bjäreholt Co-authored-by: John Griffiths * added more options for site args; improved function names; removed some redundant lines (#209) * fix subject num parsing bug * analysis report function improvements for openbci cyton and gtec unicorn devices * run exp fix * Update requirements.txt * fixes to get docs building by github action (#210) * fixes to get docs building by github action * reverted some changes * Update 01r__ssvep_viz.py Co-authored-by: John Griffiths * Update README.rst small commit to test doc build workflow on this branch * removing gsoc submodule Co-authored-by: Erik Bjäreholt Co-authored-by: Parv Agarwal <65726543+Parvfect@users.noreply.github.com> Co-authored-by: Parvfect Co-authored-by: Ben Pettit * update dependencies - seaborn * docs/perf: reduced the imports: `cueing` example * bug: update deprecated `plot_psd()` method - feature of new `mne` version. - instead of doing plot_psd() from the `mne.io.Raw` object, must do this: - `raw.compute_psd().plot()` - i.e., has to pass through a `spectrum` object * updated deprec. `mne` function * perf: removed importage of unused packages from example - One of them, i.e., `collections.Iterable` is even deprecated. - Must use `collections.abc.Iterable` instead now. - Resulting in faster build/user run * bugfix: `plot_conditions` - due to `sns` deprecation * bugfix: resolved `psd_welch()` deprecation (`mne`) --------- Co-authored-by: John Griffiths Co-authored-by: Erik Bjäreholt Co-authored-by: Parv Agarwal <65726543+Parvfect@users.noreply.github.com> Co-authored-by: Parvfect Co-authored-by: Ben Pettit Co-authored-by: Taha Morshedzadeh --- eegnb/analysis/utils.py | 21 ++++++++----------- .../01r__cueing_singlesub_analysis.py | 10 ++++----- examples/visual_ssvep/01r__ssvep_viz.py | 12 ++++++++--- requirements.txt | 2 +- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/eegnb/analysis/utils.py b/eegnb/analysis/utils.py index 119500d8..4a0fa53d 100644 --- a/eegnb/analysis/utils.py +++ b/eegnb/analysis/utils.py @@ -4,10 +4,10 @@ import logging from collections import OrderedDict from glob import glob -from typing import Union, List, Dict -from collections import Iterable +from typing import Union, List#, Dict +# from collections import Iterable from time import sleep, time -from numpy.core.fromnumeric import std +# from numpy.core.fromnumeric import std import keyboard import os @@ -277,14 +277,16 @@ def plot_conditions( for ch in range(channel_count): for cond, color in zip(conditions.values(), palette): - sns.tsplot( - X[y.isin(cond), ch], - time=times, + sns.lineplot( + data=pd.DataFrame(X[y.isin(cond), ch].T, index=times), + x=times, + y=ch, color=color, n_boot=n_boot, - ci=ci, ax=axes[ch], + errorbar=('ci',ci) ) + axes[ch].set(xlabel='Time (s)', ylabel='Amplitude (uV)', title=epochs.ch_names[channel_order[ch]]) if diff_waveform: diff = np.nanmean(X[y == diff_waveform[1], ch], axis=0) - np.nanmean( @@ -298,11 +300,6 @@ def plot_conditions( x=0, ymin=ylim[0], ymax=ylim[1], color="k", lw=1, label="_nolegend_" ) - axes[0].set_xlabel("Time (s)") - axes[0].set_ylabel("Amplitude (uV)") - axes[-1].set_xlabel("Time (s)") - axes[1].set_ylabel("Amplitude (uV)") - if diff_waveform: legend = ["{} - {}".format(diff_waveform[1], diff_waveform[0])] + list( conditions.keys() diff --git a/examples/visual_cueing/01r__cueing_singlesub_analysis.py b/examples/visual_cueing/01r__cueing_singlesub_analysis.py index 9ba11022..8d367165 100644 --- a/examples/visual_cueing/01r__cueing_singlesub_analysis.py +++ b/examples/visual_cueing/01r__cueing_singlesub_analysis.py @@ -14,7 +14,7 @@ # # Some standard pythonic imports -import os,sys,glob,numpy as np,pandas as pd +import os,numpy as np#,sys,glob,pandas as pd from collections import OrderedDict import warnings warnings.filterwarnings('ignore') @@ -22,7 +22,7 @@ import matplotlib.patches as patches # MNE functions -from mne import Epochs,find_events, concatenate_raws +from mne import Epochs,find_events#, concatenate_raws from mne.time_frequency import tfr_morlet # EEG-Notebooks functions @@ -73,7 +73,7 @@ # One way to analyze the SSVEP is to plot the power spectral density, or PSD. SSVEPs should appear as peaks in power for certain frequencies. We expect clear peaks in the spectral domain at the stimulation frequencies of 30 and 20 Hz. # -raw.plot_psd(); +raw.compute_psd().plot(); # Should see the electrical noise at 60 Hz, and maybe a peak at the red and blue channels between 7-14 Hz (Alpha) @@ -84,8 +84,8 @@ # Most ERP components are composed of lower frequency fluctuations in the EEG signal. Thus, we can filter out all frequencies between 1 and 30 hz in order to increase our ability to detect them. # -raw.filter(1,30, method='iir') -raw.plot_psd(fmin=1, fmax=30); +raw.filter(1,30, method='iir'); +raw.compute_psd(fmin=1, fmax=30).plot(); ################################################################################################### # Epoching diff --git a/examples/visual_ssvep/01r__ssvep_viz.py b/examples/visual_ssvep/01r__ssvep_viz.py index d812681c..974e3e6a 100644 --- a/examples/visual_ssvep/01r__ssvep_viz.py +++ b/examples/visual_ssvep/01r__ssvep_viz.py @@ -26,7 +26,7 @@ # MNE functions from mne import Epochs,find_events -from mne.time_frequency import psd_welch,tfr_morlet +from mne.time_frequency import tfr_morlet # EEG-Notebooks functions from eegnb.analysis.utils import load_data,plot_conditions @@ -88,8 +88,14 @@ # Next, we can compare the PSD of epochs specifically during 20hz and 30hz stimulus presentation f, axs = plt.subplots(2, 1, figsize=(10, 10)) -psd1, freq1 = psd_welch(epochs['30 Hz'], n_fft=1028, n_per_seg=256 * 3, picks='all') -psd2, freq2 = psd_welch(epochs['20 Hz'], n_fft=1028, n_per_seg=256 * 3, picks='all') + +welch_params=dict(method='welch', + n_fft=1028, + n_per_seg=256 * 3, + picks='all') + +psd1, freq1 = epochs['30 Hz'].compute_psd(**welch_params).get_data(return_freqs=True) +psd2, freq2 = epochs['20 Hz'].compute_psd(**welch_params).get_data(return_freqs=True) psd1 = 10 * np.log10(psd1) psd2 = 10 * np.log10(psd2) diff --git a/requirements.txt b/requirements.txt index 568058b3..770bd491 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ scikit-learn>=0.23.2 pandas>=1.1.4 numpy>=1.19.4 mne>=0.20.8 -seaborn==0.9.0 +seaborn>=0.9.0 pyriemann>=0.2.7 jupyter muselsl>=2.0.2 From d0715cf13aa72e29858a7a369550bae91cf1e4af Mon Sep 17 00:00:00 2001 From: Ben Pettit Date: Sun, 5 Mar 2023 07:41:08 +1000 Subject: [PATCH 24/32] Updated psychopy (#215) * update psychopy for psychxr compatibility * updated n170 example to run again. --- eegnb/experiments/Experiment.py | 9 +++-- .../visual_n170/00x__n170_run_experiment.py | 36 +++++++++---------- requirements.txt | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/eegnb/experiments/Experiment.py b/eegnb/experiments/Experiment.py index 35da046a..c6e314dd 100644 --- a/eegnb/experiments/Experiment.py +++ b/eegnb/experiments/Experiment.py @@ -92,7 +92,8 @@ def setup(self, instructions=True): # Generating a random int for the filename random_id = random.randint(1000,10000) # Generating save function - self.save_fn = generate_save_fn(self.eeg.device_name, "visual_n170", random_id, random_id, "unnamed") + experiment_directory = self.name.replace(' ', '_') + self.save_fn = generate_save_fn(self.eeg.device_name, experiment_directory, random_id, random_id, "unnamed") print( f"No path for a save file was passed to the experiment. Saving data to {self.save_fn}" ) @@ -164,5 +165,7 @@ def run(self, instructions=True): # Closing the window self.window.close() - - \ No newline at end of file + @property + def name(self) -> str: + """ This experiment's name """ + return self.exp_name diff --git a/examples/visual_n170/00x__n170_run_experiment.py b/examples/visual_n170/00x__n170_run_experiment.py index 2258aa08..d0cf1f02 100644 --- a/examples/visual_n170/00x__n170_run_experiment.py +++ b/examples/visual_n170/00x__n170_run_experiment.py @@ -12,31 +12,31 @@ # --------------------- # # Imports -import os -from eegnb import generate_save_fn from eegnb.devices.eeg import EEG -from eegnb.experiments.visual_n170 import n170 +from eegnb.experiments import VisualN170 # Define some variables -board_name = "muse" -experiment = "visual_n170" + +# Experiment type +experiment = VisualN170() + +# EEG device +experiment.eeg = EEG(device="cyton") # "muse") + +# Test subject id subject_id = 0 -session_nb = 0 -record_duration = 120 -################################################################################################### -# Initiate EEG device -# --------------------- -# -# Start EEG device -eeg_device = EEG(device=board_name) +# Session number +session_nb = 0 -# Create save file name -save_fn = generate_save_fn(board_name, experiment, subject_id, session_nb) -print(save_fn) +# Experiment recording duration +experiment.duration = 120 ################################################################################################### # Run experiment # --------------------- -# -n170.present(duration=record_duration, eeg=eeg_device, save_fn=save_fn) +# +experiment.run() + +# Saved csv location +print(experiment.save_fn) diff --git a/requirements.txt b/requirements.txt index 770bd491..9d274be9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Main repo requirements -psychopy==2020.2.3 +psychopy==2022.2.4 psychtoolbox scikit-learn>=0.23.2 pandas>=1.1.4 From 53154c2f32fb286457d537f438bca4f62620e639 Mon Sep 17 00:00:00 2001 From: Parv Agarwal <65726543+Parvfect@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:41:00 +0000 Subject: [PATCH 25/32] Updated doc examples for N170, P300 and SSVEP after Experiment Class Refactor (#218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * major update: merging develop to master (#217) * example test commit (#182) * example test commit * example edit * ci: run test workflow on develop branch * ci: add develop branch to job triggers * ci: fix syntax issue in workflow * fix: fixed import (brainflow updated API) * build(deps): locked pylsl==1.10.5 (#187) * Experiment Class Refactor (update to #183), converting specific experiments to subclasses (#184) * First commit * Second commit * Modifications * Lol * Lol * Incorporated N170 and p300, looking good for a PR * ssvep update * Implementing subclasses instead of loose functions * fix: fixed import (brainflow updated API) * Playing around still * Fixing import errors * Adding abstractmethod decorators * Still working on the import error * Guess what's finally working * Comments and naming ticks * More comments * Live coding demonstration * ssvep adapted * Adapting Auditory Oddball * changing save_fn to self.save_fun * This maybe the last big change * utils file changed, changes work through cli Co-authored-by: Erik Bjäreholt * Submodule added for gsoc * Adding pipelines for cli analysis (#202) * started pipelines function * almost working simple function equivalents of nb scripts * fix: fixed import (brainflow updated API) * sqc fixes for unicorn (#176) * Ignore pushes * Trying to create a cli * Stepping through the problem * First commit * Fixing pause in signal quality check * Fixing Signal quality check problem * fix the technical debt * Save path done for automated saving pdf * I feel amazing * Almost through * Update eegnb/cli/__main__.py Co-authored-by: Erik Bjäreholt * Trying to create cli but it's being really painful * Extra word cli error * Changed example handling * Pain * Adding whole datapath * Finally fixed cli * hmm * Looking good * added hyperlink * Having some issues with detecting css and image deltetion * Just the css now * Fixed the css linking problem though it's a weird soln * Automated running, still fnames problem * Hahahah embedded images in html * Improving code * Okay now * Look at that * Almost there just the two figures now * Now * Added attrdict to do with cli error Co-authored-by: John Griffiths Co-authored-by: Erik Bjäreholt Co-authored-by: John Griffiths * added more options for site args; improved function names; removed some redundant lines (#209) * fix subject num parsing bug * analysis report function improvements for openbci cyton and gtec unicorn devices * run exp fix * Update requirements.txt * fixes to get docs building by github action (#210) * fixes to get docs building by github action * reverted some changes * Update 01r__ssvep_viz.py Co-authored-by: John Griffiths * Update README.rst small commit to test doc build workflow on this branch * removing gsoc submodule Co-authored-by: Erik Bjäreholt Co-authored-by: Parv Agarwal <65726543+Parvfect@users.noreply.github.com> Co-authored-by: Parvfect Co-authored-by: Ben Pettit * Updated doc examples * Update 00x__n170_run_experiment.py fix: typo in func param --------- Co-authored-by: John Griffiths Co-authored-by: Erik Bjäreholt Co-authored-by: Ben Pettit Co-authored-by: Ore O --- .../visual_n170/00x__n170_run_experiment.py | 26 +++++++++---------- .../visual_p300/00x__p300_run_experiment.py | 10 ++++--- .../visual_ssvep/00x__ssvep_run_experiment.py | 7 ++--- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/examples/visual_n170/00x__n170_run_experiment.py b/examples/visual_n170/00x__n170_run_experiment.py index d0cf1f02..dddaf6dc 100644 --- a/examples/visual_n170/00x__n170_run_experiment.py +++ b/examples/visual_n170/00x__n170_run_experiment.py @@ -12,25 +12,25 @@ # --------------------- # # Imports +from eegnb import generate_save_fn from eegnb.devices.eeg import EEG from eegnb.experiments import VisualN170 # Define some variables +board_name = "muse2" # board name +experiment_name = "visual_n170" # experiment name +subject_id = 0 # test subject id +session_nb = 0 # session number +record_duration = 120 # recording duration -# Experiment type -experiment = VisualN170() - -# EEG device -experiment.eeg = EEG(device="cyton") # "muse") +# generate save path +save_fn = generate_save_fn(board_name, experiment_name, subject_id, subject_nb) -# Test subject id -subject_id = 0 +# create device object +eeg_device = EEG(device=board_name) -# Session number -session_nb = 0 - -# Experiment recording duration -experiment.duration = 120 +# Experiment type +experiment = VisualN170(duration=record_duration, eeg=eeg_device, save_fn=save_fn) ################################################################################################### # Run experiment @@ -39,4 +39,4 @@ experiment.run() # Saved csv location -print(experiment.save_fn) +print("Recording saved in", experiment.save_fn) diff --git a/examples/visual_p300/00x__p300_run_experiment.py b/examples/visual_p300/00x__p300_run_experiment.py index 8edcdbfd..754f5340 100644 --- a/examples/visual_p300/00x__p300_run_experiment.py +++ b/examples/visual_p300/00x__p300_run_experiment.py @@ -15,10 +15,10 @@ import os from eegnb import generate_save_fn from eegnb.devices.eeg import EEG -from eegnb.experiments.visual_p300 import p300 +from eegnb.experiments import VisualP300 # Define some variables -board_name = "muse" +board_name = "muse2" experiment = "visual_p300" subject_id = 0 session_nb = 0 @@ -38,5 +38,7 @@ ################################################################################################### # Run experiment # --------------------- -# -p300.present(duration=record_duration, eeg=eeg_device, save_fn=save_fn) +# +# Create Experiment Object +p300 = VisualP300(duration=record_duration, eeg=eeg_device, save_fn=save_fn) +p300.run() diff --git a/examples/visual_ssvep/00x__ssvep_run_experiment.py b/examples/visual_ssvep/00x__ssvep_run_experiment.py index 1741760e..dfb8c046 100644 --- a/examples/visual_ssvep/00x__ssvep_run_experiment.py +++ b/examples/visual_ssvep/00x__ssvep_run_experiment.py @@ -15,10 +15,10 @@ import os from eegnb import generate_save_fn from eegnb.devices.eeg import EEG -from eegnb.experiments.visual_ssvep import ssvep +from eegnb.experiments import VisualSSVEP # Define some variables -board_name = "muse" +board_name = "muse2" experiment = "visual_ssvep" subject_id = 0 session_nb = 0 @@ -39,4 +39,5 @@ # Run experiment # --------------------- # -ssvep.present(duration=record_duration, eeg=eeg_device, save_fn=save_fn) +ssvep = VisualSSVEP(duration=record_duration, eeg=eeg_device, save_fn=save_fn) +ssvep.run() \ No newline at end of file From c24b7c75e9f95c1f61edbe7bd6f629eab9d1a758 Mon Sep 17 00:00:00 2001 From: Ben Pettit Date: Sun, 12 Mar 2023 06:42:26 +1000 Subject: [PATCH 26/32] fix error breaking n170 test (#223) --- examples/visual_n170/00x__n170_run_experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/visual_n170/00x__n170_run_experiment.py b/examples/visual_n170/00x__n170_run_experiment.py index dddaf6dc..51b6d2c6 100644 --- a/examples/visual_n170/00x__n170_run_experiment.py +++ b/examples/visual_n170/00x__n170_run_experiment.py @@ -24,7 +24,7 @@ record_duration = 120 # recording duration # generate save path -save_fn = generate_save_fn(board_name, experiment_name, subject_id, subject_nb) +save_fn = generate_save_fn(board_name, experiment_name, subject_id, session_nb) # create device object eeg_device = EEG(device=board_name) From 81dd2445368a9ced6dadee5bf5f704be221e2f90 Mon Sep 17 00:00:00 2001 From: Ben Pettit Date: Thu, 27 Apr 2023 15:59:43 +1000 Subject: [PATCH 27/32] fixed requirements.txt so 'pip install -e .' would work on windows. (#229) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ddd23eff..ec501766 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ pywinhook @ https://github.com/ActivityWatch/wheels/raw/master/pywinhook/pyWinho # pyglet downgrade to prevent threadmode warning on windows # See issue: https://github.com/psychopy/psychopy/issues/2876 -pyglet==1.4.10 ; platform_system == "Windows" +pyglet==1.4.11 ; platform_system == "Windows" # Test requirements mypy From 7be54d6b2b3fa3d3a800cdc580f58254c17c4363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 27 Apr 2023 17:02:20 +0200 Subject: [PATCH 28/32] Get CI test builds working again! (#170) * Revert "Revert PR #167: "ci: fix broken CI" (#169)" This reverts commit 2d748719b656160ea10fe94f29ff5c613e35f957. * Update Makefile * fix: Update vep.py import * Update Makefile * Update vep.py * fix: typo in makefile * fix: update BaseExperiment class reference * Update Makefile * Update vep.py * Update 01r__n170_viz.py * makefile: install libnotify4 --------- Co-authored-by: Ore O --- .github/workflows/docs.yml | 11 +- .github/workflows/test.yml | 57 ++---- Makefile | 39 ++++ doc/conf.py | 2 +- eegnb/analysis/utils.py | 12 +- eegnb/analysis/utils_old.py | 294 ---------------------------- eegnb/cli/__main__.py | 6 +- eegnb/datasets/datasets.py | 23 ++- eegnb/experiments/visual_vep/vep.py | 5 +- requirements.txt | 5 +- 10 files changed, 90 insertions(+), 364 deletions(-) create mode 100644 Makefile delete mode 100755 eegnb/analysis/utils_old.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index eb09e561..97eb7698 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,17 +17,18 @@ jobs: python-version: 3.8 - name: Install dependencies run: | + make install-deps-apt python -m pip install --upgrade pip wheel python -m pip install attrdict - # Install wxPython wheels since they are distribution-specific and therefore not on PyPI - # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython + make install-deps-wxpython + - name: Build project + run: | + make build - pip install . - name: Build docs run: | - cd doc && make html + make docs - name: Deploy Docs uses: peaceiris/actions-gh-pages@v3 if: github.ref == 'refs/heads/master' # TODO: Deploy seperate develop-version of docs? diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae7f9302..678c8b4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: jobs: test: - name: ${{ matrix.os }}, py-${{ matrix.python_version }} + name: test (${{ matrix.os }}, py-${{ matrix.python_version }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -44,29 +44,17 @@ jobs: - name: Install APT dependencies if: "startsWith(runner.os, 'Linux')" run: | - # update archive links - sudo apt-get update - # xvfb is a dependency to create a virtual display - # libgtk-3-dev is a requirement for wxPython - # freeglut3-dev is a requirement for a wxPython dependency - sudo apt-get -y install xvfb libgtk-3-dev freeglut3-dev + make install-deps-apt + - name: Upgrade pip + run: | + python -m pip install --upgrade pip wheel - name: Install Linux dependencies if: "startsWith(runner.os, 'Linux')" run: | - python -m pip install --upgrade pip wheel - - # Install wxPython wheels since they are distribution-specific and therefore not on PyPI - # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython - - pip install . - - - name: Install MacOS/Windows dependencies + make install-deps-wxpython + - name: Install dependencies run: | - python -m pip install --upgrade pip wheel - pip install . - pip install -U psychtoolbox # JG_ADD - # EB: needed? + make build - name: Run eegnb install test shell: bash run: | @@ -83,9 +71,10 @@ jobs: Xvfb :0 -screen 0 1024x768x24 -ac +extension GLX +render -noreset &> xvfb.log & export DISPLAY=:0 fi - pytest + make test typecheck: + name: typecheck (${{ matrix.os }}, py-${{ matrix.python_version }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -102,27 +91,17 @@ jobs: - name: Install APT dependencies if: "startsWith(runner.os, 'Linux')" run: | - # update archive links - sudo apt-get update - # xvfb is a dependency to create a virtual display - # libgtk-3-dev is a requirement for wxPython - # freeglut3-dev is a requirement for a wxPython dependency - sudo apt-get -y install xvfb libgtk-3-dev freeglut3-dev + make install-deps-apt + - name: Upgrade pip + run: | + python -m pip install --upgrade pip wheel - name: Install Linux dependencies if: "startsWith(runner.os, 'Linux')" run: | - python -m pip install --upgrade pip wheel - - # Install wxPython wheels since they are distribution-specific and therefore not on PyPI - # See: https://wxpython.org/pages/downloads/index.html - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython - - pip install . - - name: Install MacOS/Windows dependencies + make install-deps-wxpython + - name: Install dependencies run: | - python -m pip install --upgrade pip wheel - pip install . + make build - name: Typecheck run: | - # Exclude visual_cueing due to errors - python -m mypy --exclude 'examples/visual_cueing' + make typecheck diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..29e53347 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +build: + pip install . + +test: + pytest + +typecheck: + # Exclude visual_cueing due to errors + python -m mypy --exclude 'examples/visual_cueing' + +docs: + cd doc && make html + +clean: + cd doc && make clean + +install-deps-apt: + sudo apt-get update # update archive links + + # xvfb is a dependency to create a virtual display + # libgtk-3-dev is a requirement for wxPython + # freeglut3-dev is a requirement for a wxPython dependency + # portaudio19-dev *might* be required to import psychopy on Ubuntu + # pulseaudio *might* be required to actually run the tests (on PsychoPy import) + # libpulse-dev required to build pocketsphinx (speech recognition dependency of psychopy) + # libsdl2-dev required by psychopy + # libnotify4 is so we can have the libnotify.so module used in wxPython working + sudo apt-get -y install xvfb libgtk-3-dev freeglut3-dev portaudio19-dev libpulse-dev pulseaudio libsdl2-dev libnotify4 + + # configure dynamic links + sudo ldconfig + + UPDATED_LIBPATH=$(sudo find / -name libnotify.so) + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$UPDATED_LIBPATH + +install-deps-wxpython: + # Install wxPython wheels since they are distribution-specific and therefore not on PyPI + # See: https://wxpython.org/pages/downloads/index.html + pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython diff --git a/doc/conf.py b/doc/conf.py index 426a57b2..d4503b43 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -284,7 +284,7 @@ def setup(app): 'backreferences_dir': 'generated', # Where to drop linking files between examples & API 'doc_module': ('eeg-notebooks',), 'reference_url': {'eeg-notebooksS': None}, - 'remove_conffig_comments': True, + 'remove_config_comments': True, } """ diff --git a/eegnb/analysis/utils.py b/eegnb/analysis/utils.py index 4a0fa53d..67338ef0 100644 --- a/eegnb/analysis/utils.py +++ b/eegnb/analysis/utils.py @@ -2,6 +2,7 @@ from copy import deepcopy import math import logging +import sys from collections import OrderedDict from glob import glob from typing import Union, List#, Dict @@ -20,6 +21,7 @@ from mne.channels import make_standard_montage from mne.filter import create_filter from matplotlib import pyplot as plt +from scipy import stats from scipy.signal import lfilter, lfilter_zi from eegnb import _get_recording_dir @@ -221,7 +223,8 @@ def plot_conditions( ylim=(-6, 6), diff_waveform=(1, 2), channel_count=4, - channel_order=None): + channel_order=None, +): """Plot ERP conditions. Args: epochs (mne.epochs): EEG epochs @@ -247,10 +250,9 @@ def plot_conditions( """ if channel_order: - channel_order = np.array(channel_order) + channel_order = np.array(channel_order) else: - channel_order = np.array(range(channel_count)) - + channel_order = np.array(range(channel_count)) if isinstance(conditions, dict): conditions = OrderedDict(conditions) @@ -260,7 +262,7 @@ def plot_conditions( X = epochs.get_data() * 1e6 - X = X[:,channel_order] + X = X[:, channel_order] times = epochs.times y = pd.Series(epochs.events[:, -1]) diff --git a/eegnb/analysis/utils_old.py b/eegnb/analysis/utils_old.py deleted file mode 100755 index 16f01ffe..00000000 --- a/eegnb/analysis/utils_old.py +++ /dev/null @@ -1,294 +0,0 @@ -# -*- coding: utf-8 -*- - -from glob import glob -import os -from collections import OrderedDict - -from mne import create_info, concatenate_raws -from mne.io import RawArray -from mne.channels import make_standard_montage -import pandas as pd -import numpy as np -import seaborn as sns -from matplotlib import pyplot as plt - - -sns.set_context("talk") -sns.set_style("white") - - -def load_muse_csv_as_raw( - filename, - sfreq=256.0, - ch_ind=[0, 1, 2, 3], - stim_ind=5, - replace_ch_names=None, - verbose=1, -): - """Load CSV files into a Raw object. - - Args: - filename (str or list): path or paths to CSV files to load - - Keyword Args: - subject_nb (int or str): subject number. If 'all', load all - subjects. - session_nb (int or str): session number. If 'all', load all - sessions. - sfreq (float): EEG sampling frequency - ch_ind (list): indices of the EEG channels to keep - stim_ind (int): index of the stim channel - replace_ch_names (dict or None): dictionary containing a mapping to - rename channels. Useful when an external electrode was used. - - Returns: - (mne.io.array.array.RawArray): loaded EEG - """ - n_channel = len(ch_ind) - - raw = [] - print(filename) - for fname in filename: - print(fname) - # read the file - data = pd.read_csv(fname, index_col=0) - - # name of each channels - ch_names = list(data.columns)[0:n_channel] + ["Stim"] - - if replace_ch_names is not None: - ch_names = [ - c if c not in replace_ch_names.keys() else replace_ch_names[c] - for c in ch_names - ] - - # type of each channels - ch_types = ["eeg"] * n_channel + ["stim"] - montage = make_standard_montage("standard_1005") - - # get data and exclude Aux channel - data = data.values[:, ch_ind + [stim_ind]].T - - # convert in Volts (from uVolts) - data[:-1] *= 1e-6 - - # create MNE object - info = create_info( - ch_names=ch_names, - ch_types=ch_types, - sfreq=sfreq, - montage=montage, - verbose=verbose, - ) - raw.append(RawArray(data=data, info=info, verbose=verbose)) - - # concatenate all raw objects - print("raw is") - print(raw) - raws = concatenate_raws(raw, verbose=verbose) - - return raws - - -def load_data( - data_dir, - site="eegnb_examples", - experiment="visual_n170", - device="muse2016", - subject_nb=1, - session_nb=1, - sfreq=256.0, - ch_ind=[0, 1, 2, 3], - stim_ind=5, - replace_ch_names=None, - verbose=1, -): - - """Load CSV files from the /data directory into a Raw object. - - Args: - data_dir (str): directory inside /data that contains the - CSV files to load, e.g., 'auditory/P300' - - Keyword Args: - subject_nb (int or str): subject number. If 'all', load all - subjects. - session_nb (int or str): session number. If 'all', load all - sessions. - sfreq (float): EEG sampling frequency - ch_ind (list): indices of the EEG channels to keep - stim_ind (int): index of the stim channel - replace_ch_names (dict or None): dictionary containing a mapping to - rename channels. Useful when an external electrode was used. - - Returns: - (mne.io.array.array.RawArray): loaded EEG - """ - if subject_nb == "all": - subject_nb = "*" - if session_nb == "all": - session_nb = "*" - - subject_nb_str = "%04.f" % subject_nb - session_nb_str = "%03.f" % session_nb - subsess = "subject{}/session{}/*.csv".format(subject_nb_str, session_nb_str) - data_path = os.path.join(data_dir, experiment, site, device, subsess) - fnames = glob(data_path) - - return load_muse_csv_as_raw( - fnames, - sfreq=sfreq, - ch_ind=ch_ind, - stim_ind=stim_ind, - replace_ch_names=replace_ch_names, - verbose=verbose, - ) - - -def plot_conditions( - epochs, - conditions=OrderedDict(), - ci=97.5, - n_boot=1000, - title="", - palette=None, - ylim=(-6, 6), - diff_waveform=(1, 2), -): - """Plot ERP conditions. - - Args: - epochs (mne.epochs): EEG epochs - - Keyword Args: - conditions (OrderedDict): dictionary that contains the names of the - conditions to plot as keys, and the list of corresponding marker - numbers as value. E.g., - - conditions = {'Non-target': [0, 1], - 'Target': [2, 3, 4]} - - ci (float): confidence interval in range [0, 100] - n_boot (int): number of bootstrap samples - title (str): title of the figure - palette (list): color palette to use for conditions - ylim (tuple): (ymin, ymax) - diff_waveform (tuple or None): tuple of ints indicating which - conditions to subtract for producing the difference waveform. - If None, do not plot a difference waveform - - Returns: - (matplotlib.figure.Figure): figure object - (list of matplotlib.axes._subplots.AxesSubplot): list of axes - """ - if isinstance(conditions, dict): - conditions = OrderedDict(conditions) - - if palette is None: - palette = sns.color_palette("hls", len(conditions) + 1) - - X = epochs.get_data() * 1e6 - times = epochs.times - y = pd.Series(epochs.events[:, -1]) - - fig, axes = plt.subplots(2, 2, figsize=[12, 6], sharex=True, sharey=True) - axes = [axes[1, 0], axes[0, 0], axes[0, 1], axes[1, 1]] - - for ch in range(4): - for cond, color in zip(conditions.values(), palette): - sns.tsplot( - X[y.isin(cond), ch], - time=times, - color=color, - n_boot=n_boot, - ci=ci, - ax=axes[ch], - ) - - if diff_waveform: - diff = np.nanmean(X[y == diff_waveform[1], ch], axis=0) - np.nanmean( - X[y == diff_waveform[0], ch], axis=0 - ) - axes[ch].plot(times, diff, color="k", lw=1) - - axes[ch].set_title(epochs.ch_names[ch]) - axes[ch].set_ylim(ylim) - axes[ch].axvline( - x=0, ymin=ylim[0], ymax=ylim[1], color="k", lw=1, label="_nolegend_" - ) - - axes[0].set_xlabel("Time (s)") - axes[0].set_ylabel("Amplitude (uV)") - axes[-1].set_xlabel("Time (s)") - axes[1].set_ylabel("Amplitude (uV)") - - if diff_waveform: - legend = ["{} - {}".format(diff_waveform[1], diff_waveform[0])] + list( - conditions.keys() - ) - else: - legend = conditions.keys() - axes[-1].legend(legend, loc="lower right") - sns.despine() - plt.tight_layout() - - if title: - fig.suptitle(title, fontsize=20) - - return fig, axes - - -def plot_highlight_regions( - x, y, hue, hue_thresh=0, xlabel="", ylabel="", legend_str=() -): - """Plot a line with highlighted regions based on additional value. - - Plot a line and highlight ranges of x for which an additional value - is lower than a threshold. For example, the additional value might be - pvalues, and the threshold might be 0.05. - - Args: - x (array_like): x coordinates - y (array_like): y values of same shape as `x` - - Keyword Args: - hue (array_like): values to be plotted as hue based on `hue_thresh`. - Must be of the same shape as `x` and `y`. - hue_thresh (float): threshold to be applied to `hue`. Regions for which - `hue` is lower than `hue_thresh` will be highlighted. - xlabel (str): x-axis label - ylabel (str): y-axis label - legend_str (tuple): legend for the line and the highlighted regions - - Returns: - (matplotlib.figure.Figure): figure object - (list of matplotlib.axes._subplots.AxesSubplot): list of axes - """ - fig, axes = plt.subplots(1, 1, figsize=(10, 5), sharey=True) - - axes.plot(x, y, lw=2, c="k") - plt.xlabel(xlabel) - plt.ylabel(ylabel) - - kk = 0 - a = [] - while kk < len(hue): - if hue[kk] < hue_thresh: - b = kk - kk += 1 - while kk < len(hue): - if hue[kk] > hue_thresh: - break - else: - kk += 1 - a.append([b, kk - 1]) - else: - kk += 1 - - st = (x[1] - x[0]) / 2.0 - for p in a: - axes.axvspan(x[p[0]] - st, x[p[1]] + st, facecolor="g", alpha=0.5) - plt.legend(legend_str) - sns.despine() - - return fig, axes diff --git a/eegnb/cli/__main__.py b/eegnb/cli/__main__.py index 9a196e8e..bdd43bb8 100644 --- a/eegnb/cli/__main__.py +++ b/eegnb/cli/__main__.py @@ -1,9 +1,7 @@ -from eegnb import DATA_DIR import click -from time import sleep -from os import path import os -import shutil + +from eegnb import DATA_DIR from eegnb.datasets.datasets import zip_data_folders from .introprompt import intro_prompt, analysis_intro_prompt diff --git a/eegnb/datasets/datasets.py b/eegnb/datasets/datasets.py index 3fc7ae87..21add031 100644 --- a/eegnb/datasets/datasets.py +++ b/eegnb/datasets/datasets.py @@ -1,11 +1,17 @@ -import os,sys,glob,shutil,numpy as np, pandas as pd -import requests, zipfile,gdown +import os +import glob +import shutil +import zipfile + +import requests +import gdown + from datetime import datetime from eegnb import DATA_DIR # eegnb example data sites. do not select these when zipping recordings -eegnb_sites = ['eegnb_examples', 'grifflab_dev', 'jadinlab_home'] +eegnb_sites = ["eegnb_examples", "grifflab_dev", "jadinlab_home"] def fetch_dataset( @@ -64,7 +70,7 @@ def fetch_dataset( } # If no non-default top-level data path specified, use default - if data_dir == None: + if data_dir is None: data_dir = DATA_DIR # check parameter entries @@ -85,12 +91,9 @@ def fetch_dataset( destination = os.path.join(data_dir, "downloaded_data.zip") if download_method == "gdown": - URL = "https://drive.google.com/uc?id=" + gdrive_locs[experiment] gdown.download(URL, destination, quiet=False) - elif download_method == "requests": - URL = "https://docs.google.com/uc?export=download" session = requests.Session() @@ -114,6 +117,8 @@ def fetch_dataset( for chunk in response.iter_content(CHUNK_SIZE): if chunk: f.write(chunk) + else: + raise ValueError("download_method not supported") # unzip the file with zipfile.ZipFile(destination, "r") as zip_ref: @@ -160,9 +165,7 @@ def fetch_dataset( return fnames - -def zip_data_folders(experiment: str, - site: str="local"): +def zip_data_folders(experiment: str, site: str = "local"): """ Run data zipping diff --git a/eegnb/experiments/visual_vep/vep.py b/eegnb/experiments/visual_vep/vep.py index fd28844e..9cfcef96 100644 --- a/eegnb/experiments/visual_vep/vep.py +++ b/eegnb/experiments/visual_vep/vep.py @@ -1,10 +1,11 @@ from time import time, strftime, gmtime from pylsl import StreamInfo, StreamOutlet -from eegnb.experiments.Experiment import Experiment from typing import Optional +from eegnb.experiments import Experiment +from eegnb.devices.eeg import EEG -class VisualVEP(Experiment): +class VisualVEP(Experiment.BaseExperiment): def __init__(self, duration=120, eeg: Optional[EEG]=None, save_fn=None, n_trials = 2000, iti = 0.2, soa = 0.2, jitter = 0.1): diff --git a/requirements.txt b/requirements.txt index ec501766..145d58ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ scikit-learn>=0.23.2 pandas>=1.1.4 numpy>=1.19.4,<1.24 # due to outdated libs not changing the names after: https://github.com/numpy/numpy/pull/22607 mne>=0.20.8 -seaborn>=0.9.0 +seaborn>=0.11.0 pyriemann>=0.2.7 jupyter muselsl>=2.0.2 @@ -28,9 +28,6 @@ attrdict3 wxPython>=4.0 ; platform_system == "Linux" click -# can be removed once minimum version is Python 3.7 -dataclasses; python_version == '3.6' - # pywinhook needs some special treatment since there are only wheels on PyPI for Python 3.7-3.8, and building requires special tools (swig, VS C++ tools) # See issue: https://github.com/NeuroTechX/eeg-notebooks/issues/29 pywinhook>=1.6.0 ; platform_system == "Windows" and (python_version == "3.7" or python_version == "3.8") From a4bec9b4eb129169e9114ff451d4bea1361b276d Mon Sep 17 00:00:00 2001 From: Ben Pettit Date: Fri, 17 Nov 2023 02:04:21 +1000 Subject: [PATCH 29/32] fix macos build (#245) --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 29e53347..6e1c179f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ build: - pip install . + # Use pep517 to install pygatt==4.0.5(deprecated setuptools/egg installer) on macos + pip install --use-pep517 . test: pytest From d8b597626d4622694ff14b86e86a63d23ec87683 Mon Sep 17 00:00:00 2001 From: Taha Morshedzadeh Date: Wed, 6 Dec 2023 21:10:48 +0000 Subject: [PATCH 30/32] Add devcontainer configuration for CPU environment --- .devcontainer/cpu/devcontainer.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .devcontainer/cpu/devcontainer.json diff --git a/.devcontainer/cpu/devcontainer.json b/.devcontainer/cpu/devcontainer.json new file mode 100644 index 00000000..2cbd93b6 --- /dev/null +++ b/.devcontainer/cpu/devcontainer.json @@ -0,0 +1,26 @@ +{ + "name": "EEG-ExPy-CPU", + "image": "mcr.microsoft.com/devcontainers/python:3.7", + + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ], + "settings": { + "python.pythonPath": "/usr/local/bin/python" + } + } + }, + + "forwardPorts": [ + 8000, + 8888, + 5000, + 6000 + ], + // print the python version: + "postCreateCommand": "python --version && pip install -r requirements.txt && pip install -e . && echo 'Dependencies installed'", + "appPort": 8000 + + } \ No newline at end of file From 7cf777489ef263ea742e0bfff658d008eb39d6c7 Mon Sep 17 00:00:00 2001 From: Taha Morshedzadeh Date: Fri, 8 Dec 2023 19:42:08 +0000 Subject: [PATCH 31/32] Fixed the python version needed --- .devcontainer/cpu/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/cpu/devcontainer.json b/.devcontainer/cpu/devcontainer.json index 2cbd93b6..3bdaa714 100644 --- a/.devcontainer/cpu/devcontainer.json +++ b/.devcontainer/cpu/devcontainer.json @@ -1,6 +1,6 @@ { "name": "EEG-ExPy-CPU", - "image": "mcr.microsoft.com/devcontainers/python:3.7", + "image": "mcr.microsoft.com/devcontainers/python:3.8", "customizations": { "vscode": { From d00c2dd6364d8b1aa4e109710cad27c94bea502b Mon Sep 17 00:00:00 2001 From: John Griffiths Date: Thu, 7 Mar 2024 03:14:43 -0500 Subject: [PATCH 32/32] Fix plot conditions (#257) * fixed plot_conditions functoni issues * small change to viz n170 plotting example * small change to viz p300 plotting example * fixed to plotting issue * modify plot command * update example files * fix condition label bug * fix: set layout engine to fix colorbar error --------- Co-authored-by: Ore O --- eegnb/analysis/utils.py | 56 ++++++++++--------- .../01r__cueing_singlesub_analysis.py | 16 ++++-- .../04r__cueing_group_analysis_winter2019.py | 2 +- examples/visual_n170/01r__n170_viz.py | 19 ++++--- examples/visual_p300/01r__p300_viz.py | 23 +++++--- examples/visual_ssvep/01r__ssvep_viz.py | 2 + 6 files changed, 70 insertions(+), 48 deletions(-) diff --git a/eegnb/analysis/utils.py b/eegnb/analysis/utils.py index eea11a97..21269a83 100644 --- a/eegnb/analysis/utils.py +++ b/eegnb/analysis/utils.py @@ -19,6 +19,7 @@ from mne.channels import make_standard_montage from mne.filter import create_filter from matplotlib import pyplot as plt +from matplotlib import lines as mlines from scipy import stats from scipy.signal import lfilter, lfilter_zi @@ -251,6 +252,7 @@ def plot_conditions( channel_order = np.array(channel_order) else: channel_order = np.array(range(channel_count)) + channel_names = np.array(epochs.ch_names)[channel_order] if isinstance(conditions, dict): conditions = OrderedDict(conditions) @@ -258,9 +260,8 @@ def plot_conditions( if palette is None: palette = sns.color_palette("hls", len(conditions) + 1) - X = epochs.get_data() * 1e6 - - X = X[:, channel_order] + dfX = epochs.to_data_frame() + dfX[channel_names] *= 1e6 times = epochs.times y = pd.Series(epochs.events[:, -1]) @@ -275,12 +276,13 @@ def plot_conditions( plot_axes.append(axes[axis_x, axis_y]) axes = plot_axes - for ch in range(channel_count): - for cond, color in zip(conditions.values(), palette): + for ch,ch_name in enumerate(channel_names): + for cond,cond_name, color in zip(conditions.values(),conditions.keys(), palette): + dfXc = dfX[dfX.condition.isin(conditions[cond_name])] sns.lineplot( - data=pd.DataFrame(X[y.isin(cond), ch].T, index=times), - x=times, - y=ch, + data=dfXc, + x="time", + y=ch_name, color=color, n_boot=n_boot, ax=axes[ch], @@ -289,26 +291,30 @@ def plot_conditions( axes[ch].set(xlabel='Time (s)', ylabel='Amplitude (uV)', title=epochs.ch_names[channel_order[ch]]) if diff_waveform: - diff = np.nanmean(X[y == diff_waveform[1], ch], axis=0) - np.nanmean( - X[y == diff_waveform[0], ch], axis=0 - ) + dfXc1 = dfX[dfX.condition.isin(conditions[diff_waveform[1]])] + dfXc2 = dfX[dfX.condition.isin(conditions[diff_waveform[0]])] + dfXc1_mn = dfXc1.set_index(['time', 'epoch'])[ch_name].unstack('epoch').mean(axis=1) + dfXc2_mn = dfXc2.set_index(['time', 'epoch'])[ch_name].unstack('epoch').mean(axis=1) + diff = (dfXc1_mn - dfXc2_mn).values axes[ch].plot(times, diff, color="k", lw=1) - axes[ch].set_title(epochs.ch_names[channel_order[ch]]) + axes[ch].set_title(ch_name) axes[ch].set_ylim(ylim) axes[ch].axvline( x=0, ymin=ylim[0], ymax=ylim[1], color="k", lw=1, label="_nolegend_" ) + legs = [] + for cond,cond_name,color in zip(conditions.values(),conditions.keys(), palette): + lh = mlines.Line2D([], [], color=color, marker='', ls='-', label=cond_name) + legs.append(lh) if diff_waveform: - legend = ["{} - {}".format(diff_waveform[1], diff_waveform[0])] + list( - conditions.keys() - ) - else: - legend = conditions.keys() - axes[-1].legend( - legend, bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0 - ) + lh = mlines.Line2D([], [], color="k", marker='', ls='-', + label = "{} - {}".format(diff_waveform[1], diff_waveform[0])) + legs.append(lh) + + axes[-1].legend(handles=legs, + bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0) sns.despine() plt.tight_layout() @@ -328,6 +334,9 @@ def plot_highlight_regions( Args: x (array_like): x coordinates y (array_like): y values of same shape as `x` + Keyword Args: + hue (array_like): values to be plotted as hue based on `hue_thresh`. + Must be of the same shape as `x` and `y`. Keyword Args: hue (array_like): values to be plotted as hue based on `hue_thresh`. Must be of the same shape as `x` and `y`. @@ -453,12 +462,9 @@ def check_report(eeg: EEG, n_times: int=60, pause_time=5, thres_std_low=None, th Usage: ------ from eegnb.devices.eeg import EEG - from eegnb.analysis.utils import check_report - eeg = EEG(device='museS') - check_report(eeg) + standard deviation for a quality recording. - The thres_std_low & thres_std_high values are the - lower and upper bound of accepted + thresholds = { standard deviation for a quality recording. thresholds = { diff --git a/examples/visual_cueing/01r__cueing_singlesub_analysis.py b/examples/visual_cueing/01r__cueing_singlesub_analysis.py index 8d367165..b3b4cf69 100644 --- a/examples/visual_cueing/01r__cueing_singlesub_analysis.py +++ b/examples/visual_cueing/01r__cueing_singlesub_analysis.py @@ -109,12 +109,15 @@ print('sample drop %: ', (1 - len(epochs.events)/len(events)) * 100) conditions = OrderedDict() -conditions['LeftCue'] = [1] -conditions['RightCue'] = [2] +#conditions['LeftCue'] = [1] +#conditions['RightCue'] = [2] +conditions['LeftCue'] = ['LeftCue'] +conditions['RightCue'] = ['RightCue'] +diffwave = ('LeftCue', 'RightCue') fig, ax = plot_conditions(epochs, conditions=conditions, ci=97.5, n_boot=1000, title='', - diff_waveform=(1, 2), ylim=(-20,20)) + diff_waveform=diffwave, ylim=(-20,20)) ################################################################################################### # Spectrogram @@ -242,10 +245,11 @@ print('sample drop %: ', (1 - len(epochs.events)/len(events)) * 100) conditions = OrderedDict() -conditions['ValidTarget'] = [21,22] -conditions['InvalidTarget'] = [11,12] +conditions['ValidTarget'] = ['ValidTarget_Left', 'ValidTarget_Right'] +conditions['InvalidTarget'] = ['InvalidTarget_Left', 'InvalidTarget_Right'] +diffwave = ('ValidTarget', 'InvalidTarget') fig, ax = plot_conditions(epochs, conditions=conditions, ci=97.5, n_boot=1000, title='', - diff_waveform=(1, 2), ylim=(-20,20)) + diff_waveform=diffwave, ylim=(-20,20)) diff --git a/examples/visual_cueing/04r__cueing_group_analysis_winter2019.py b/examples/visual_cueing/04r__cueing_group_analysis_winter2019.py index 50c382c8..8e0c1e93 100644 --- a/examples/visual_cueing/04r__cueing_group_analysis_winter2019.py +++ b/examples/visual_cueing/04r__cueing_group_analysis_winter2019.py @@ -22,7 +22,7 @@ from mne.time_frequency import tfr_morlet # EEG-Noteooks functions -from eegnb.analysis.utils import load_data,plot_conditions +from eegnb.analysis.utils import load_data from eegnb.datasets import fetch_dataset # sphinx_gallery_thumbnail_number = 1 diff --git a/examples/visual_n170/01r__n170_viz.py b/examples/visual_n170/01r__n170_viz.py index 9aa48f7d..ca4a3c02 100644 --- a/examples/visual_n170/01r__n170_viz.py +++ b/examples/visual_n170/01r__n170_viz.py @@ -24,6 +24,7 @@ # Some standard pythonic imports import os +from matplotlib import pyplot as plt from collections import OrderedDict import warnings warnings.filterwarnings('ignore') @@ -96,16 +97,20 @@ # ---------------------------- conditions = OrderedDict() -conditions['House'] = [1] -conditions['Face'] = [2] +#conditions['House'] = [1] +#conditions['Face'] = [2] +conditions['House'] = ['House'] +conditions['Face'] = ['Face'] +diffwav = ('Face', 'House') fig, ax = plot_conditions(epochs, conditions=conditions, ci=97.5, n_boot=1000, title='', - diff_waveform=None, #(1, 2)) - channel_order=[1,0,2,3]) # reordering of epochs.ch_names according to [[0,2],[1,3]] of subplot axes + diff_waveform=diffwav, + channel_order=[1,0,2,3]) +# reordering of epochs.ch_names according to [[0,2],[1,3]] of subplot axes # Manually adjust the ylims -for i in [0,2]: ax[i].set_ylim([-0.5,0.5]) -for i in [1,3]: ax[i].set_ylim([-1.5,2.5]) - +for i in [0,2]: ax[i].set_ylim([-0.5e6,0.5e6]) +for i in [1,3]: ax[i].set_ylim([-1.5e6,2.5e6]) +plt.tight_layout() diff --git a/examples/visual_p300/01r__p300_viz.py b/examples/visual_p300/01r__p300_viz.py index 67d2bc5e..2b0f3ac8 100644 --- a/examples/visual_p300/01r__p300_viz.py +++ b/examples/visual_p300/01r__p300_viz.py @@ -21,6 +21,7 @@ # Some standard pythonic imports import os +from matplotlib import pyplot as plt from collections import OrderedDict import warnings warnings.filterwarnings('ignore') @@ -80,26 +81,30 @@ # Create an array containing the timestamps and type of each stimulus (i.e. face or house) events = find_events(raw) -event_id = {'Non-Target': 1, 'Target': 2} +event_id = {'non-target': 1, 'target': 2} epochs = Epochs(raw, events=events, event_id=event_id, - tmin=-0.1, tmax=0.8, baseline=None, - reject={'eeg': 100e-6}, preload=True, + tmin=-0.1, tmax=0.8, baseline=None, reject={'eeg': 100e-6}, preload=True, verbose=False, picks=[0,1,2,3]) print('sample drop %: ', (1 - len(epochs.events)/len(events)) * 100) -epochs - - ################################################################################################### # Epoch average # ---------------------------- conditions = OrderedDict() -conditions['Non-target'] = [1] -conditions['Target'] = [2] +conditions['non-target'] = ['non-target'] +conditions['target'] = ['target'] +diffwav = ["non-target", "target"] fig, ax = plot_conditions(epochs, conditions=conditions, ci=97.5, n_boot=1000, title='', - diff_waveform=(1, 2)) + channel_order=[1,0,2,3],ylim=[-2E6,2.5E6], + diff_waveform = diffwav) + +# Manually adjust the ylims +for i in [0,2]: ax[i].set_ylim([-0.5e6,0.5e6]) +for i in [1,3]: ax[i].set_ylim([-1.5e6,2.5e6]) + +plt.tight_layout() diff --git a/examples/visual_ssvep/01r__ssvep_viz.py b/examples/visual_ssvep/01r__ssvep_viz.py index 974e3e6a..22da3b6a 100644 --- a/examples/visual_ssvep/01r__ssvep_viz.py +++ b/examples/visual_ssvep/01r__ssvep_viz.py @@ -146,6 +146,8 @@ tfr.plot(picks=[4], baseline=(-0.5, -0.1), mode='logratio', title='POz - 20 Hz stim'); +# Set Layout engine to tight to fix error with using colorbar layout error +plt.figure().set_layout_engine('tight'); plt.tight_layout() # Once again we can see clear SSVEPs at 30hz and 20hz