braindecode 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. Braindecode-0.5.1.dist-info/LICENSE.txt +31 -0
  2. Braindecode-0.5.1.dist-info/METADATA +133 -0
  3. Braindecode-0.5.1.dist-info/RECORD +69 -0
  4. Braindecode-0.5.1.dist-info/WHEEL +5 -0
  5. Braindecode-0.5.1.dist-info/top_level.txt +2 -0
  6. braindecode/__init__.py +4 -0
  7. braindecode/classifier.py +258 -0
  8. braindecode/datasets/__init__.py +7 -0
  9. braindecode/datasets/base.py +293 -0
  10. braindecode/datasets/bbci.py +679 -0
  11. braindecode/datasets/moabb.py +110 -0
  12. braindecode/datasets/sleep_physionet.py +103 -0
  13. braindecode/datasets/tuh.py +230 -0
  14. braindecode/datautil/__init__.py +11 -0
  15. braindecode/datautil/mne.py +136 -0
  16. braindecode/datautil/preprocess.py +364 -0
  17. braindecode/datautil/serialization.py +111 -0
  18. braindecode/datautil/util.py +36 -0
  19. braindecode/datautil/windowers.py +435 -0
  20. braindecode/datautil/xy.py +77 -0
  21. braindecode/models/__init__.py +12 -0
  22. braindecode/models/deep4.py +252 -0
  23. braindecode/models/eegnet.py +339 -0
  24. braindecode/models/eegresnet.py +232 -0
  25. braindecode/models/functions.py +43 -0
  26. braindecode/models/hybrid.py +103 -0
  27. braindecode/models/modules.py +150 -0
  28. braindecode/models/shallow_fbcsp.py +158 -0
  29. braindecode/models/sleep_stager_chambon_2018.py +113 -0
  30. braindecode/models/tcn.py +143 -0
  31. braindecode/models/util.py +66 -0
  32. braindecode/regressor.py +240 -0
  33. braindecode/samplers/__init__.py +5 -0
  34. braindecode/samplers/base.py +78 -0
  35. braindecode/samplers/ssl.py +118 -0
  36. braindecode/training/__init__.py +8 -0
  37. braindecode/training/callbacks.py +21 -0
  38. braindecode/training/losses.py +17 -0
  39. braindecode/training/scoring.py +296 -0
  40. braindecode/util.py +362 -0
  41. braindecode/version.py +1 -0
  42. braindecode/visualization/__init__.py +5 -0
  43. braindecode/visualization/gradients.py +53 -0
  44. test/__init__.py +0 -0
  45. test/acceptance_tests/__init__.py +0 -0
  46. test/acceptance_tests/test_cropped_decoding.py +171 -0
  47. test/acceptance_tests/test_eeg_classifier.py +255 -0
  48. test/acceptance_tests/test_model_architectures.py +54 -0
  49. test/acceptance_tests/test_trialwise_decoding.py +168 -0
  50. test/unit_tests/__init__.py +0 -0
  51. test/unit_tests/datasets/__init__.py +0 -0
  52. test/unit_tests/datasets/test_dataset.py +227 -0
  53. test/unit_tests/datasets/test_mne.py +145 -0
  54. test/unit_tests/datasets/test_sleep_physionet.py +23 -0
  55. test/unit_tests/datasets/test_tuh.py +97 -0
  56. test/unit_tests/datasets/test_xy.py +31 -0
  57. test/unit_tests/datautil/__init__.py +0 -0
  58. test/unit_tests/datautil/test_preprocess.py +270 -0
  59. test/unit_tests/datautil/test_serialization.py +127 -0
  60. test/unit_tests/datautil/test_windowers.py +519 -0
  61. test/unit_tests/models/__init__.py +0 -0
  62. test/unit_tests/models/test_models.py +136 -0
  63. test/unit_tests/models/test_util.py +22 -0
  64. test/unit_tests/test_eeg_classifier.py +73 -0
  65. test/unit_tests/test_eeg_regressor.py +72 -0
  66. test/unit_tests/test_scoring.py +282 -0
  67. test/unit_tests/test_util.py +30 -0
  68. test/unit_tests/visualization/__init__.py +0 -0
  69. test/unit_tests/visualization/test_gradients.py +38 -0
@@ -0,0 +1,110 @@
1
+ """Dataset objects for some public datasets.
2
+ """
3
+
4
+ # Authors: Hubert Banville <hubert.jbanville@gmail.com>
5
+ # Lukas Gemein <l.gemein@gmail.com>
6
+ # Simon Brandt <simonbrandt@protonmail.com>
7
+ # David Sabbagh <dav.sabbagh@gmail.com>
8
+ #
9
+ # License: BSD (3-clause)
10
+
11
+ import pandas as pd
12
+ import mne
13
+
14
+ from .base import BaseDataset, BaseConcatDataset, WindowsDataset
15
+
16
+
17
+ def _find_dataset_in_moabb(dataset_name):
18
+ # soft dependency on moabb
19
+ from moabb.datasets.utils import dataset_list
20
+ for dataset in dataset_list:
21
+ if dataset_name == dataset.__name__:
22
+ # return an instance of the found dataset class
23
+ return dataset()
24
+ raise ValueError("'dataset_name' not found in moabb datasets")
25
+
26
+
27
+ def _fetch_and_unpack_moabb_data(dataset, subject_ids):
28
+ data = dataset.get_data(subject_ids)
29
+ raws, subject_ids, session_ids, run_ids = [], [], [], []
30
+ for subj_id, subj_data in data.items():
31
+ for sess_id, sess_data in subj_data.items():
32
+ for run_id, raw in sess_data.items():
33
+ # set annotation if empty
34
+ if len(raw.annotations) == 0:
35
+ annots = _annotations_from_moabb_stim_channel(raw, dataset)
36
+ raw.set_annotations(annots)
37
+ raws.append(raw)
38
+ subject_ids.append(subj_id)
39
+ session_ids.append(sess_id)
40
+ run_ids.append(run_id)
41
+ description = pd.DataFrame({
42
+ 'subject': subject_ids,
43
+ 'session': session_ids,
44
+ 'run': run_ids
45
+ })
46
+ return raws, description
47
+
48
+
49
+ def _annotations_from_moabb_stim_channel(raw, dataset):
50
+ # find events from stim channel
51
+ events = mne.find_events(raw)
52
+
53
+ # get annotations from events
54
+ event_desc = {k: v for v, k in dataset.event_id.items()}
55
+ annots = mne.annotations_from_events(events, raw.info['sfreq'], event_desc)
56
+
57
+ # set trial on and offset given by moabb
58
+ onset, offset = dataset.interval
59
+ annots.onset += onset
60
+ annots.duration += offset - onset
61
+ return annots
62
+
63
+
64
+ def fetch_data_with_moabb(dataset_name, subject_ids):
65
+ # ToDo: update path to where moabb downloads / looks for the data
66
+ """Fetch data using moabb.
67
+
68
+ Parameters
69
+ ----------
70
+ dataset_name: str
71
+ the name of a dataset included in moabb
72
+ subject_ids: list(int) | int
73
+ (list of) int of subject(s) to be fetched
74
+
75
+ Returns
76
+ -------
77
+ raws: mne.Raw
78
+ info: pandas.DataFrame
79
+ """
80
+ dataset = _find_dataset_in_moabb(dataset_name)
81
+ subject_id = [subject_ids] if isinstance(subject_ids, int) else subject_ids
82
+ return _fetch_and_unpack_moabb_data(dataset, subject_id)
83
+
84
+
85
+ class MOABBDataset(BaseConcatDataset):
86
+ """A class for moabb datasets.
87
+
88
+ Parameters
89
+ ----------
90
+ dataset_name: name of dataset included in moabb to be fetched
91
+ subject_ids: list(int) | int
92
+ (list of) int of subject(s) to be fetched
93
+ """
94
+ def __init__(self, dataset_name, subject_ids):
95
+ raws, description = fetch_data_with_moabb(dataset_name, subject_ids)
96
+ all_base_ds = [BaseDataset(raw, row)
97
+ for raw, (_, row) in zip(raws, description.iterrows())]
98
+ super().__init__(all_base_ds)
99
+
100
+
101
+ class BNCI2014001(MOABBDataset):
102
+ """See moabb.datasets.bnci.BNCI2014001"""
103
+ def __init__(self, *args, **kwargs):
104
+ super().__init__("BNCI2014001", *args, **kwargs)
105
+
106
+
107
+ class HGD(MOABBDataset):
108
+ """See moabb.datasets.schirrmeister2017.Schirrmeister2017"""
109
+ def __init__(self, *args, **kwargs):
110
+ super().__init__("Schirrmeister2017", *args, **kwargs)
@@ -0,0 +1,103 @@
1
+ # Authors: Hubert Banville <hubert.jbanville@gmail.com>
2
+ #
3
+ # License: BSD (3-clause)
4
+
5
+
6
+ import os
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import mne
11
+ from mne.datasets.sleep_physionet.age import fetch_data
12
+
13
+ from .base import BaseDataset, BaseConcatDataset
14
+
15
+
16
+ class SleepPhysionet(BaseConcatDataset):
17
+ """Sleep Physionet dataset.
18
+
19
+ Sleep dataset from https://physionet.org/content/sleep-edfx/1.0.0/.
20
+ Contains overnight recordings from 78 healthy subjects.
21
+
22
+ See [MNE example](https://mne.tools/stable/auto_tutorials/sample-datasets/plot_sleep.html).
23
+
24
+ Parameters
25
+ ----------
26
+ subject_ids: list(int) | int | None
27
+ (list of) int of subject(s) to be loaded. If None, load all available
28
+ subjects.
29
+ recording_ids: list(int) | None
30
+ Recordings to load per subject (each subject except 13 has two
31
+ recordings). Can be [1], [2] or [1, 2] (same as None).
32
+ preload: bool
33
+ If True, preload the data of the Raw objects.
34
+ load_eeg_only: bool
35
+ If True, only load the EEG channels and discard the others (EOG, EMG,
36
+ temperature, respiration) to avoid resampling the other signals.
37
+ crop_wake_mins: float
38
+ Number of minutes of wake time to keep before the first sleep event
39
+ and after the last sleep event. Used to reduce the imbalance in this
40
+ dataset. Default of 30 mins.
41
+ """
42
+ def __init__(self, subject_ids=None, recording_ids=None, preload=False,
43
+ load_eeg_only=True, crop_wake_mins=30):
44
+ if subject_ids is None:
45
+ subject_ids = range(83)
46
+ if recording_ids is None:
47
+ recording_ids = [1, 2]
48
+
49
+ paths = fetch_data(
50
+ subject_ids, recording=recording_ids, on_missing='warn')
51
+
52
+ all_base_ds = list()
53
+ for p in paths:
54
+ raw, desc = self._load_raw(
55
+ p[0], p[1], preload=preload, load_eeg_only=load_eeg_only,
56
+ crop_wake_mins=crop_wake_mins)
57
+ base_ds = BaseDataset(raw, desc)
58
+ all_base_ds.append(base_ds)
59
+ super().__init__(all_base_ds)
60
+
61
+ @staticmethod
62
+ def _load_raw(raw_fname, ann_fname, preload, load_eeg_only=True,
63
+ crop_wake_mins=False):
64
+ ch_mapping = {
65
+ 'EOG horizontal': 'eog',
66
+ 'Resp oro-nasal': 'misc',
67
+ 'EMG submental': 'misc',
68
+ 'Temp rectal': 'misc',
69
+ 'Event marker': 'misc'
70
+ }
71
+ exclude = ch_mapping.keys() if load_eeg_only else ()
72
+
73
+ raw = mne.io.read_raw_edf(raw_fname, preload=preload, exclude=exclude)
74
+ annots = mne.read_annotations(ann_fname)
75
+ raw.set_annotations(annots, emit_warning=False)
76
+
77
+ if crop_wake_mins > 0:
78
+ # Find first and last sleep stages
79
+ mask = [
80
+ x[-1] in ['1', '2', '3', '4', 'R'] for x in annots.description]
81
+ sleep_event_inds = np.where(mask)[0]
82
+
83
+ # Crop raw
84
+ tmin = annots[int(sleep_event_inds[0])]['onset'] - \
85
+ crop_wake_mins * 60
86
+ tmax = annots[int(sleep_event_inds[-1])]['onset'] + \
87
+ crop_wake_mins * 60
88
+ raw.crop(tmin=tmin, tmax=tmax)
89
+
90
+ # Rename EEG channels
91
+ ch_names = {
92
+ i: i.replace('EEG ', '') for i in raw.ch_names if 'EEG' in i}
93
+ mne.rename_channels(raw.info, ch_names)
94
+
95
+ if not load_eeg_only:
96
+ raw.set_channel_types(ch_mapping)
97
+
98
+ basename = os.path.basename(raw_fname)
99
+ subj_nb = int(basename[3:5])
100
+ sess_nb = int(basename[5])
101
+ desc = pd.Series({'subject': subj_nb, 'recording': sess_nb}, name='')
102
+
103
+ return raw, desc
@@ -0,0 +1,230 @@
1
+ """
2
+ Dataset classes for the Temple University Hospital (TUH) EEG Corpus and the
3
+ TUH Abnormal EEG Corpus.
4
+ """
5
+
6
+ # Authors: Lukas Gemein <l.gemein@gmail.com>
7
+ #
8
+ # License: BSD (3-clause)
9
+
10
+ import re
11
+ import os
12
+ import glob
13
+
14
+ import pandas as pd
15
+ import mne
16
+
17
+ from .base import BaseDataset, BaseConcatDataset
18
+
19
+
20
+ class TUH(BaseConcatDataset):
21
+ """Temple University Hospital (TUH) EEG Corpus
22
+ (www.isip.piconepress.com/projects/tuh_eeg/html/downloads.shtml#c_tueg).
23
+
24
+ Parameters
25
+ ----------
26
+ path: str
27
+ Parent directory of the dataset.
28
+ recording_ids: list(int) | int
29
+ A (list of) int of recording id(s) to be read (order matters and will
30
+ overwrite default chronological order, e.g. if recording_ids=[1,0],
31
+ then the first recording returned by this class will be chronologically
32
+ later then the second recording. Provide recording_ids in ascending
33
+ order to preserve chronological order.).
34
+ target_name: str
35
+ Can be 'gender', or 'age'.
36
+ preload: bool
37
+ If True, preload the data of the Raw objects.
38
+ add_physician_reports: bool
39
+ If True, the physician reports will be read from disk and added to the
40
+ description.
41
+ """
42
+ def __init__(self, path, recording_ids=None, target_name=None,
43
+ preload=False, add_physician_reports=False):
44
+ # create an index of all files and gather easily accessible info
45
+ # without actually touching the files
46
+ file_paths = glob.glob(os.path.join(path, '**/*.edf'), recursive=True)
47
+ descriptions = _create_chronological_description(file_paths)
48
+ # limit to specified recording ids before doing slow stuff
49
+ if recording_ids is not None:
50
+ descriptions = descriptions[recording_ids]
51
+ # create datasets gathering more info about the files touching them
52
+ # reading the raws and potentially preloading the data
53
+ base_datasets = self._create_datasets(
54
+ descriptions, target_name, preload, add_physician_reports)
55
+ super().__init__(base_datasets)
56
+
57
+ @staticmethod
58
+ def _create_datasets(descriptions, target_name, preload,
59
+ add_physician_reports):
60
+ # this is the second loop (slow)
61
+ base_datasets = []
62
+ for file_path_i, file_path in descriptions.loc['path'].iteritems():
63
+ # parse age and gender information from EDF header
64
+ age, gender = _parse_age_and_gender_from_edf_header(file_path)
65
+ raw = mne.io.read_raw_edf(file_path, preload=preload)
66
+ # read info relevant for preprocessing from raw without loading it
67
+ sfreq = raw.info['sfreq']
68
+ n_samples = raw.n_times
69
+ d = {
70
+ 'sfreq': float(sfreq),
71
+ 'n_samples': int(n_samples),
72
+ 'age': int(age),
73
+ 'gender': gender,
74
+ }
75
+ if add_physician_reports:
76
+ physician_report = _read_physician_report(file_path)
77
+ d['report'] = physician_report
78
+ additional_description = pd.Series(d)
79
+ description = pd.concat(
80
+ [descriptions.pop(file_path_i), additional_description])
81
+ base_dataset = BaseDataset(raw, description,
82
+ target_name=target_name)
83
+ base_datasets.append(base_dataset)
84
+ return base_datasets
85
+
86
+
87
+ def _create_chronological_description(file_paths):
88
+ # this is the first loop (fast)
89
+ descriptions = []
90
+ for file_path in file_paths:
91
+ description = _parse_description_from_file_path(file_path)
92
+ descriptions.append(pd.Series(description))
93
+ descriptions = pd.concat(descriptions, axis=1)
94
+ # order descriptions chronologically
95
+ descriptions.sort_values(
96
+ ["year", "month", "day", "subject", "session", "segment"],
97
+ axis=1, inplace=True)
98
+ # https://stackoverflow.com/questions/42284617/reset-column-index-pandas
99
+ descriptions = descriptions.T.reset_index(drop=True).T
100
+ return descriptions
101
+
102
+
103
+ def _parse_description_from_file_path(file_path):
104
+ # stackoverflow.com/questions/3167154/how-to-split-a-dos-path-into-its-components-in-python # noqa
105
+ file_path = os.path.normpath(file_path)
106
+ tokens = file_path.split(os.sep)
107
+ # expect file paths as tuh_eeg/version/file_type/reference/data_split/
108
+ # subject/recording session/file
109
+ # e.g. tuh_eeg/v1.1.0/edf/01_tcp_ar/027/00002729/
110
+ # s001_2006_04_12/00002729_s001.edf
111
+ version = tokens[-7]
112
+ year, month, day = tokens[-2].split('_')[1:]
113
+ subject_id = tokens[-3]
114
+ session = tokens[-2].split('_')[0]
115
+ segment = tokens[-1].split('_')[-1].split('.')[-2]
116
+ reference = tokens[-5].split('_')[2]
117
+ return {
118
+ 'path': file_path,
119
+ 'version': version,
120
+ 'year': int(year),
121
+ 'month': int(month),
122
+ 'day': int(day),
123
+ 'subject': int(subject_id),
124
+ 'session': int(session[1:]),
125
+ 'segment': int(segment[1:]),
126
+ 'reference': reference,
127
+ }
128
+
129
+
130
+ def _read_physician_report(file_path):
131
+ directory = os.path.dirname(file_path)
132
+ txt_file = glob.glob(os.path.join(directory, '**/*.txt'), recursive=True)
133
+ # check that there is at most one txt file in the same directory
134
+ assert len(txt_file) in [0, 1]
135
+ report = ''
136
+ if txt_file:
137
+ txt_file = txt_file[0]
138
+ # somewhere in the corpus, encoding apparently changed
139
+ # first try to read as utf-8, if it does not work use latin-1
140
+ try:
141
+ with open(txt_file, 'r', encoding='utf-8') as f:
142
+ report = f.read()
143
+ except UnicodeDecodeError:
144
+ with open(txt_file, 'r', encoding='latin-1') as f:
145
+ report = f.read()
146
+ return report
147
+
148
+
149
+ def _read_edf_header(file_path):
150
+ f = open(file_path, "rb")
151
+ header = f.read(88)
152
+ f.close()
153
+ return header
154
+
155
+
156
+ def _parse_age_and_gender_from_edf_header(file_path):
157
+ header = _read_edf_header(file_path)
158
+ # bytes 8 to 88 contain ascii local patient identification
159
+ # see https://www.teuniz.net/edfbrowser/edf%20format%20description.html
160
+ patient_id = header[8:].decode("ascii")
161
+ age = -1
162
+ found_age = re.findall(r"Age:(\d+)", patient_id)
163
+ if len(found_age) == 1:
164
+ age = int(found_age[0])
165
+ gender = "X"
166
+ found_gender = re.findall(r"\s([F|M])\s", patient_id)
167
+ if len(found_gender) == 1:
168
+ gender = found_gender[0]
169
+ return age, gender
170
+
171
+
172
+ class TUHAbnormal(TUH):
173
+ """Temple University Hospital (TUH) Abnormal EEG Corpus.
174
+ see www.isip.piconepress.com/projects/tuh_eeg/html/downloads.shtml#c_tuab
175
+
176
+ Parameters
177
+ ----------
178
+ path: str
179
+ Parent directory of the dataset.
180
+ recording_ids: list(int) | int
181
+ A (list of) int of recording id(s) to be read (order matters and will
182
+ overwrite default chronological order, e.g. if recording_ids=[1,0],
183
+ then the first recording returned by this class will be chronologically
184
+ later then the second recording. Provide recording_ids in ascending
185
+ order to preserve chronological order.).
186
+ target_name: str
187
+ Can be 'pathological', 'gender', or 'age'.
188
+ preload: bool
189
+ If True, preload the data of the Raw objects.
190
+ add_physician_reports: bool
191
+ If True, the physician reports will be read from disk and added to the
192
+ description.
193
+ """
194
+ def __init__(self, path, recording_ids=None, target_name='pathological',
195
+ preload=False, add_physician_reports=False):
196
+ super().__init__(path=path, recording_ids=recording_ids,
197
+ preload=preload,
198
+ add_physician_reports=add_physician_reports)
199
+ additional_descriptions = []
200
+ for file_path in self.description.path:
201
+ additional_description = (
202
+ self._parse_additional_description_from_file_path(file_path))
203
+ additional_descriptions.append(additional_description)
204
+ additional_descriptions = pd.DataFrame(additional_descriptions)
205
+ self.description = pd.concat(
206
+ [self.description, additional_descriptions], axis=1)
207
+ # not 100% sure if this is required:
208
+ # set target name and target of base datasets
209
+ for ds_i, ds in enumerate(self.datasets):
210
+ ds.target_name = target_name
211
+ ds.target = self.description.loc[ds_i, target_name]
212
+ for k in additional_descriptions.columns:
213
+ ds.description[k] = self.description.loc[ds_i, k]
214
+
215
+ @staticmethod
216
+ def _parse_additional_description_from_file_path(file_path):
217
+ file_path = os.path.normpath(file_path)
218
+ tokens = file_path.split(os.sep)
219
+ # expect paths as version/file type/data_split/pathology status/
220
+ # reference/subset/subject/recording session/file
221
+ # e.g. v2.0.0/edf/train/normal/01_tcp_ar/000/00000021/
222
+ # s004_2013_08_15/00000021_s004_t000.edf
223
+ assert ('abnormal' in tokens or 'normal' in tokens), (
224
+ 'No pathology labels found.')
225
+ assert ('train' in tokens or 'eval' in tokens), (
226
+ 'No train or eval set information found.')
227
+ return {
228
+ 'train': 'train' in tokens,
229
+ 'pathological': 'abnormal' in tokens,
230
+ }
@@ -0,0 +1,11 @@
1
+ """
2
+ Utilities for data manipulation.
3
+ """
4
+
5
+ from .windowers import create_windows_from_events, create_fixed_length_windows
6
+ from .preprocess import (zscore, scale, exponential_moving_demean,
7
+ exponential_moving_standardize, filterbank,
8
+ preprocess, Preprocessor)
9
+ from .xy import create_from_X_y
10
+ from .mne import create_from_mne_raw, create_from_mne_epochs
11
+ from .serialization import save_concat_dataset, load_concat_dataset
@@ -0,0 +1,136 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import mne
4
+
5
+ from ..datasets.base import BaseDataset, BaseConcatDataset, WindowsDataset
6
+ from ..datautil.windowers import (
7
+ _check_windowing_arguments, create_windows_from_events)
8
+
9
+
10
+ def create_from_mne_raw(
11
+ raws, trial_start_offset_samples, trial_stop_offset_samples,
12
+ window_size_samples, window_stride_samples, drop_last_window,
13
+ descriptions=None, mapping=None, preload=False, drop_bad_windows=True):
14
+ """Create WindowsDatasets from mne.RawArrays
15
+
16
+ Parameters
17
+ ----------
18
+ raws: array-like
19
+ list of mne.RawArrays
20
+ trial_start_offset_samples: int
21
+ start offset from original trial onsets in samples
22
+ trial_stop_offset_samples: int
23
+ stop offset from original trial stop in samples
24
+ window_size_samples: int
25
+ window size
26
+ window_stride_samples: int
27
+ stride between windows
28
+ drop_last_window: bool
29
+ whether or not have a last overlapping window, when
30
+ windows do not equally divide the continuous signal
31
+ descriptions: array-like
32
+ list of dicts or pandas.Series with additional information about the raws
33
+ mapping: dict(str: int)
34
+ mapping from event description to target value
35
+ preload: bool
36
+ if True, preload the data of the Epochs objects.
37
+ drop_bad_windows: bool
38
+ If True, call `.drop_bad()` on the resulting mne.Epochs object. This
39
+ step allows identifying e.g., windows that fall outside of the
40
+ continuous recording. It is suggested to run this step here as otherwise
41
+ the BaseConcatDataset has to be updated as well.
42
+
43
+ Returns
44
+ -------
45
+ windows_datasets: BaseConcatDataset
46
+ X and y transformed to a dataset format that is compativle with skorch
47
+ and braindecode
48
+ """
49
+ if descriptions is not None:
50
+ if len(descriptions) != len(raws):
51
+ raise ValueError(
52
+ f"length of 'raws' ({len(raws)}) and 'description' "
53
+ f"({len(descriptions)}) has to match")
54
+ base_datasets = [BaseDataset(raw, desc) for raw, desc in
55
+ zip(raws, descriptions)]
56
+ else:
57
+ base_datasets = [BaseDataset(raw) for raw in raws]
58
+
59
+ base_datasets = BaseConcatDataset(base_datasets)
60
+ windows_datasets = create_windows_from_events(
61
+ base_datasets,
62
+ trial_start_offset_samples=trial_start_offset_samples,
63
+ trial_stop_offset_samples=trial_stop_offset_samples,
64
+ window_size_samples=window_size_samples,
65
+ window_stride_samples=window_stride_samples,
66
+ drop_last_window=drop_last_window,
67
+ mapping=mapping,
68
+ drop_bad_windows=drop_bad_windows,
69
+ preload=preload
70
+ )
71
+ return windows_datasets
72
+
73
+
74
+ def create_from_mne_epochs(list_of_epochs, window_size_samples,
75
+ window_stride_samples, drop_last_window):
76
+ """Create WindowsDatasets from mne.Epochs
77
+
78
+ Parameters
79
+ ----------
80
+ list_of_epochs: array-like
81
+ list of mne.Epochs
82
+ window_size_samples: int
83
+ window size
84
+ window_stride_samples: int
85
+ stride between windows
86
+ drop_last_window: bool
87
+ whether or not have a last overlapping window, when
88
+ windows do not equally divide the continuous signal
89
+
90
+ Returns
91
+ -------
92
+ windows_datasets: BaseConcatDataset
93
+ X and y transformed to a dataset format that is compativle with skorch
94
+ and braindecode
95
+ """
96
+ _check_windowing_arguments(0, 0, window_size_samples,
97
+ window_stride_samples)
98
+
99
+ list_of_windows_ds = []
100
+ for epochs in list_of_epochs:
101
+ event_descriptions = epochs.events[:, 2]
102
+ original_trial_starts = epochs.events[:, 0]
103
+ stop = len(epochs.times) - window_size_samples
104
+
105
+ # already includes last incomplete window start
106
+ starts = np.arange(0, stop + 1, window_stride_samples)
107
+
108
+ if not drop_last_window and starts[-1] < stop:
109
+ # if last window does not end at trial stop, make it stop there
110
+ starts = np.append(starts, stop)
111
+
112
+ fake_events = [[start, window_size_samples, -1] for start in
113
+ starts]
114
+
115
+ for trial_i, trial in enumerate(epochs):
116
+ metadata = pd.DataFrame({
117
+ 'i_window_in_trial': np.arange(len(fake_events)),
118
+ 'i_start_in_trial': starts + original_trial_starts[trial_i],
119
+ 'i_stop_in_trial': starts + original_trial_starts[
120
+ trial_i] + window_size_samples,
121
+ 'target': len(fake_events) * [event_descriptions[trial_i]]
122
+ })
123
+ # window size - 1, since tmax is inclusive
124
+ mne_epochs = mne.Epochs(
125
+ mne.io.RawArray(trial, epochs.info), fake_events,
126
+ baseline=None,
127
+ tmin=0,
128
+ tmax=(window_size_samples - 1) / epochs.info["sfreq"],
129
+ metadata=metadata)
130
+
131
+ mne_epochs.drop_bad(reject=None, flat=None)
132
+
133
+ windows_ds = WindowsDataset(mne_epochs)
134
+ list_of_windows_ds.append(windows_ds)
135
+
136
+ return BaseConcatDataset(list_of_windows_ds)