braindecode 1.6.0.dev1028__tar.gz → 1.6.1__tar.gz
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.
- {braindecode-1.6.0.dev1028/braindecode.egg-info → braindecode-1.6.1}/PKG-INFO +2 -2
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/augmentation/base.py +67 -19
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/augmentation/functional.py +44 -30
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/__init__.py +2 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/base.py +109 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bids/hub.py +46 -1
- braindecode-1.6.1/braindecode/datasets/collate.py +105 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/moabb.py +0 -10
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/eegneuralnet.py +49 -1
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/functional/__init__.py +8 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/functional/functions.py +159 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/__init__.py +13 -3
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/atcnet.py +118 -1
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/attn_sleep.py +0 -10
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/biot.py +3 -10
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/brainmodule.py +185 -54
- braindecode-1.6.1/braindecode/models/eegdino.py +533 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegitnet.py +33 -12
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegnet.py +1 -10
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegpt.py +29 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegsym.py +72 -96
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegtcnet.py +62 -27
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/fbmsnet.py +18 -5
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/medformer.py +4 -22
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/msvtnet.py +7 -3
- braindecode-1.6.1/braindecode/models/mvpformer.py +750 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/sinc_shallow.py +2 -2
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/sstdpn.py +10 -4
- braindecode-1.6.1/braindecode/models/steegformer.py +654 -0
- braindecode-1.6.1/braindecode/models/summary.csv +62 -0
- braindecode-1.6.1/braindecode/models/tcformer.py +643 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/tsinception.py +1 -11
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/util.py +55 -2
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/__init__.py +8 -2
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/activation.py +34 -0
- braindecode-1.6.1/braindecode/modules/blocks.py +317 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/filter.py +80 -19
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/layers.py +126 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/preprocessing/mne_preprocess.py +152 -8
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/training/scoring.py +8 -1
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/util.py +36 -1
- braindecode-1.6.1/braindecode/version.py +1 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1/braindecode.egg-info}/PKG-INFO +2 -2
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode.egg-info/SOURCES.txt +5 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode.egg-info/requires.txt +1 -1
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/api.rst +11 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/conf.py +14 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/index.rst +5 -4
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/models_table.rst +8 -3
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/whats_new.rst +212 -4
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/pyproject.toml +7 -1
- braindecode-1.6.0.dev1028/braindecode/models/summary.csv +0 -57
- braindecode-1.6.0.dev1028/braindecode/modules/blocks.py +0 -160
- braindecode-1.6.0.dev1028/braindecode/version.py +0 -1
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/LICENSE.txt +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/MANIFEST.in +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/NOTICE.txt +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/README.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/__init__.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/augmentation/__init__.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/augmentation/transforms.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/classifier.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bbci.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bcicomp.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bids/__init__.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bids/datasets.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bids/format.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bids/hub_format.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bids/hub_io.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bids/hub_validation.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/bids/iterable.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/chb_mit.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/mne.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/nmt.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/registry.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/siena.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/sleep_physio_challe_18.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/sleep_physionet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/tuh.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/utils.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datasets/xy.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datautil/__init__.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datautil/channel_utils.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datautil/hub_formats.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datautil/serialization.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/datautil/util.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/functional/initialization.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/attentionbasenet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/base.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/bendr.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/cbramod.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/codebrain.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/config.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/contrawr.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/ctnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/deep4.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/deepsleepnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/dgcnn.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegconformer.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eeginception_erp.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eeginception_mi.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegminer.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegnex.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/eegsimpleconv.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/emg2qwerty.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/fbcnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/fblightconvnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/hybrid.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/ifnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/interpolated.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/labram.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/luna.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/meta_neuromotor.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/patchedtransformer.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/reve.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/sccnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/shallow_fbcsp.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/signal_jepa.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/sleep_stager_blanco_2020.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/sleep_stager_chambon_2018.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/sparcnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/syncnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/tcn.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/tidnet.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/models/usleep.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/attention.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/convolution.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/interpolation.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/linear.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/parametrization.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/stats.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/util.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/modules/wrapper.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/preprocessing/__init__.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/preprocessing/eegprep_preprocess.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/preprocessing/preprocess.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/preprocessing/util.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/preprocessing/windowers.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/regressor.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/samplers/__init__.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/samplers/base.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/samplers/ssl.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/training/__init__.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/training/callbacks.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/training/losses.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/visualization/__init__.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/visualization/attribution.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/visualization/confusion_matrices.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/visualization/frequency.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/visualization/metrics.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/visualization/sanity.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode/visualization/topology.py +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode.egg-info/dependency_links.txt +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/braindecode.egg-info/top_level.txt +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/Makefile +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/_templates/autosummary/class.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/_templates/autosummary/class_in_subdir.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/_templates/autosummary/function.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/_templates/autosummary/function_in_subdir.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/cite.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/help.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/install/install.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/install/install_pip.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/install/install_source.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/attention.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/channel.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/convolution.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/filterbank.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/gnn.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/interpretable.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/lbm.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/recurrent.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/categorization/spd.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/models.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/models_categorization.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/models/models_visualization.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/docs/sg_execution_times.rst +0 -0
- {braindecode-1.6.0.dev1028 → braindecode-1.6.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: braindecode
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.1
|
|
4
4
|
Summary: Deep learning software to decode EEG, ECG or MEG signals
|
|
5
5
|
Author-email: Robin Tibor Schirrmeister <robintibor@gmail.com>, Bruno Aristimunha Pinto <b.aristimunha@gmail.com>, Alexandre Gramfort <agramfort@meta.com>
|
|
6
6
|
Maintainer-email: Alexandre Gramfort <agramfort@meta.com>, Bruno Aristimunha Pinto <b.aristimunha@gmail.com>, Robin Tibor Schirrmeister <robintibor@gmail.com>
|
|
@@ -46,7 +46,7 @@ Requires-Dist: zarr>=3.0; extra == "hub"
|
|
|
46
46
|
Provides-Extra: viz
|
|
47
47
|
Requires-Dist: captum>=0.7.0; extra == "viz"
|
|
48
48
|
Provides-Extra: tests
|
|
49
|
-
Requires-Dist: pytest; extra == "tests"
|
|
49
|
+
Requires-Dist: pytest<9.1; extra == "tests"
|
|
50
50
|
Requires-Dist: pytest-cov; extra == "tests"
|
|
51
51
|
Requires-Dist: codecov; extra == "tests"
|
|
52
52
|
Requires-Dist: pytest_cases; extra == "tests"
|
|
@@ -105,7 +105,15 @@ class Transform(torch.nn.Module):
|
|
|
105
105
|
mask = self._get_mask(out_X.shape[0], out_X.device)
|
|
106
106
|
num_valid = mask.sum().long()
|
|
107
107
|
|
|
108
|
-
if num_valid
|
|
108
|
+
if num_valid == out_X.shape[0]:
|
|
109
|
+
# Fast path: every sample is augmented (e.g. ``probability=1.0``,
|
|
110
|
+
# the common case). Operate on the whole batch and skip the boolean
|
|
111
|
+
# gather/scatter (``out_X[mask]`` then ``out_X[mask] = ...``), which
|
|
112
|
+
# would otherwise copy the batch twice.
|
|
113
|
+
out_X, out_y = self.operation(
|
|
114
|
+
out_X, out_y, **self.get_augmentation_params(out_X, out_y)
|
|
115
|
+
)
|
|
116
|
+
elif num_valid > 0:
|
|
109
117
|
# Uses the mask to define the output
|
|
110
118
|
out_X[mask, ...], tr_y = self.operation(
|
|
111
119
|
out_X[mask, ...],
|
|
@@ -168,23 +176,47 @@ class Compose(Transform):
|
|
|
168
176
|
return X, y
|
|
169
177
|
|
|
170
178
|
|
|
171
|
-
|
|
172
|
-
"""
|
|
173
|
-
|
|
174
|
-
with device control.
|
|
175
|
-
"""
|
|
179
|
+
class _AugmentationCollate:
|
|
180
|
+
"""Collate that applies a transform to each batch, with optional expansion.
|
|
176
181
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
A picklable callable (so it works with ``num_workers > 0``, unlike a
|
|
183
|
+
closure) that collates the batch once and then either augments it in place
|
|
184
|
+
or grows it.
|
|
180
185
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
transform : Transform
|
|
189
|
+
Transform applied to the collated ``(X, y)``.
|
|
190
|
+
device : str | torch.device | None, optional
|
|
191
|
+
Device the batch is moved to before transforming. Defaults to None.
|
|
192
|
+
n_augmentation : int, optional
|
|
193
|
+
``0`` (default) applies the transform in place (batch size unchanged).
|
|
194
|
+
``> 0`` keeps the clean originals and appends ``n_augmentation``
|
|
195
|
+
independently transformed copies, returning ``(X, y)`` of
|
|
196
|
+
``(1 + n_augmentation)`` times the original size.
|
|
197
|
+
"""
|
|
186
198
|
|
|
187
|
-
|
|
199
|
+
def __init__(self, transform, device=None, n_augmentation=0):
|
|
200
|
+
self.transform = transform
|
|
201
|
+
self.device = device
|
|
202
|
+
self.n_augmentation = n_augmentation
|
|
203
|
+
|
|
204
|
+
def __call__(self, batch):
|
|
205
|
+
collated = default_collate(batch)
|
|
206
|
+
X, y = collated[:2]
|
|
207
|
+
if self.device is not None:
|
|
208
|
+
X = X.to(self.device)
|
|
209
|
+
y = y.to(self.device)
|
|
210
|
+
if self.n_augmentation == 0:
|
|
211
|
+
return (*self.transform(X, y), *collated[2:])
|
|
212
|
+
# Fixed expansion: keep the clean originals, then append
|
|
213
|
+
# ``n_augmentation`` independently transformed copies.
|
|
214
|
+
xs, ys = [X], [y]
|
|
215
|
+
for _ in range(self.n_augmentation):
|
|
216
|
+
aug_X, aug_y = self.transform(X, y)
|
|
217
|
+
xs.append(aug_X)
|
|
218
|
+
ys.append(aug_y)
|
|
219
|
+
return torch.cat(xs), torch.cat(ys)
|
|
188
220
|
|
|
189
221
|
|
|
190
222
|
class AugmentedDataLoader(DataLoader):
|
|
@@ -198,28 +230,44 @@ class AugmentedDataLoader(DataLoader):
|
|
|
198
230
|
Transform or sequence of Transform to be applied to each batch.
|
|
199
231
|
device : str | torch.device | None, optional
|
|
200
232
|
Device on which to transform the data. Defaults to None.
|
|
233
|
+
n_augmentation : int, optional
|
|
234
|
+
Number of augmented copies to append to each batch (fixed expansion).
|
|
235
|
+
When ``0`` (default) the transforms are applied in place and the batch
|
|
236
|
+
keeps its size (stochastic-per-epoch augmentation, backwards-compatible).
|
|
237
|
+
When ``> 0`` each batch becomes ``(1 + n_augmentation)`` times larger:
|
|
238
|
+
the clean originals are kept and ``n_augmentation`` independently
|
|
239
|
+
transformed copies are appended. This expresses augmentations defined as
|
|
240
|
+
a fixed set-expansion (e.g. the EEG-Inception MI 6x training set,
|
|
241
|
+
``n_augmentation=5``). In this mode batches are returned as ``(X, y)``.
|
|
201
242
|
**kwargs : dict, optional
|
|
202
243
|
keyword arguments to pass to standard DataLoader class.
|
|
203
244
|
"""
|
|
204
245
|
|
|
205
|
-
def __init__(
|
|
246
|
+
def __init__(
|
|
247
|
+
self, dataset, transforms=None, device=None, n_augmentation=0, **kwargs
|
|
248
|
+
):
|
|
206
249
|
if "collate_fn" in kwargs:
|
|
207
250
|
raise ValueError(
|
|
208
251
|
"collate_fn cannot be used in this context because it is used "
|
|
209
252
|
"to pass transform"
|
|
210
253
|
)
|
|
254
|
+
if n_augmentation < 0:
|
|
255
|
+
raise ValueError("n_augmentation must be a non-negative integer.")
|
|
211
256
|
if transforms is None or (
|
|
212
257
|
isinstance(transforms, list) and len(transforms) == 0
|
|
213
258
|
):
|
|
214
|
-
|
|
259
|
+
transform = IdentityTransform()
|
|
215
260
|
elif isinstance(transforms, (Transform, nn.Module)):
|
|
216
|
-
|
|
261
|
+
transform = transforms
|
|
217
262
|
elif isinstance(transforms, list):
|
|
218
|
-
|
|
263
|
+
transform = Compose(transforms)
|
|
219
264
|
else:
|
|
220
265
|
raise TypeError(
|
|
221
266
|
"transforms can be either a Transform object "
|
|
222
267
|
"or a list of Transform objects."
|
|
223
268
|
)
|
|
224
269
|
|
|
270
|
+
self.collated_tr = _AugmentationCollate(
|
|
271
|
+
transform, device=device, n_augmentation=n_augmentation
|
|
272
|
+
)
|
|
225
273
|
super().__init__(dataset, collate_fn=self.collated_tr, **kwargs)
|
|
@@ -18,7 +18,7 @@ from mne.filter import notch_filter
|
|
|
18
18
|
from scipy.interpolate import Rbf
|
|
19
19
|
from sklearn.utils import check_random_state
|
|
20
20
|
from torch.fft import fft, ifft
|
|
21
|
-
from torch.nn.functional import
|
|
21
|
+
from torch.nn.functional import pad
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def identity(X: torch.Tensor, y: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
|
@@ -253,25 +253,34 @@ def channels_dropout(
|
|
|
253
253
|
return X * mask.unsqueeze(-1), y
|
|
254
254
|
|
|
255
255
|
|
|
256
|
-
def
|
|
256
|
+
def _make_channel_permutation(
|
|
257
257
|
X: torch.Tensor, mask: torch.Tensor, random_state: int | np.random.Generator | None
|
|
258
258
|
) -> torch.Tensor:
|
|
259
|
+
"""Per-sample channel permutation that shuffles only the masked channels.
|
|
260
|
+
|
|
261
|
+
Returns a ``(batch, n_channels)`` index permutation: output channel ``i`` is
|
|
262
|
+
taken from input channel ``perm[b, i]``. Masked channels are permuted among
|
|
263
|
+
their own positions; unmasked channels keep their place.
|
|
264
|
+
|
|
265
|
+
The per-sample ``rng.permutation`` is kept (rather than a vectorized random
|
|
266
|
+
draw) so the output is bit-for-bit identical to the previous one-hot/matmul
|
|
267
|
+
implementation — vectorizing the draw would change seeded results. The win
|
|
268
|
+
is downstream: the caller gathers with this index instead of building
|
|
269
|
+
``(batch, n_channels, n_channels)`` one-hot matrices and a matmul.
|
|
270
|
+
"""
|
|
259
271
|
rng = check_random_state(random_state)
|
|
260
272
|
batch_size, n_channels, _ = X.shape
|
|
261
|
-
hard_mask = mask.round()
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
channels_to_shuffle = torch.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
273
|
+
hard_mask = mask.round().bool()
|
|
274
|
+
channels = torch.arange(n_channels, device=X.device)
|
|
275
|
+
perm = channels.repeat(batch_size, 1)
|
|
276
|
+
for b in range(batch_size):
|
|
277
|
+
channels_to_shuffle = channels[hard_mask[b]]
|
|
278
|
+
perm[b, channels_to_shuffle] = torch.tensor(
|
|
279
|
+
rng.permutation(channels_to_shuffle.cpu()),
|
|
280
|
+
device=X.device,
|
|
281
|
+
dtype=perm.dtype,
|
|
270
282
|
)
|
|
271
|
-
|
|
272
|
-
channels_permutation[channels_to_shuffle] = reordered_channels
|
|
273
|
-
batch_permutations[b, ...] = one_hot(channels_permutation)
|
|
274
|
-
return batch_permutations
|
|
283
|
+
return perm
|
|
275
284
|
|
|
276
285
|
|
|
277
286
|
def channels_shuffle(
|
|
@@ -314,8 +323,11 @@ def channels_shuffle(
|
|
|
314
323
|
if p_shuffle == 0:
|
|
315
324
|
return X, y
|
|
316
325
|
mask = _pick_channels_randomly(X, 1 - p_shuffle, random_state)
|
|
317
|
-
|
|
318
|
-
|
|
326
|
+
perm = _make_channel_permutation(X, mask, random_state)
|
|
327
|
+
# Gather channels (output[b, i] = X[b, perm[b, i]]) instead of building
|
|
328
|
+
# one-hot permutation matrices and a (batch, C, C) @ (batch, C, T) matmul.
|
|
329
|
+
perm = perm.unsqueeze(-1).expand(-1, -1, X.shape[-1])
|
|
330
|
+
return torch.gather(X, 1, perm), y
|
|
319
331
|
|
|
320
332
|
|
|
321
333
|
def gaussian_noise(
|
|
@@ -1188,19 +1200,21 @@ def mask_encoding(
|
|
|
1188
1200
|
32 (2024): 875-886.
|
|
1189
1201
|
"""
|
|
1190
1202
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1203
|
+
# All channels are zeroed at the same time positions, so build a single
|
|
1204
|
+
# (batch, n_times) time-mask and broadcast it over channels, vectorized
|
|
1205
|
+
# (no Python loop over batch * n_segments).
|
|
1206
|
+
device = X.device
|
|
1207
|
+
batch_indices = torch.arange(X.shape[0], device=device).repeat_interleave(
|
|
1208
|
+
n_segments * segment_length
|
|
1209
|
+
)
|
|
1210
|
+
seg_indices = (
|
|
1211
|
+
time_start.flatten().to(device)[:, None]
|
|
1212
|
+
+ torch.arange(segment_length, device=device)
|
|
1213
|
+
).flatten()
|
|
1214
|
+
time_mask = torch.zeros(X.shape[0], X.shape[-1], dtype=torch.bool, device=device)
|
|
1215
|
+
time_mask[batch_indices, seg_indices] = True
|
|
1216
|
+
|
|
1217
|
+
return X.masked_fill(time_mask.unsqueeze(1), 0), y
|
|
1204
1218
|
|
|
1205
1219
|
|
|
1206
1220
|
def channels_rereference(
|
|
@@ -10,6 +10,7 @@ from .base import (
|
|
|
10
10
|
from .bcicomp import BCICompetitionIVDataset4
|
|
11
11
|
from .bids import BIDSDataset, BIDSEpochsDataset
|
|
12
12
|
from .chb_mit import CHBMIT
|
|
13
|
+
from .collate import pad_channels_collate
|
|
13
14
|
from .mne import create_from_mne_epochs, create_from_mne_raw
|
|
14
15
|
from .moabb import BNCI2014_001, HGD, MOABBDataset
|
|
15
16
|
from .nmt import NMT
|
|
@@ -42,4 +43,5 @@ __all__ = [
|
|
|
42
43
|
"SleepPhysionetChallenge2018",
|
|
43
44
|
"create_from_X_y",
|
|
44
45
|
"BCICompetitionIVDataset4",
|
|
46
|
+
"pad_channels_collate",
|
|
45
47
|
]
|
|
@@ -192,6 +192,31 @@ def _channel_info(mne_obj):
|
|
|
192
192
|
return n_ch, type_str, sfreq
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
def _compute_ch_pos(info, targets_from):
|
|
196
|
+
"""Return ``(n_ch, 3)`` float32 electrode positions aligned to ``X``'s rows.
|
|
197
|
+
|
|
198
|
+
Positions are the MNE head-frame coordinates ``info["chs"][i]["loc"][:3]``
|
|
199
|
+
(meters) -- the same representation braindecode models already consume (see
|
|
200
|
+
e.g. ``signal_jepa``/``REVE``). When ``targets_from != "metadata"`` the
|
|
201
|
+
window's misc channels are dropped from ``X`` in ``__getitem__``, so the
|
|
202
|
+
matching misc rows are dropped here too. Emits a one-time ``UserWarning`` if
|
|
203
|
+
no channel carries a real position (no montage set).
|
|
204
|
+
"""
|
|
205
|
+
pos = np.asarray([ch["loc"][:3] for ch in info["chs"]], dtype="float32")
|
|
206
|
+
if targets_from != "metadata":
|
|
207
|
+
misc_mask = np.asarray(info.get_channel_types()) == "misc"
|
|
208
|
+
pos = pos[~misc_mask]
|
|
209
|
+
has_pos = np.isfinite(pos).all(axis=1) & np.any(pos != 0, axis=1)
|
|
210
|
+
if pos.size and not has_pos.any():
|
|
211
|
+
warnings.warn(
|
|
212
|
+
"Returning channel positions but no montage is set: all positions "
|
|
213
|
+
"are zero/non-finite. Call ``raw.set_montage(...)`` (or set a "
|
|
214
|
+
"montage during preprocessing) before enabling ``return_ch_pos``.",
|
|
215
|
+
UserWarning,
|
|
216
|
+
)
|
|
217
|
+
return pos
|
|
218
|
+
|
|
219
|
+
|
|
195
220
|
def _window_info(crop_inds, sfreq):
|
|
196
221
|
"""Extract window size from crop indices.
|
|
197
222
|
|
|
@@ -448,6 +473,10 @@ class RecordDataset(Dataset[tuple[np.ndarray, int | str, tuple[int, int, int]]])
|
|
|
448
473
|
):
|
|
449
474
|
self._description = _create_description(description)
|
|
450
475
|
self.transform = transform
|
|
476
|
+
# Opt-in: when True, ``__getitem__`` appends channel positions (n_ch, 3).
|
|
477
|
+
# Default off keeps the public ``(X, y, crop_inds)`` contract intact.
|
|
478
|
+
self.return_ch_pos = False
|
|
479
|
+
self._ch_pos = None
|
|
451
480
|
|
|
452
481
|
@abstractmethod
|
|
453
482
|
def __len__(self) -> int:
|
|
@@ -750,6 +779,20 @@ class EEGWindowsDataset(_ZarrMixin, RecordDataset):
|
|
|
750
779
|
@raw.setter
|
|
751
780
|
def raw(self, value):
|
|
752
781
|
self._raw = value
|
|
782
|
+
self._ch_pos = None # invalidate cached positions
|
|
783
|
+
|
|
784
|
+
@property
|
|
785
|
+
def ch_pos(self):
|
|
786
|
+
"""Electrode positions ``(n_ch, 3)`` (x, y, z) aligned to ``X``'s channels.
|
|
787
|
+
|
|
788
|
+
Cached MNE head-frame coordinates in meters. Rows match the channels
|
|
789
|
+
returned by :meth:`__getitem__` (misc target channels excluded when
|
|
790
|
+
``targets_from="channels"``). All-zero/NaN when no montage is set.
|
|
791
|
+
"""
|
|
792
|
+
if self._ch_pos is None:
|
|
793
|
+
info = self._raw.info if self._raw is not None else self._make_mne_info()
|
|
794
|
+
self._ch_pos = _compute_ch_pos(info, self.targets_from)
|
|
795
|
+
return self._ch_pos
|
|
753
796
|
|
|
754
797
|
def __getitem__(self, index: int):
|
|
755
798
|
"""Get a window and its target.
|
|
@@ -793,6 +836,8 @@ class EEGWindowsDataset(_ZarrMixin, RecordDataset):
|
|
|
793
836
|
y = X[misc_mask, :]
|
|
794
837
|
y = y.copy()
|
|
795
838
|
X = X[~misc_mask, :]
|
|
839
|
+
if self.return_ch_pos:
|
|
840
|
+
return X, y, crop_inds, self.ch_pos
|
|
796
841
|
return X, y, crop_inds
|
|
797
842
|
|
|
798
843
|
def __len__(self):
|
|
@@ -1016,6 +1061,24 @@ class WindowsDataset(_ZarrMixin, RecordDataset):
|
|
|
1016
1061
|
@windows.setter
|
|
1017
1062
|
def windows(self, value):
|
|
1018
1063
|
self._windows = value
|
|
1064
|
+
self._ch_pos = None # invalidate cached positions
|
|
1065
|
+
|
|
1066
|
+
@property
|
|
1067
|
+
def ch_pos(self):
|
|
1068
|
+
"""Electrode positions ``(n_ch, 3)`` (x, y, z) aligned to ``X``'s channels.
|
|
1069
|
+
|
|
1070
|
+
Cached MNE head-frame coordinates in meters. Rows match the channels
|
|
1071
|
+
returned by :meth:`__getitem__` (misc target channels excluded when
|
|
1072
|
+
``targets_from="channels"``). All-zero/NaN when no montage is set.
|
|
1073
|
+
"""
|
|
1074
|
+
if self._ch_pos is None:
|
|
1075
|
+
info = (
|
|
1076
|
+
self._windows.info
|
|
1077
|
+
if self._windows is not None
|
|
1078
|
+
else self._make_mne_info()
|
|
1079
|
+
)
|
|
1080
|
+
self._ch_pos = _compute_ch_pos(info, self.targets_from)
|
|
1081
|
+
return self._ch_pos
|
|
1019
1082
|
|
|
1020
1083
|
@staticmethod
|
|
1021
1084
|
def _can_use_fast_get_epoch_from_raw(epochs: mne.BaseEpochs) -> bool:
|
|
@@ -1068,6 +1131,8 @@ class WindowsDataset(_ZarrMixin, RecordDataset):
|
|
|
1068
1131
|
# necessary to cast as list to get list of three tensors from batch,
|
|
1069
1132
|
# otherwise get single 2d-tensor...
|
|
1070
1133
|
crop_inds = self.crop_inds[index].tolist()
|
|
1134
|
+
if self.return_ch_pos:
|
|
1135
|
+
return X, y, crop_inds, self.ch_pos
|
|
1071
1136
|
return X, y, crop_inds
|
|
1072
1137
|
|
|
1073
1138
|
def __len__(self) -> int:
|
|
@@ -1458,6 +1523,50 @@ class BaseConcatDataset(ConcatDataset, HubDatasetMixin, Generic[T]):
|
|
|
1458
1523
|
)
|
|
1459
1524
|
return self
|
|
1460
1525
|
|
|
1526
|
+
def set_return_ch_pos(self, value: bool = True) -> "BaseConcatDataset":
|
|
1527
|
+
"""Toggle returning electrode positions from every subdataset.
|
|
1528
|
+
|
|
1529
|
+
When enabled, each windowed subdataset's ``__getitem__`` appends a
|
|
1530
|
+
``(n_ch, 3)`` array of electrode positions (x, y, z), so a batch yields
|
|
1531
|
+
``(X, y, crop_inds, ch_pos)``. Heterogeneous (variable-channel)
|
|
1532
|
+
collections additionally need
|
|
1533
|
+
:func:`braindecode.datasets.pad_channels_collate` as the DataLoader
|
|
1534
|
+
``collate_fn``. Default-off: leaves the ``(X, y, crop_inds)`` contract
|
|
1535
|
+
untouched.
|
|
1536
|
+
|
|
1537
|
+
Parameters
|
|
1538
|
+
----------
|
|
1539
|
+
value : bool
|
|
1540
|
+
Whether to return channel positions (default ``True``).
|
|
1541
|
+
|
|
1542
|
+
Returns
|
|
1543
|
+
-------
|
|
1544
|
+
self : BaseConcatDataset
|
|
1545
|
+
"""
|
|
1546
|
+
for ds in self.datasets:
|
|
1547
|
+
ds.return_ch_pos = value
|
|
1548
|
+
return self
|
|
1549
|
+
|
|
1550
|
+
@property
|
|
1551
|
+
def ch_pos(self):
|
|
1552
|
+
"""Electrode positions ``(n_ch, 3)`` from the first recording.
|
|
1553
|
+
|
|
1554
|
+
Convenience accessor mirroring how the repr reports channel info "from
|
|
1555
|
+
first recording". For per-sample positions in a heterogeneous
|
|
1556
|
+
collection, enable :meth:`set_return_ch_pos` and read them from the
|
|
1557
|
+
batch instead.
|
|
1558
|
+
"""
|
|
1559
|
+
if not self.datasets:
|
|
1560
|
+
raise ValueError("Empty BaseConcatDataset has no channel positions.")
|
|
1561
|
+
first = self.datasets[0]
|
|
1562
|
+
if not hasattr(first, "ch_pos"):
|
|
1563
|
+
raise AttributeError(
|
|
1564
|
+
f"{type(first).__name__} does not expose channel positions; "
|
|
1565
|
+
"ch_pos is available on windowed datasets (EEGWindowsDataset / "
|
|
1566
|
+
"WindowsDataset)."
|
|
1567
|
+
)
|
|
1568
|
+
return first.ch_pos
|
|
1569
|
+
|
|
1461
1570
|
def _outdated_save(self, path, overwrite=False):
|
|
1462
1571
|
"""This is a copy of the old saving function, that had inconsistent.
|
|
1463
1572
|
|
|
@@ -327,6 +327,7 @@ class HubDatasetMixin:
|
|
|
327
327
|
|
|
328
328
|
n_recordings = len(self.datasets)
|
|
329
329
|
first_ds = self.datasets[0]
|
|
330
|
+
descriptions = [ds.description for ds in self.datasets]
|
|
330
331
|
|
|
331
332
|
# Get dataset-specific info based on type using registry
|
|
332
333
|
dataset_type = get_dataset_type(first_ds)
|
|
@@ -358,6 +359,9 @@ class HubDatasetMixin:
|
|
|
358
359
|
raise TypeError(f"Unsupported dataset type: {dataset_type}")
|
|
359
360
|
|
|
360
361
|
# Create README content and save
|
|
362
|
+
license_value, license_needs_update = _infer_license_from_descriptions(
|
|
363
|
+
descriptions
|
|
364
|
+
)
|
|
361
365
|
readme_content = _generate_readme_content(
|
|
362
366
|
format_info=format_info,
|
|
363
367
|
n_recordings=n_recordings,
|
|
@@ -366,6 +370,8 @@ class HubDatasetMixin:
|
|
|
366
370
|
data_type=data_type,
|
|
367
371
|
n_windows=n_windows,
|
|
368
372
|
total_duration=total_duration,
|
|
373
|
+
license=license_value,
|
|
374
|
+
license_needs_update=license_needs_update,
|
|
369
375
|
)
|
|
370
376
|
|
|
371
377
|
# Save README
|
|
@@ -466,6 +472,7 @@ class HubDatasetMixin:
|
|
|
466
472
|
token: Optional[str] = None,
|
|
467
473
|
cache_dir: Optional[Union[str, Path]] = None,
|
|
468
474
|
force_download: bool = False,
|
|
475
|
+
revision: str | None = None,
|
|
469
476
|
**kwargs,
|
|
470
477
|
):
|
|
471
478
|
"""
|
|
@@ -485,6 +492,9 @@ class HubDatasetMixin:
|
|
|
485
492
|
cache directory (~/.cache/huggingface/datasets).
|
|
486
493
|
force_download : bool, default=False
|
|
487
494
|
Whether to force re-download even if cached.
|
|
495
|
+
revision : str | None, default=None
|
|
496
|
+
Specific branch, tag, or commit to download. If None, uses the
|
|
497
|
+
repository's default revision.
|
|
488
498
|
**kwargs
|
|
489
499
|
Additional arguments (currently unused).
|
|
490
500
|
|
|
@@ -526,6 +536,7 @@ class HubDatasetMixin:
|
|
|
526
536
|
token=token,
|
|
527
537
|
cache_dir=cache_dir,
|
|
528
538
|
force_download=force_download,
|
|
539
|
+
revision=revision,
|
|
529
540
|
)
|
|
530
541
|
|
|
531
542
|
# Load format info
|
|
@@ -962,6 +973,8 @@ def _generate_readme_content(
|
|
|
962
973
|
n_windows: int,
|
|
963
974
|
total_duration: float | None = None,
|
|
964
975
|
format: str = "zarr",
|
|
976
|
+
license: str = "please-specify",
|
|
977
|
+
license_needs_update: bool = True,
|
|
965
978
|
):
|
|
966
979
|
"""Generate README.md content for a dataset uploaded to the Hub.
|
|
967
980
|
|
|
@@ -983,6 +996,10 @@ def _generate_readme_content(
|
|
|
983
996
|
Total duration in seconds across all recordings.
|
|
984
997
|
format : str
|
|
985
998
|
Storage format (default: "zarr").
|
|
999
|
+
license : str
|
|
1000
|
+
Dataset license string to write in YAML front matter.
|
|
1001
|
+
license_needs_update : bool
|
|
1002
|
+
Whether to append a reminder comment next to the license field.
|
|
986
1003
|
|
|
987
1004
|
Returns
|
|
988
1005
|
-------
|
|
@@ -998,6 +1015,10 @@ def _generate_readme_content(
|
|
|
998
1015
|
str(timedelta(seconds=int(total_duration))) if total_duration else "N/A"
|
|
999
1016
|
)
|
|
1000
1017
|
|
|
1018
|
+
license_line = f"license: {license}"
|
|
1019
|
+
if license_needs_update:
|
|
1020
|
+
license_line += " # Please update this field to reflect your dataset's license"
|
|
1021
|
+
|
|
1001
1022
|
return f"""---
|
|
1002
1023
|
tags:
|
|
1003
1024
|
- braindecode
|
|
@@ -1005,7 +1026,7 @@ tags:
|
|
|
1005
1026
|
- neuroscience
|
|
1006
1027
|
- brain-computer-interface
|
|
1007
1028
|
- deep-learning
|
|
1008
|
-
|
|
1029
|
+
{license_line}
|
|
1009
1030
|
---
|
|
1010
1031
|
|
|
1011
1032
|
# EEG Dataset
|
|
@@ -1101,3 +1122,27 @@ if hasattr(dataset.datasets[0], "bids_channels"):
|
|
|
1101
1122
|
|
|
1102
1123
|
*Created with [braindecode](https://braindecode.org)*
|
|
1103
1124
|
"""
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def _infer_license_from_descriptions(
|
|
1128
|
+
descriptions: list[pd.Series | None],
|
|
1129
|
+
) -> tuple[str, bool]:
|
|
1130
|
+
"""Infer a single dataset license from descriptions.
|
|
1131
|
+
|
|
1132
|
+
Returns inferred license and whether a manual update reminder is needed.
|
|
1133
|
+
"""
|
|
1134
|
+
licenses = set()
|
|
1135
|
+
for description in descriptions:
|
|
1136
|
+
if description is None:
|
|
1137
|
+
continue
|
|
1138
|
+
raw_license = description.get("license", None)
|
|
1139
|
+
if raw_license is None:
|
|
1140
|
+
continue
|
|
1141
|
+
normalized = str(raw_license).strip()
|
|
1142
|
+
if normalized and normalized.lower() != "nan":
|
|
1143
|
+
licenses.add(normalized)
|
|
1144
|
+
|
|
1145
|
+
if len(licenses) == 1:
|
|
1146
|
+
return next(iter(licenses)), False
|
|
1147
|
+
|
|
1148
|
+
return "please-specify", True
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Collate functions for braindecode datasets.
|
|
2
|
+
|
|
3
|
+
:func:`pad_channels_collate` makes a :class:`~braindecode.datasets.BaseConcatDataset`
|
|
4
|
+
that mixes recordings with *different channel sets* (heterogeneous montages)
|
|
5
|
+
batchable: it pads every sample's channels to the batch maximum and emits a
|
|
6
|
+
boolean channel mask. Pair it with ``set_return_ch_pos(True)`` so each sample
|
|
7
|
+
carries its electrode positions -- channel identity is then encoded by the
|
|
8
|
+
positions, not by row index, which is exactly what position-aware models
|
|
9
|
+
(e.g. ``REVE``) consume.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Authors: The braindecode developers
|
|
13
|
+
#
|
|
14
|
+
# License: BSD (3-clause)
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import torch
|
|
20
|
+
from torch.utils.data._utils.collate import default_collate
|
|
21
|
+
|
|
22
|
+
__all__ = ["pad_channels_collate"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def pad_channels_collate(batch, pad_value: float = 0.0, pos_pad_value: float = 0.0):
|
|
26
|
+
"""Collate variable-channel windows by padding channels to the batch maximum.
|
|
27
|
+
|
|
28
|
+
Each item of ``batch`` is the tuple returned by a windowed dataset's
|
|
29
|
+
``__getitem__``: either ``(X, y, crop_inds)`` or, when
|
|
30
|
+
``return_ch_pos=True`` (see
|
|
31
|
+
:meth:`braindecode.datasets.BaseConcatDataset.set_return_ch_pos`),
|
|
32
|
+
``(X, y, crop_inds, ch_pos)``. Signals with different numbers of channels
|
|
33
|
+
are zero-padded along the channel axis to ``max_ch`` (the largest channel
|
|
34
|
+
count in the batch) so they can be stacked into a single tensor, and a
|
|
35
|
+
boolean ``ch_mask`` marks the real (non-padded) channels.
|
|
36
|
+
|
|
37
|
+
The time dimension is assumed uniform (fixed-length windows); a
|
|
38
|
+
:class:`ValueError` is raised otherwise.
|
|
39
|
+
|
|
40
|
+
This makes a heterogeneous collection **batchable**. *Consuming* the result
|
|
41
|
+
(so a model ignores padded channels) requires a position/channel-aware
|
|
42
|
+
model; fixed-geometry models (e.g. ``ShallowFBCSPNet``) structurally need a
|
|
43
|
+
constant channel count and cannot use variable batches.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
batch : list of tuple
|
|
48
|
+
Samples from a windowed dataset (see above).
|
|
49
|
+
pad_value : float
|
|
50
|
+
Value used to pad the signal ``X`` channel rows (default ``0.0``).
|
|
51
|
+
pos_pad_value : float
|
|
52
|
+
Value used to pad the position ``ch_pos`` rows (default ``0.0``).
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
X : torch.Tensor
|
|
57
|
+
Padded signals, shape ``(batch, max_ch, n_times)``.
|
|
58
|
+
y : torch.Tensor
|
|
59
|
+
Targets, collated with ``default_collate``.
|
|
60
|
+
crop_inds : list
|
|
61
|
+
Crop indices, collated with ``default_collate``.
|
|
62
|
+
ch_pos : torch.Tensor
|
|
63
|
+
Only when the samples include positions. Padded positions, shape
|
|
64
|
+
``(batch, max_ch, 3)``.
|
|
65
|
+
ch_mask : torch.Tensor
|
|
66
|
+
Boolean mask, shape ``(batch, max_ch)``; ``True`` for real channels,
|
|
67
|
+
``False`` for padding.
|
|
68
|
+
"""
|
|
69
|
+
if len(batch) == 0:
|
|
70
|
+
raise ValueError("pad_channels_collate received an empty batch.")
|
|
71
|
+
|
|
72
|
+
has_pos = len(batch[0]) >= 4
|
|
73
|
+
Xs = [np.asarray(item[0]) for item in batch]
|
|
74
|
+
|
|
75
|
+
n_times = Xs[0].shape[1]
|
|
76
|
+
if any(x.shape[1] != n_times for x in Xs):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"pad_channels_collate only pads the channel axis; all windows must "
|
|
79
|
+
f"share the same number of time samples, got {sorted({x.shape[1] for x in Xs})}."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
bsz = len(batch)
|
|
83
|
+
max_ch = max(x.shape[0] for x in Xs)
|
|
84
|
+
|
|
85
|
+
X_out = torch.full((bsz, max_ch, n_times), pad_value, dtype=torch.float32)
|
|
86
|
+
ch_mask = torch.zeros((bsz, max_ch), dtype=torch.bool)
|
|
87
|
+
pos_out = (
|
|
88
|
+
torch.full((bsz, max_ch, 3), pos_pad_value, dtype=torch.float32)
|
|
89
|
+
if has_pos
|
|
90
|
+
else None
|
|
91
|
+
)
|
|
92
|
+
for i, (item, x) in enumerate(zip(batch, Xs)):
|
|
93
|
+
c = x.shape[0]
|
|
94
|
+
X_out[i, :c] = torch.as_tensor(x, dtype=torch.float32)
|
|
95
|
+
ch_mask[i, :c] = True
|
|
96
|
+
if pos_out is not None:
|
|
97
|
+
pos_out[i, :c] = torch.as_tensor(np.asarray(item[3]), dtype=torch.float32)
|
|
98
|
+
|
|
99
|
+
# y and crop_inds carry no channel axis -> standard collation.
|
|
100
|
+
y = default_collate([item[1] for item in batch])
|
|
101
|
+
crop_inds = default_collate([item[2] for item in batch])
|
|
102
|
+
|
|
103
|
+
if pos_out is not None:
|
|
104
|
+
return X_out, y, crop_inds, pos_out, ch_mask
|
|
105
|
+
return X_out, y, crop_inds, ch_mask
|
|
@@ -15,7 +15,6 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
import mne
|
|
17
17
|
import pandas as pd
|
|
18
|
-
from mne.utils import deprecated
|
|
19
18
|
|
|
20
19
|
from braindecode.util import _update_moabb_docstring
|
|
21
20
|
|
|
@@ -208,12 +207,3 @@ class HGD(MOABBDataset):
|
|
|
208
207
|
|
|
209
208
|
def __init__(self, subject_ids):
|
|
210
209
|
super().__init__("Schirrmeister2017", subject_ids=subject_ids)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
@deprecated(
|
|
214
|
-
"`BNCI2014001` was renamed to `BNCI2014_001` in v1.13; this alias will be removed in v1.14."
|
|
215
|
-
)
|
|
216
|
-
class BNCI2014001(BNCI2014_001):
|
|
217
|
-
"""Deprecated alias for BNCI2014001."""
|
|
218
|
-
|
|
219
|
-
pass
|