moecog 0.1.0__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.
moecog/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """MOECoG — Mother of All ECoG Benchmarks.
2
+
3
+ A MOABB-style benchmarking framework for electrocorticographic (ECoG) motor decoding.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """Analysis — results storage, statistics, and visualization."""
@@ -0,0 +1,8 @@
1
+ """ECoG dataset loaders."""
2
+
3
+ from .base import BaseECoGDataset, ElectrodeInfo
4
+
5
+ __all__ = [
6
+ "BaseECoGDataset",
7
+ "ElectrodeInfo",
8
+ ]
@@ -0,0 +1,161 @@
1
+ """Base class for all ECoG datasets."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+
9
+
10
+ @dataclass
11
+ class ElectrodeInfo:
12
+ """Patient-specific electrode metadata.
13
+
14
+ Parameters
15
+ ----------
16
+ positions : np.ndarray, shape (n_channels, 3)
17
+ Electrode coordinates (MNI or native space).
18
+ labels : list of str
19
+ Channel names.
20
+ hemisphere : list of str or None
21
+ Hemisphere per electrode ("L" or "R").
22
+ lobe : list of str or None
23
+ Anatomical lobe per electrode.
24
+ gyrus : list of str or None
25
+ Gyrus per electrode.
26
+ brodmann_area : list of int or None
27
+ Brodmann area per electrode.
28
+ grid_type : str
29
+ Electrode array type: "grid", "strip", or "depth".
30
+ spacing_mm : float
31
+ Inter-electrode distance in millimeters.
32
+ """
33
+
34
+ positions: np.ndarray
35
+ labels: list[str]
36
+ hemisphere: list[str] | None = None
37
+ lobe: list[str] | None = None
38
+ gyrus: list[str] | None = None
39
+ brodmann_area: list[int] | None = None
40
+ grid_type: str = "grid"
41
+ spacing_mm: float = 10.0
42
+
43
+
44
+ class BaseECoGDataset(ABC):
45
+ """Base class for all ECoG datasets.
46
+
47
+ Every concrete dataset must implement:
48
+ - ``_get_single_subject_data(subject)``
49
+ - ``data_path(subject)``
50
+ - ``get_electrode_info(subject)``
51
+
52
+ Parameters
53
+ ----------
54
+ subjects : list of int
55
+ Available subject IDs.
56
+ sessions_per_subject : int
57
+ Number of recording sessions per subject.
58
+ events : dict or None
59
+ Mapping of event names to integer codes. None for pure regression datasets.
60
+ code : str
61
+ Unique dataset identifier string.
62
+ paradigm : str
63
+ Paradigm type: "motor_regression", "motor_imagery", or "naturalistic".
64
+ interval : list of float or None
65
+ Epoch window [tmin, tmax] in seconds. None for continuous data.
66
+ sfreq : float
67
+ Sampling frequency in Hz.
68
+ doi : str or None
69
+ DOI of the associated publication.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ subjects: list[int],
75
+ sessions_per_subject: int,
76
+ events: dict[str, int] | None,
77
+ code: str,
78
+ paradigm: str,
79
+ interval: list[float] | None,
80
+ sfreq: float,
81
+ doi: str | None = None,
82
+ ):
83
+ self.subject_list = subjects
84
+ self.n_sessions = sessions_per_subject
85
+ self.event_id = events
86
+ self.code = code
87
+ self.paradigm_type = paradigm
88
+ self.interval = interval
89
+ self.sfreq = sfreq
90
+ self.doi = doi
91
+
92
+ def get_data(self, subjects=None):
93
+ """Load data for one or more subjects.
94
+
95
+ Parameters
96
+ ----------
97
+ subjects : list of int or None
98
+ Subject IDs to load. If None, loads all subjects.
99
+
100
+ Returns
101
+ -------
102
+ dict
103
+ Nested dict: ``{subject: {session: {run: mne.io.Raw}}}``.
104
+ """
105
+ subjects = subjects or self.subject_list
106
+ data = {}
107
+ for subject in subjects:
108
+ data[subject] = self._get_single_subject_data(subject)
109
+ return data
110
+
111
+ @abstractmethod
112
+ def _get_single_subject_data(self, subject):
113
+ """Load all sessions and runs for a single subject.
114
+
115
+ Parameters
116
+ ----------
117
+ subject : int
118
+ Subject identifier.
119
+
120
+ Returns
121
+ -------
122
+ dict
123
+ ``{session_id: {run_id: mne.io.Raw}}``.
124
+ """
125
+
126
+ @abstractmethod
127
+ def data_path(self, subject):
128
+ """Return local file paths for a subject's data, downloading if needed.
129
+
130
+ Parameters
131
+ ----------
132
+ subject : int
133
+ Subject identifier.
134
+
135
+ Returns
136
+ -------
137
+ list of Path
138
+ Local paths to the data files.
139
+ """
140
+
141
+ @abstractmethod
142
+ def get_electrode_info(self, subject):
143
+ """Return electrode metadata for a subject.
144
+
145
+ Parameters
146
+ ----------
147
+ subject : int
148
+ Subject identifier.
149
+
150
+ Returns
151
+ -------
152
+ ElectrodeInfo
153
+ Electrode positions and anatomical labels.
154
+ """
155
+
156
+ def __repr__(self):
157
+ return (
158
+ f"{self.__class__.__name__}(code={self.code!r}, "
159
+ f"subjects={len(self.subject_list)}, "
160
+ f"sessions={self.n_sessions})"
161
+ )
@@ -0,0 +1,7 @@
1
+ """Evaluation strategies for ECoG benchmarking."""
2
+
3
+ from .base import BaseEvaluation
4
+
5
+ __all__ = [
6
+ "BaseEvaluation",
7
+ ]
@@ -0,0 +1,111 @@
1
+ """Base evaluation class."""
2
+
3
+ import time
4
+ from abc import ABC, abstractmethod
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from sklearn.base import clone
9
+
10
+
11
+ class BaseEvaluation(ABC):
12
+ """Base class for all evaluation strategies.
13
+
14
+ Parameters
15
+ ----------
16
+ paradigm : BaseParadigm
17
+ Paradigm instance defining the task.
18
+ datasets : list of BaseECoGDataset
19
+ Datasets to evaluate on (filtered for paradigm compatibility).
20
+ n_splits : int
21
+ Number of cross-validation folds.
22
+ random_state : int
23
+ Random seed for reproducibility.
24
+ """
25
+
26
+ def __init__(self, paradigm, datasets, n_splits=5, random_state=42):
27
+ self.paradigm = paradigm
28
+ self.datasets = [d for d in datasets if paradigm.is_valid(d)]
29
+ self.n_splits = n_splits
30
+ self.random_state = random_state
31
+
32
+ def process(self, pipelines):
33
+ """Run all pipelines on all compatible datasets.
34
+
35
+ Parameters
36
+ ----------
37
+ pipelines : dict of str to sklearn estimator
38
+ Named pipelines to evaluate.
39
+
40
+ Returns
41
+ -------
42
+ pd.DataFrame
43
+ Results with columns: dataset, subject, session, pipeline,
44
+ score, metric, time, n_samples, n_channels, fold.
45
+ """
46
+ all_results = []
47
+ for dataset in self.datasets:
48
+ X, y, metadata = self.paradigm.get_data(dataset)
49
+ results = self._evaluate(dataset, X, y, metadata, pipelines)
50
+ all_results.extend(results)
51
+ return pd.DataFrame(all_results)
52
+
53
+ @abstractmethod
54
+ def _evaluate(self, dataset, X, y, metadata, pipelines):
55
+ """Implement the cross-validation strategy.
56
+
57
+ Returns
58
+ -------
59
+ list of dict
60
+ One dict per (pipeline, fold, subject) combination.
61
+ """
62
+
63
+ def _score_pipeline(self, pipeline, X_train, y_train, X_test, y_test):
64
+ """Fit a pipeline and compute scores."""
65
+ t0 = time.time()
66
+ clf = clone(pipeline)
67
+
68
+ if X_train.ndim == 3:
69
+ try:
70
+ clf.fit(X_train, y_train)
71
+ y_pred = clf.predict(X_test)
72
+ except ValueError:
73
+ X_tr = X_train.reshape(X_train.shape[0], -1)
74
+ X_te = X_test.reshape(X_test.shape[0], -1)
75
+ clf.fit(X_tr, y_train)
76
+ y_pred = clf.predict(X_te)
77
+ else:
78
+ clf.fit(X_train, y_train)
79
+ y_pred = clf.predict(X_test)
80
+
81
+ duration = time.time() - t0
82
+
83
+ scoring = self.paradigm.scoring()
84
+ if isinstance(scoring, str):
85
+ scores = {scoring: _compute_metric(scoring, y_test, y_pred)}
86
+ else:
87
+ scores = {
88
+ name: _compute_metric(name, y_test, y_pred) for name in scoring
89
+ }
90
+
91
+ return {"scores": scores, "time": duration}
92
+
93
+
94
+ def _compute_metric(metric_name, y_true, y_pred):
95
+ """Compute a single evaluation metric."""
96
+ from scipy.stats import pearsonr
97
+ from sklearn.metrics import accuracy_score, cohen_kappa_score, r2_score
98
+
99
+ if metric_name == "pearson_r":
100
+ if y_true.ndim == 1:
101
+ return pearsonr(y_true, y_pred)[0]
102
+ rs = [pearsonr(y_true[:, i], y_pred[:, i])[0] for i in range(y_true.shape[1])]
103
+ return float(np.mean(rs))
104
+ elif metric_name == "r2":
105
+ return r2_score(y_true, y_pred, multioutput="uniform_average")
106
+ elif metric_name == "accuracy":
107
+ return accuracy_score(y_true, y_pred)
108
+ elif metric_name == "kappa":
109
+ return cohen_kappa_score(y_true, y_pred)
110
+ else:
111
+ raise ValueError(f"Unknown metric: {metric_name}")
@@ -0,0 +1,9 @@
1
+ """ECoG paradigms — transform raw data into (X, y, metadata)."""
2
+
3
+ from .base import BaseClassificationParadigm, BaseParadigm, BaseRegressionParadigm
4
+
5
+ __all__ = [
6
+ "BaseParadigm",
7
+ "BaseClassificationParadigm",
8
+ "BaseRegressionParadigm",
9
+ ]
@@ -0,0 +1,209 @@
1
+ """Base paradigm classes for classification and regression tasks."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ import mne
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+
10
+ class BaseParadigm(ABC):
11
+ """Base class for all paradigms.
12
+
13
+ Transforms raw ECoG data from a dataset into ``(X, y, metadata)`` arrays
14
+ ready for scikit-learn pipelines.
15
+
16
+ Parameters
17
+ ----------
18
+ fmin : float
19
+ Lower bandpass frequency in Hz.
20
+ fmax : float
21
+ Upper bandpass frequency in Hz.
22
+ resample : float or None
23
+ Target sampling rate in Hz. None keeps the original rate.
24
+ channels : list of str or None
25
+ Channel names to select. None uses all ECoG channels.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ fmin: float = 0.5,
31
+ fmax: float = 200.0,
32
+ resample: float | None = None,
33
+ channels: list[str] | None = None,
34
+ ):
35
+ self.fmin = fmin
36
+ self.fmax = fmax
37
+ self.resample = resample
38
+ self.channels = channels
39
+
40
+ @abstractmethod
41
+ def get_data(self, dataset, subjects=None):
42
+ """Extract (X, y, metadata) from a dataset.
43
+
44
+ Returns
45
+ -------
46
+ X : np.ndarray
47
+ y : np.ndarray
48
+ metadata : pd.DataFrame
49
+ """
50
+
51
+ @abstractmethod
52
+ def is_valid(self, dataset) -> bool:
53
+ """Check if a dataset is compatible with this paradigm."""
54
+
55
+ @abstractmethod
56
+ def scoring(self):
57
+ """Return scoring metric name(s)."""
58
+
59
+ @property
60
+ @abstractmethod
61
+ def datasets(self) -> list:
62
+ """Return list of compatible dataset classes."""
63
+
64
+ def _preprocess_raw(self, raw):
65
+ """Apply bandpass filter, channel selection, and resampling."""
66
+ raw = raw.copy()
67
+ raw.filter(self.fmin, self.fmax, verbose=False)
68
+ if self.channels:
69
+ raw.pick_channels(self.channels)
70
+ if self.resample:
71
+ raw.resample(self.resample, verbose=False)
72
+ return raw
73
+
74
+
75
+ class BaseClassificationParadigm(BaseParadigm):
76
+ """Base paradigm for epoched classification tasks.
77
+
78
+ Parameters
79
+ ----------
80
+ tmin : float
81
+ Epoch start time relative to event onset (seconds).
82
+ tmax : float or None
83
+ Epoch end time relative to event onset (seconds).
84
+ baseline : tuple or None
85
+ Baseline correction window.
86
+ """
87
+
88
+ def __init__(self, tmin=0.0, tmax=None, baseline=None, **kwargs):
89
+ super().__init__(**kwargs)
90
+ self.tmin = tmin
91
+ self.tmax = tmax
92
+ self.baseline = baseline
93
+
94
+ @abstractmethod
95
+ def used_events(self, dataset):
96
+ """Return the event dict this paradigm uses from the dataset."""
97
+
98
+ def get_data(self, dataset, subjects=None):
99
+ subjects = subjects or dataset.subject_list
100
+ all_X, all_y, all_meta = [], [], []
101
+
102
+ for subject in subjects:
103
+ sub_data = dataset.get_data(subjects=[subject])
104
+ for session_id, runs in sub_data[subject].items():
105
+ for run_id, raw in runs.items():
106
+ raw = self._preprocess_raw(raw)
107
+ events, _ = mne.events_from_annotations(raw, verbose=False)
108
+ used = self.used_events(dataset)
109
+ tmax = self.tmax if self.tmax is not None else dataset.interval[1]
110
+ epochs = mne.Epochs(
111
+ raw,
112
+ events,
113
+ event_id=used,
114
+ tmin=self.tmin,
115
+ tmax=tmax,
116
+ baseline=self.baseline,
117
+ preload=True,
118
+ verbose=False,
119
+ )
120
+ X = epochs.get_data(copy=False)
121
+ y = np.array(
122
+ [
123
+ list(used.keys())[list(used.values()).index(e)]
124
+ for e in epochs.events[:, 2]
125
+ ]
126
+ )
127
+ meta = pd.DataFrame(
128
+ {"subject": subject, "session": session_id, "run": run_id},
129
+ index=range(len(y)),
130
+ )
131
+ all_X.append(X)
132
+ all_y.append(y)
133
+ all_meta.append(meta)
134
+
135
+ return (
136
+ np.concatenate(all_X),
137
+ np.concatenate(all_y),
138
+ pd.concat(all_meta).reset_index(drop=True),
139
+ )
140
+
141
+
142
+ class BaseRegressionParadigm(BaseParadigm):
143
+ """Base paradigm for continuous regression tasks.
144
+
145
+ Uses causal windowing: target ``y[i]`` is the value at the **end** of
146
+ window ``i``, ensuring the model only sees past neural data.
147
+
148
+ Parameters
149
+ ----------
150
+ window_size : float
151
+ Window length in seconds.
152
+ window_stride : float
153
+ Stride between windows in seconds.
154
+ """
155
+
156
+ def __init__(self, window_size=0.5, window_stride=0.05, **kwargs):
157
+ super().__init__(**kwargs)
158
+ self.window_size = window_size
159
+ self.window_stride = window_stride
160
+
161
+ @abstractmethod
162
+ def _extract_targets(self, raw, dataset):
163
+ """Extract continuous target signals from a Raw object.
164
+
165
+ Returns
166
+ -------
167
+ np.ndarray, shape (n_samples, n_targets)
168
+ """
169
+
170
+ def get_data(self, dataset, subjects=None):
171
+ subjects = subjects or dataset.subject_list
172
+ all_X, all_y, all_meta = [], [], []
173
+
174
+ for subject in subjects:
175
+ sub_data = dataset.get_data(subjects=[subject])
176
+ for session_id, runs in sub_data[subject].items():
177
+ for run_id, raw in runs.items():
178
+ raw = self._preprocess_raw(raw)
179
+ targets = self._extract_targets(raw, dataset)
180
+
181
+ sfreq = raw.info["sfreq"]
182
+ data = raw.pick(picks="ecog", exclude=[]).get_data()
183
+ win_samples = int(self.window_size * sfreq)
184
+ stride_samples = int(self.window_stride * sfreq)
185
+
186
+ n_windows = (data.shape[1] - win_samples) // stride_samples + 1
187
+ X = np.zeros((n_windows, data.shape[0], win_samples))
188
+ y = np.zeros((n_windows, targets.shape[1]))
189
+
190
+ for i in range(n_windows):
191
+ start = i * stride_samples
192
+ end = start + win_samples
193
+ X[i] = data[:, start:end]
194
+ # Causal: target at end of window
195
+ y[i] = targets[min(end, targets.shape[0] - 1)]
196
+
197
+ meta = pd.DataFrame(
198
+ {"subject": subject, "session": session_id, "run": run_id},
199
+ index=range(n_windows),
200
+ )
201
+ all_X.append(X)
202
+ all_y.append(y)
203
+ all_meta.append(meta)
204
+
205
+ return (
206
+ np.concatenate(all_X),
207
+ np.concatenate(all_y),
208
+ pd.concat(all_meta).reset_index(drop=True),
209
+ )
@@ -0,0 +1 @@
1
+ """Pipelines — feature extractors and decoders for ECoG."""
@@ -0,0 +1 @@
1
+ """Neural network decoders for ECoG signals."""
@@ -0,0 +1 @@
1
+ """Feature extraction transformers for ECoG signals."""
@@ -0,0 +1 @@
1
+ """Utilities — electrode mapping, signal processing helpers."""
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.4
2
+ Name: moecog
3
+ Version: 0.1.0
4
+ Summary: Mother of All ECoG Benchmarks — MOABB-style benchmarking for ECoG motor decoding
5
+ Author-email: Yifan Yu <yifanyu97@gmail.com>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/epyifany/MOECoG
8
+ Project-URL: Repository, https://github.com/epyifany/MOECoG
9
+ Project-URL: Issues, https://github.com/epyifany/MOECoG/issues
10
+ Keywords: ECoG,electrocorticography,BCI,brain-computer interface,motor decoding,benchmark,neuroscience,neural decoding
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: mne>=1.5
25
+ Requires-Dist: numpy>=1.24
26
+ Requires-Dist: scipy>=1.10
27
+ Requires-Dist: scikit-learn>=1.3
28
+ Requires-Dist: pandas>=2.0
29
+ Requires-Dist: h5py>=3.8
30
+ Requires-Dist: pooch>=1.7
31
+ Requires-Dist: tqdm>=4.65
32
+ Provides-Extra: deep
33
+ Requires-Dist: torch>=2.0; extra == "deep"
34
+ Requires-Dist: braindecode>=0.8; extra == "deep"
35
+ Provides-Extra: nwb
36
+ Requires-Dist: pynwb>=2.5; extra == "nwb"
37
+ Requires-Dist: dandi>=0.55; extra == "nwb"
38
+ Provides-Extra: viz
39
+ Requires-Dist: matplotlib>=3.7; extra == "viz"
40
+ Requires-Dist: seaborn>=0.12; extra == "viz"
41
+ Provides-Extra: anatomy
42
+ Requires-Dist: nilearn>=0.10; extra == "anatomy"
43
+ Requires-Dist: nibabel>=5.0; extra == "anatomy"
44
+ Provides-Extra: dev
45
+ Requires-Dist: moecog[anatomy,deep,nwb,viz]; extra == "dev"
46
+ Requires-Dist: pytest>=7.4; extra == "dev"
47
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
48
+ Requires-Dist: ruff>=0.1; extra == "dev"
49
+ Provides-Extra: all
50
+ Requires-Dist: moecog[anatomy,deep,nwb,viz]; extra == "all"
51
+ Dynamic: license-file
52
+
53
+ # MOECoG — Mother of All ECoG Benchmarks
54
+
55
+ A MOABB-style benchmarking framework for electrocorticographic (ECoG) motor decoding.
56
+
57
+ > *"ECoG isn't just the future — it's the testable present."*
58
+
59
+ ## Vision
60
+
61
+ [MOABB](https://github.com/NeuroTechX/moabb) transformed EEG-BCI research by making algorithm comparison reproducible and fair. **MOECoG does the same for ECoG motor decoding** — the signal modality at the critical intersection of clinical viability (long-term stability, lower surgical risk) and high-performance neural control (high-gamma access, mm-scale spatial resolution).
62
+
63
+ This project will be successful when we read in an abstract:
64
+
65
+ > *"...the proposed method obtained a correlation of 0.82 on MOECoG, outperforming the state of the art by 12%..."*
66
+
67
+ ## Why ECoG Needs Its Own Benchmark
68
+
69
+ | Property | EEG (MOABB) | ECoG (MOECoG) |
70
+ |---|---|---|
71
+ | Signal type | Scalp potentials | Cortical surface potentials |
72
+ | Key features | mu/beta ERD/ERS | High-gamma broadband (>70 Hz) + beta suppression |
73
+ | Spatial resolution | ~cm | ~mm |
74
+ | Electrode geometry | Standard montages (10-20) | Patient-specific grids/strips |
75
+ | Primary tasks | Classification (L/R imagery) | Both classification AND continuous regression |
76
+ | Cross-subject | Standard channel alignment | Requires anatomical registration |
77
+ | Noise profile | EMG, EOG artifacts | Epileptiform activity, referencing |
78
+
79
+ MOABB's paradigm/dataset/evaluation/pipeline abstraction is brilliant — but its assumptions (fixed channel montages, epoched classification, standard frequency bands) break down for ECoG.
80
+
81
+ ## Architecture
82
+
83
+ MOECoG follows MOABB's 4-concept design, adapted for ECoG:
84
+
85
+ ```
86
+ +--------------+ +---------------+ +---------------+ +---------------+
87
+ | Dataset |--->| Paradigm |--->| Evaluation |--->| Pipeline |
88
+ | | | | | | | |
89
+ | Raw ECoG + | | Motor Imagery | | WithinSubject | | Feature ext. |
90
+ | electrode | | Finger Flex | | CrossSession | | + Classifier |
91
+ | positions + | | Arm Reach | | Transfer | | or Regressor |
92
+ | anatomy | | Grasp Type | | | | |
93
+ +--------------+ +---------------+ +---------------+ +---------------+
94
+ ```
95
+
96
+ ### Key Differences from MOABB
97
+
98
+ - **Dual-task paradigms:** Classification (which finger?) AND regression (finger trajectory)
99
+ - **Anatomical electrode registration:** Patient-specific grids mapped to MNI/FreeSurfer atlas
100
+ - **Broadband feature extraction:** High-gamma (70-150 Hz), beta (13-30 Hz), phase-amplitude coupling
101
+ - **Continuous decoding metrics:** Correlation coefficient (r), R-squared, normalized MSE — not just accuracy
102
+ - **Naturalistic movement support:** Not just cued trials but free/spontaneous movements (AJILE12)
103
+
104
+ ## Included Datasets
105
+
106
+ ### Tier 1: Core Benchmark
107
+
108
+ Motor-specific, public, well-documented.
109
+
110
+ | ID | Dataset | Source | Subjects | Task | Channels | Modality |
111
+ |---|---|---|---|---|---|---|
112
+ | MillerFingerFlex | BCI Competition IV Dataset 4 | Miller & Schalk | 3 | Individual finger flexion (5-class regression) | 48-64 | ECoG grid |
113
+ | MillerLibrary | Stanford/Mayo ECoG Library | Miller 2019, *Nature Human Behaviour* | 34 | 16 experiments (motor, sensory, language, visual) | Varies | ECoG grid |
114
+ | AJILE12 | Annotated Joints in Long-term ECoG | Peterson et al. 2022, *Scientific Data* | 12 | Naturalistic wrist movements | >=64 | ECoG grid |
115
+
116
+ ### Tier 2: Extended Benchmark
117
+
118
+ | ID | Dataset | Source | Subjects | Task |
119
+ |---|---|---|---|---|
120
+ | BCITetraplegia | BCI and Tetraplegia (WIMAGINE) | Benabid/Costecalde et al. | 1 (chronic) | 4-class motor imagery, 2D cursor |
121
+ | GraspECoG | Natural grasp types | Various (Pistohl, Bleichner) | Varies | Grasp classification |
122
+ | MoveAgain | Blackrock/BrainGate ECoG subsets | If released publicly | Varies | Arm/hand movement |
123
+
124
+ ### Tier 3: Cross-Modality Comparison
125
+
126
+ | ID | Dataset | Why included |
127
+ |---|---|---|
128
+ | MOABB_MI | MOABB motor imagery EEG datasets | Direct EEG vs ECoG comparison on matched paradigms |
129
+
130
+ ## Paradigms
131
+
132
+ ### FingerFlexionRegression
133
+
134
+ - **Task:** Predict continuous finger flexion trajectories from ECoG
135
+ - **Metrics:** Pearson r, R-squared, NRMSE per finger
136
+ - **Datasets:** MillerFingerFlex, MillerLibrary (motor subset)
137
+ - **Baseline:** BCI Competition IV Dataset 4 leaderboard
138
+
139
+ ### MotorImageryClassification
140
+
141
+ - **Task:** Classify imagined/attempted movements (L/R hand, feet, tongue, etc.)
142
+ - **Metrics:** Accuracy, ROC-AUC, Cohen's kappa
143
+ - **Datasets:** MillerLibrary (motor imagery subset), BCITetraplegia
144
+
145
+ ### NaturalisticReachDecoding
146
+
147
+ - **Task:** Decode wrist movement onset and trajectory from unconstrained behavior
148
+ - **Metrics:** Event detection F1, trajectory r, latency
149
+ - **Datasets:** AJILE12
150
+
151
+ ### GraspClassification
152
+
153
+ - **Task:** Classify grasp types or hand gestures from sensorimotor ECoG
154
+ - **Metrics:** Accuracy, confusion matrix analysis
155
+ - **Datasets:** GraspECoG, MillerLibrary (gesture subset)
156
+
157
+ ## Evaluation Strategies
158
+
159
+ | Strategy | Description | Use Case |
160
+ |---|---|---|
161
+ | WithinSubjectCV | K-fold within single subject | Standard single-patient decoding |
162
+ | CrossSessionEval | Train on session A, test on session B | Stability / recalibration assessment |
163
+ | CrossSubjectTransfer | Leave-one-subject-out (with atlas projection) | Generalization / zero-shot transfer |
164
+ | TemporalStabilityEval | Chronological split (early to late) | Long-term signal stability |
165
+
166
+ ## Baseline Pipelines
167
+
168
+ ### Feature Extraction
169
+
170
+ - **LogBandPower** — Log power in canonical bands (mu, beta, low-gamma, high-gamma)
171
+ - **BroadbandChange** — Miller's broadband spectral change method
172
+ - **PAC** — Phase-amplitude coupling (theta/gamma, beta/high-gamma)
173
+ - **CSP_ECoG** — Common Spatial Patterns adapted for patient-specific grids
174
+ - **TimeFrequency** — Continuous wavelet / multitaper spectrograms
175
+
176
+ ### Decoders
177
+
178
+ - **LDA / SVM / LogisticRegression** — Classical classifiers
179
+ - **Ridge / Kalman** — Linear regressors for trajectory decoding
180
+ - **ECoGNet** — Lightweight CNN for ECoG (braindecode-compatible)
181
+ - **FingerFlex** — Convolutional encoder-decoder (Lomtev et al.)
182
+ - **HTNet** — Transfer learning across subjects via Hilbert transform
183
+
184
+ ## Quick Start
185
+
186
+ ```python
187
+ import moecog
188
+ from moecog.datasets import MillerFingerFlex
189
+ from moecog.paradigms import FingerFlexionRegression
190
+ from moecog.evaluations import WithinSubjectCV
191
+ from moecog.pipelines.features import LogBandPower
192
+ from sklearn.linear_model import Ridge
193
+ from sklearn.pipeline import make_pipeline
194
+
195
+ # Define pipeline
196
+ pipelines = {
197
+ "LogBandPower+Ridge": make_pipeline(LogBandPower(), Ridge(alpha=1.0))
198
+ }
199
+
200
+ # Load data
201
+ dataset = MillerFingerFlex()
202
+ paradigm = FingerFlexionRegression(fmin=1, fmax=150)
203
+ evaluation = WithinSubjectCV(paradigm=paradigm, datasets=[dataset], n_splits=5)
204
+
205
+ # Run benchmark
206
+ results = evaluation.process(pipelines)
207
+ print(results.groupby("pipeline")["score"].mean())
208
+ ```
209
+
210
+ ## Installation
211
+
212
+ ```bash
213
+ pip install moecog
214
+ ```
215
+
216
+ Or for development:
217
+
218
+ ```bash
219
+ git clone https://github.com/epyifany/MOECoG.git
220
+ cd MOECoG
221
+ pip install -e ".[dev]"
222
+ ```
223
+
224
+ ### Dependencies
225
+
226
+ **Core:**
227
+ - `mne >= 1.5` — ECoG signal handling, coordinate transforms
228
+ - `numpy`, `scipy`, `scikit-learn` — Core ML
229
+ - `pandas` — Results management
230
+ - `h5py` — Persistent results storage
231
+ - `pooch` — Robust data downloading
232
+
233
+ **Optional:**
234
+ - `torch`, `braindecode` — Deep learning baselines (`pip install moecog[deep]`)
235
+ - `pynwb`, `dandi` — NWB data access for AJILE12 (`pip install moecog[nwb]`)
236
+ - `nilearn`, `nibabel` — Anatomical registration (`pip install moecog[anatomy]`)
237
+ - `matplotlib`, `seaborn` — Visualization (`pip install moecog[viz]`)
238
+
239
+ ## Citation
240
+
241
+ If you use MOECoG in your research, please cite:
242
+
243
+ ```bibtex
244
+ @software{moecog2025,
245
+ title = {MOECoG: Mother of All ECoG Benchmarks},
246
+ author = {Yu, Yifan},
247
+ year = {2025},
248
+ url = {https://github.com/epyifany/MOECoG}
249
+ }
250
+ ```
251
+
252
+ And the foundational datasets:
253
+
254
+ - Miller, K.J. "A library of human electrocorticographic data and analyses." *Nature Human Behaviour* 3(11), 1225-1235 (2019).
255
+ - Peterson, S.M. et al. "AJILE12: Long-term naturalistic human intracranial neural recordings and pose." *Scientific Data* 9, 184 (2022).
256
+ - Schalk, G. et al. BCI Competition IV Dataset 4.
257
+
258
+ ## License
259
+
260
+ BSD-3-Clause (matching MOABB)
@@ -0,0 +1,17 @@
1
+ moecog/__init__.py,sha256=e5F7QRudYbcoibqVlP46le-BOnHcqjH8inBYduFXtIs,159
2
+ moecog/analysis/__init__.py,sha256=ExR9A1yjIdZMZkpF7R6nvDNJEIBy7l1oLy9M_rqZc7w,67
3
+ moecog/datasets/__init__.py,sha256=66_Y1EAyZq95-PqzxgnxQ8xhXVgg1v3ClMp5aRUuBZo,137
4
+ moecog/datasets/base.py,sha256=krZ64AwYqH8hSObZJXuyKu9WK8NO146r5RojyhCjT3I,4328
5
+ moecog/evaluations/__init__.py,sha256=iZOCMX5TwwM1Ufub-VXTa7pOK42fZiRJ49_NXdoF9bc,122
6
+ moecog/evaluations/base.py,sha256=t2pjnQC6tGwv3180WCW6CEl2oD4QN6Q9xl2QVgteyf4,3584
7
+ moecog/paradigms/__init__.py,sha256=prOaV6pBOm-TxmCgZHi95vBTGRWRi-xEb03ywQk65XI,250
8
+ moecog/paradigms/base.py,sha256=9POMm55YuVBOdAFc5am23Bt_3OEw_hsnzHX4SUizEiI,6907
9
+ moecog/pipelines/__init__.py,sha256=pD1TEeHjiqpM7W54OTHwfhnH1prFokliRpeGjg8mCs8,62
10
+ moecog/pipelines/decoders/__init__.py,sha256=7x__BdnPIPSCUVmAmsT39pzQr139zqu2pAJS_8sngHo,48
11
+ moecog/pipelines/features/__init__.py,sha256=oskX5GdeEWLQX5f9nOq_tdK98QbUNMBDNSUeqeuXTkY,56
12
+ moecog/utils/__init__.py,sha256=6qo244StRvNH_Rc8rUqLPGzU7FZc5hkITntUkC27xNc,66
13
+ moecog-0.1.0.dist-info/licenses/LICENSE,sha256=tjJri2nMHvBODozIWCOPxUaIkCMC-nEAAn-FKy1YQpQ,1495
14
+ moecog-0.1.0.dist-info/METADATA,sha256=QC6KtNGzBTJ_-to2a5sRGcyKQHDIL7Sqnk639wkVvYg,10446
15
+ moecog-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
16
+ moecog-0.1.0.dist-info/top_level.txt,sha256=vh-OEq_cEuJ4LlBwz8NC_PNJprGmFqFi5c3VVtdqtaM,7
17
+ moecog-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Yifan Yu
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ moecog