Skip to content

Commit 68d37d6

Browse files
Merge branch 'develop' into add-weibo2014-test
2 parents 7eaf09b + 297b1c4 commit 68d37d6

File tree

13 files changed

+485
-223
lines changed

13 files changed

+485
-223
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
- name: Run tests
5151
run: |
5252
echo "Running tests"
53-
pytest -vv -s --tb=long --durations=0 --log-cli-level=INFO --cov=moabb --cov-report=xml moabb/tests --verbose -n auto
53+
pytest -vv -s --tb=long --durations=0 --log-cli-level=INFO --cov=moabb --cov-report=xml moabb/tests --verbose
5454
5555
- name: Run pipelines
5656
run: |

docs/source/whats_new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Develop branch - 1.4 (dev)
1919
Enhancements
2020
~~~~~~~~~~~~
2121

22+
- Add new dataset :class:`moabb.datasets.BNCI2003_IVa` dataset (:gh:`811` by `Griffin Keeler`_)
23+
2224
Bugs
2325
~~~~
2426

@@ -42,6 +44,7 @@ Enhancements
4244
- Adding :func:`moabb.analysis.plotting.dataset_bubble_plot` plus the corresponding tutorial (:gh:`753` by `Pierre Guetschel`_)
4345
- Adding :func:`moabb.datasets.utils.plot_all_datasets` and update the tutorial (:gh:`758` by `Pierre Guetschel`_)
4446
- Improve the dataset model cards in each API page (:gh:`765` by `Pierre Guetschel`_)
47+
- Refactor :class:`moabb.evaluation.CrossSessionEvaluation`, :class:`moabb.evaluation.CrossSubjectEvaluation` and :class:`moabb.evaluation.WithinSessionEvaluation` to use the new splitter classes (:gh:`769` by `Bruno Aristimunha`_)
4548
- Adding tutorial on using mne-features (:gh:`762` by `Alexander de Ranitz`_, `Luuk Neervens`_, `Charlynn van Osch`_ and `Bruno Aristimunha`_)
4649
- Creating tutorial to expose the pre-processing steps (:gh:`771` by `Bruno Aristimunha`_)
4750
- Add function to auto-generate tables for the paper results documentation page (:gh:`785` by `Lucas Heck`_)
@@ -593,3 +596,4 @@ API changes
593596
.. _Radovan Vodila: https://github.com/rvodila
594597
.. _Ulysse Durand: https://github.com/UlysseDurand
595598
.. _Lucas Heck: https://github.com/lucas-heck
599+
.. _Griffin Keeler: https://github.com/griffinkeeler

examples/advanced_examples/plot_grid_search_withinsession.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"""
1010

1111
import os
12-
from pickle import load
1312

1413
import matplotlib.pyplot as plt
1514
import seaborn as sns
@@ -132,44 +131,3 @@
132131
)
133132
sns.pointplot(data=result, y="score", x="pipeline", ax=axes, palette="Set1")
134133
axes.set_ylabel("ROC AUC")
135-
136-
##########################################################
137-
# Load Best Model Parameter
138-
# -------------------------
139-
# The best model are automatically saved in a pickle file, in the
140-
# results directory. It is possible to load those model for each
141-
# dataset, subject and session. Here, we could see that the grid
142-
# search found a l1_ratio that is different from the baseline
143-
# value.
144-
145-
with open(
146-
"./Results/Models_WithinSession/BNCI2014-001/1/1test/GridSearchEN/fitted_model_best.pkl",
147-
"rb",
148-
) as pickle_file:
149-
GridSearchEN_Session_E = load(pickle_file)
150-
151-
print(
152-
"Best Parameter l1_ratio Session_E GridSearchEN ",
153-
GridSearchEN_Session_E.best_params_["LogistReg__l1_ratio"],
154-
)
155-
156-
print(
157-
"Best Parameter l1_ratio Session_E VanillaEN: ",
158-
pipelines["VanillaEN"].steps[2][1].l1_ratio,
159-
)
160-
161-
with open(
162-
"./Results/Models_WithinSession/BNCI2014-001/1/0train/GridSearchEN/fitted_model_best.pkl",
163-
"rb",
164-
) as pickle_file:
165-
GridSearchEN_Session_T = load(pickle_file)
166-
167-
print(
168-
"Best Parameter l1_ratio Session_T GridSearchEN ",
169-
GridSearchEN_Session_T.best_params_["LogistReg__l1_ratio"],
170-
)
171-
172-
print(
173-
"Best Parameter l1_ratio Session_T VanillaEN: ",
174-
pipelines["VanillaEN"].steps[2][1].l1_ratio,
175-
)

moabb/datasets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .bnci import BNCI2015003 # noqa: F401
2626
from .bnci import BNCI2015004 # noqa: F401
2727
from .bnci import (
28+
BNCI2003_004,
2829
BNCI2014_001,
2930
BNCI2014_002,
3031
BNCI2014_004,

moabb/datasets/bnci.py

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""BNCI 2014-001 Motor imagery dataset."""
22

3+
import io
4+
import zipfile
5+
from pathlib import Path
6+
37
import numpy as np
4-
from mne import create_info
8+
from mne import Annotations, create_info
59
from mne.channels import make_standard_montage
610
from mne.io import RawArray
711
from mne.utils import verbose
@@ -73,6 +77,7 @@ def load_data(
7377
dictionary containing events and their code.
7478
"""
7579
dataset_list = {
80+
"BNCI2003-004": _load_data_iva_2003,
7681
"BNCI2014-001": _load_data_001_2014,
7782
"BNCI2014-002": _load_data_002_2014,
7883
"BNCI2014-004": _load_data_004_2014,
@@ -88,6 +93,7 @@ def load_data(
8893
}
8994

9095
baseurl_list = {
96+
"BNCI2003-004": "https://www.bbci.de/competition/",
9197
"BNCI2014-001": BNCI_URL,
9298
"BNCI2014-002": BNCI_URL,
9399
"BNCI2015-001": BNCI_URL,
@@ -119,6 +125,57 @@ def load_data(
119125
)
120126

121127

128+
@verbose
129+
def _load_data_iva_2003(
130+
subject,
131+
path=None,
132+
force_update=False,
133+
update_path=None,
134+
base_url=None,
135+
only_filenames=False,
136+
verbose=None,
137+
):
138+
"""Loads data for the BNCI2003-IVa dataset."""
139+
# Raises ValueError is subject is not between 1 and 5
140+
if (subject < 1) or (subject > 5):
141+
raise ValueError(f"Subject must be between 1 and 5. Got {subject}")
142+
143+
subject_names = ["aa", "al", "av", "aw", "ay"]
144+
145+
# fmt: off
146+
ch_names = ['Fp1', 'AFp1', 'Fpz', 'AFp2', 'Fp2', 'AF7', 'AF3',
147+
'AF4', 'AF8', 'FAF5', 'FAF1', 'FAF2', 'FAF6', 'F7',
148+
'F5', 'F3', 'F1', 'Fz', 'F2', 'F4', 'F6', 'F8', 'FFC7',
149+
'FFC5', 'FFC3', 'FFC1', 'FFC2', 'FFC4', 'FFC6', 'FFC8',
150+
'FT9', 'FT7', 'FC5', 'FC3', 'FC1', 'FCz', 'FC2', 'FC4',
151+
'FC6', 'FT8', 'FT10', 'CFC7', 'CFC5', 'CFC3', 'CFC1',
152+
'CFC2', 'CFC4', 'CFC6', 'CFC8', 'T7', 'C5', 'C3', 'C1',
153+
'Cz','C2', 'C4', 'C6', 'T8', 'CCP7', 'CCP5', 'CCP3', 'CCP1',
154+
'CCP2', 'CCP4', 'CCP6', 'CCP8', 'TP9', 'TP7', 'CP5', 'CP3',
155+
'CP1', 'CPz', 'CP2', 'CP4', 'CP6', 'TP8', 'TP10', 'PCP7',
156+
'PCP5', 'PCP3', 'PCP1', 'PCP2', 'PCP4', 'PCP6', 'PCP8', 'P9',
157+
'P7', 'P5', 'P3', 'P1', 'Pz', 'P2', 'P4', 'P6', 'P8', 'P10',
158+
'PPO7', 'PPO5', 'PPO1', 'PPO2', 'PPO6', 'PPO8', 'PO7', 'PO3',
159+
'PO1', 'POz', 'PO2', 'PO4', 'PO8', 'OPO1', 'OPO2', 'O1',
160+
'Oz', 'O2', 'OI1', 'OI2', 'I1', 'I2']
161+
# fmt: on
162+
ch_type = ["eeg"] * 118
163+
164+
url = "{u}download/competition_iii/berlin/100Hz/data_set_IVa_{r}_mat.zip".format(
165+
u=base_url, r=subject_names[subject - 1]
166+
)
167+
168+
filename = data_path(url, path, force_update, update_path)
169+
170+
if only_filenames:
171+
return filename
172+
173+
runs, ev = _convert_bbci2003(filename[0], ch_names, ch_type)
174+
175+
session = {"0train": {"0": runs}}
176+
return session
177+
178+
122179
@verbose
123180
def _load_data_001_2014(
124181
subject,
@@ -698,6 +755,102 @@ def _convert_run_bbci(run, ch_types, verbose=None):
698755
return raw, event_id
699756

700757

758+
def _convert_bbci2003(filename, ch_names, ch_type):
759+
"""
760+
Process motor imagery data from MAT files.
761+
762+
Parameters
763+
----------
764+
filename (str):
765+
Path to the MAT file.
766+
ch_names (list of str):
767+
List of channel names.
768+
ch_type (list of str):
769+
List of channel types.
770+
771+
Returns
772+
-------
773+
raw (instance of RawArray):
774+
returns MNE Raw object.
775+
"""
776+
zip_path = Path(filename)
777+
778+
with zipfile.ZipFile(zip_path, "r") as z:
779+
mat_files = [f for f in z.namelist() if f.endswith(".mat")]
780+
781+
if not mat_files:
782+
raise FileNotFoundError("No .mat file found in zip archive.")
783+
784+
with z.open(mat_files[0]) as f:
785+
data = loadmat(io.BytesIO(f.read()))
786+
787+
run = data
788+
raw, ev = _convert_run_bbci2003(run, ch_names, ch_type)
789+
return raw, ev
790+
791+
792+
@verbose
793+
def _convert_run_bbci2003(run, ch_names, ch_types, verbose=None):
794+
"""
795+
Converts one run to a raw MNE object.
796+
797+
Parameters
798+
----------
799+
run (ndarray):
800+
The continuous EEG signal.
801+
ch_names (list of str):
802+
List of channel names.
803+
ch_types (list of str):
804+
List of channel types.
805+
verbose (bool, str, int, or None):
806+
If not None, override default verbose level (see :func:`mne.verbose`
807+
and :ref:`Logging documentation <tut_logging>` for more).
808+
809+
Returns:
810+
raw (instance of RawArray):
811+
MNE Raw object.
812+
event_id (dict):
813+
Dictionary containing class names.
814+
"""
815+
class_map = {
816+
"right": "right_hand",
817+
"foot": "feet",
818+
}
819+
820+
raw_labels = run["mrk"]["y"][0, 0][0]
821+
labels_mask = ~np.isnan(raw_labels)
822+
valid_labels = raw_labels[labels_mask]
823+
labels = valid_labels.astype(int) - 1
824+
825+
raw_positions = run["mrk"][0][0]["pos"][0]
826+
positions = raw_positions[labels_mask]
827+
828+
sfreq = float(run["nfo"][0, 0]["fs"][0, 0])
829+
eeg_data = run["cnt"]
830+
raw_classes = run["mrk"]["className"]
831+
832+
while isinstance(raw_classes, (list, np.ndarray)) and len(raw_classes) == 1:
833+
raw_classes = raw_classes[0]
834+
class_names = [cls[0] for cls in raw_classes]
835+
836+
for i, word in enumerate(class_names):
837+
if word in class_map:
838+
class_names[i] = class_map[word]
839+
840+
info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq)
841+
842+
onset = positions / sfreq
843+
duration = 0
844+
description = [class_names[i] for i in labels]
845+
annotations = Annotations(onset=onset, duration=duration, description=description)
846+
847+
event_id = {name: i for i, name in enumerate(class_names)}
848+
raw = RawArray(data=eeg_data.T, info=info, verbose=verbose)
849+
raw.set_annotations(annotations)
850+
851+
return raw, event_id
852+
853+
701854
@verbose
702855
def _convert_run_epfl(run, verbose=None):
703856
"""Convert one run to raw."""
@@ -752,6 +905,51 @@ def data_path(
752905
)
753906

754907

908+
class BNCI2003_004(MNEBNCI):
909+
"""
910+
BNCI2003_IVa Motor Imagery dataset.
911+
912+
Dataset IVa from BCI Competition III [1]_.
913+
914+
**Dataset Description**
915+
916+
This data set was recorded from five healthy subjects. Subjects sat in
917+
a comfortable chair with arms resting on armrests. This data set
918+
contains only data from the 4 initial sessions without feedback.
919+
Visual cues indicated for 3.5 s which of the following 3 motor
920+
imageries the subject should perform: (L) left hand, (R) right hand,
921+
(F) right foot. The presentation of target cues were intermitted by
922+
periods of random length, 1.75 to 2.25 s, in which the subject could
923+
relax.
924+
925+
There were two types of visual stimulation: (1) where targets were
926+
indicated by letters appearing behind a fixation cross (which might
927+
nevertheless induce little target-correlated eye movements), and (2)
928+
where a randomly moving object indicated targets (inducing target-
929+
uncorrelated eye movements). From subjects al and aw 2 sessions of
930+
both types were recorded, while from the other subjects 3 sessions
931+
of type (2) and 1 session of type (1) were recorded.
932+
933+
References
934+
----------
935+
.. [1] Guido Dornhege, Benjamin Blankertz, Gabriel Curio, and Klaus-Robert
936+
Müller. Boosting bit rates in non-invasive EEG single-trial
937+
classifications by feature combination and multi-class paradigms.
938+
IEEE Trans. Biomed. Eng., 51(6):993-1002, June 2004.
939+
"""
940+
941+
def __init__(self):
942+
super().__init__(
943+
subjects=list(range(1, 6)),
944+
sessions_per_subject=1,
945+
events={"right_hand": 0, "feet": 1},
946+
code="BNCI2003-004",
947+
interval=[0, 3.5],
948+
paradigm="imagery",
949+
doi="10.1109/TBME.2004.827088",
950+
)
951+
952+
755953
@depreciated_alias("BNCI2014001", "1.1")
756954
class BNCI2014_001(MNEBNCI):
757955
"""BNCI 2014-001 Motor Imagery dataset.

moabb/datasets/summary_imagery.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Dataset, #Subj, #Chan, #Classes, #Trials / class, Trials length (s), Freq (Hz), #Sessions, #Runs, Total_trials, PapersWithCode leaderboard
22
AlexMI,8,16,3,20,3,512,1,1,480,https://paperswithcode.com/dataset/alexandremotorimagery-moabb
3+
BNCI2003_004,5,118,2,84,3.5,100,1,1,1400,
34
BNCI2014_001,9,22,4,144,4,250,2,6,62208,https://paperswithcode.com/dataset/bnci2014-001-moabb-1
45
BNCI2014_002,14,15,2,80,5,512,1,8,17920,https://paperswithcode.com/dataset/bnci2014-002-moabb-1
56
BNCI2014_004,9,3,2,360,4.5,250,5,1,32400,https://paperswithcode.com/dataset/bnci2014-004-moabb-1

moabb/evaluations/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
WithinSessionEvaluation,
1111
)
1212
from .splitters import CrossSessionSplitter, CrossSubjectSplitter, WithinSessionSplitter
13-
from .utils import create_save_path, save_model_cv, save_model_list
13+
from .utils import _create_save_path, _save_model_cv

0 commit comments

Comments
 (0)