mini-mne 0.1.0__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.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.3
2
+ Name: mini-mne
3
+ Version: 0.1.0
4
+ Summary: A simplified MNE-Python wrapper for teaching EEG analysis
5
+ Author: Ina Bornkessel-Schlesewsky
6
+ License: BSD-3-Clause
7
+ Requires-Dist: autoreject>=0.4.3
8
+ Requires-Dist: mne>=1.12.0
9
+ Requires-Dist: mne-bids>=0.18.0
10
+ Requires-Dist: mpld3>=0.5.12
11
+ Requires-Dist: numpy>=1.21
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+
15
+ # mini-mne
16
+
17
+ A simplified wrapper around [MNE-Python](https://mne.tools) for teaching EEG data analysis. mini-mne exposes only the essential classes and methods, reducing the cognitive load for learners who are new to EEG preprocessing and analysis.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install --upgrade mini-mne
23
+ ```
24
+
25
+ Requires Python ≥ 3.11.
26
+
27
+ ## Usage
28
+
29
+ mini-mne follows the standard EEG analysis pipeline: load continuous data → epoch around events → compute ERPs.
30
+
31
+ ```python
32
+ from mini_mne import SimpleRaw, SimpleEpoch, SimpleEvoked, plot_evokeds
33
+
34
+ # Load data (downloads the MNE ERP-CORE example dataset if no path is given)
35
+ raw = SimpleRaw()
36
+
37
+ # Inspect the data
38
+ print(raw)
39
+ print(raw.sampling_frequency) # Hz
40
+ print(raw.channel_names)
41
+ print(raw.duration) # seconds
42
+
43
+ # Preprocess
44
+ raw = raw.filter(l_freq=0.1, h_freq=40.0)
45
+ raw = raw.re_reference()
46
+
47
+ # Extract events from annotations
48
+ events, event_id = raw.get_events()
49
+
50
+ # Epoch the data
51
+ epochs = SimpleEpoch(raw, events, event_id,
52
+ tmin=-0.2, tmax=0.8,
53
+ baseline=(-0.2, 0),
54
+ reject={"eeg": 100e-6})
55
+
56
+ print(epochs)
57
+ print(epochs.event_types)
58
+ print(epochs.dropped_epochs)
59
+
60
+ # Compute ERPs
61
+ evokeds = epochs.average(by_event_type=True)
62
+
63
+ # Plot
64
+ plot_evokeds(evokeds)
65
+ ```
66
+
67
+ ## Classes
68
+
69
+ | Class | Wraps | Purpose |
70
+ |---|---|---|
71
+ | `SimpleRaw` | `mne.io.Raw` | Continuous EEG data |
72
+ | `SimpleEpoch` | `mne.Epochs` | Segmented data around events |
73
+ | `SimpleEvoked` | `mne.Evoked` | Averaged ERP |
74
+
75
+ ## Licence
76
+
77
+ mini-mne is licenced under the [BSD-3-Clause licence](LICENSE).
78
+
79
+ ## Acknowledgements
80
+
81
+ mini-mne is built on top of [MNE-Python](https://mne.tools). The default example dataset is from the [ERP-CORE dataset](https://osf.io/thsqg/) (Kappenman et al., 2021).
@@ -0,0 +1,67 @@
1
+ # mini-mne
2
+
3
+ A simplified wrapper around [MNE-Python](https://mne.tools) for teaching EEG data analysis. mini-mne exposes only the essential classes and methods, reducing the cognitive load for learners who are new to EEG preprocessing and analysis.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install --upgrade mini-mne
9
+ ```
10
+
11
+ Requires Python ≥ 3.11.
12
+
13
+ ## Usage
14
+
15
+ mini-mne follows the standard EEG analysis pipeline: load continuous data → epoch around events → compute ERPs.
16
+
17
+ ```python
18
+ from mini_mne import SimpleRaw, SimpleEpoch, SimpleEvoked, plot_evokeds
19
+
20
+ # Load data (downloads the MNE ERP-CORE example dataset if no path is given)
21
+ raw = SimpleRaw()
22
+
23
+ # Inspect the data
24
+ print(raw)
25
+ print(raw.sampling_frequency) # Hz
26
+ print(raw.channel_names)
27
+ print(raw.duration) # seconds
28
+
29
+ # Preprocess
30
+ raw = raw.filter(l_freq=0.1, h_freq=40.0)
31
+ raw = raw.re_reference()
32
+
33
+ # Extract events from annotations
34
+ events, event_id = raw.get_events()
35
+
36
+ # Epoch the data
37
+ epochs = SimpleEpoch(raw, events, event_id,
38
+ tmin=-0.2, tmax=0.8,
39
+ baseline=(-0.2, 0),
40
+ reject={"eeg": 100e-6})
41
+
42
+ print(epochs)
43
+ print(epochs.event_types)
44
+ print(epochs.dropped_epochs)
45
+
46
+ # Compute ERPs
47
+ evokeds = epochs.average(by_event_type=True)
48
+
49
+ # Plot
50
+ plot_evokeds(evokeds)
51
+ ```
52
+
53
+ ## Classes
54
+
55
+ | Class | Wraps | Purpose |
56
+ |---|---|---|
57
+ | `SimpleRaw` | `mne.io.Raw` | Continuous EEG data |
58
+ | `SimpleEpoch` | `mne.Epochs` | Segmented data around events |
59
+ | `SimpleEvoked` | `mne.Evoked` | Averaged ERP |
60
+
61
+ ## Licence
62
+
63
+ mini-mne is licenced under the [BSD-3-Clause licence](LICENSE).
64
+
65
+ ## Acknowledgements
66
+
67
+ mini-mne is built on top of [MNE-Python](https://mne.tools). The default example dataset is from the [ERP-CORE dataset](https://osf.io/thsqg/) (Kappenman et al., 2021).
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11,<0.12"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "mini-mne"
7
+ version = "0.1.0"
8
+ description = "A simplified MNE-Python wrapper for teaching EEG analysis"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "BSD-3-Clause" }
12
+ authors = [{ name = "Ina Bornkessel-Schlesewsky" }]
13
+ dependencies = [
14
+ "autoreject>=0.4.3",
15
+ "mne>=1.12.0",
16
+ "mne-bids>=0.18.0",
17
+ "mpld3>=0.5.12",
18
+ "numpy>=1.21",
19
+ ]
20
+
21
+ [dependency-groups]
22
+ dev = ["pytest>=8.0"]
23
+
24
+ [tool.pytest.ini_options]
@@ -0,0 +1,3 @@
1
+ from .raw import SimpleRaw
2
+ from .epochs import SimpleEpoch
3
+ from .evoked import SimpleEvoked, plot_evokeds
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import mne
4
+ from .raw import SimpleRaw
5
+ from .evoked import SimpleEvoked
6
+
7
+ class SimpleEpoch:
8
+ """
9
+ A simplified wrapper around MNE's Epoch object for teaching purposes.
10
+ Exposes only essential attributes and methods.
11
+ """
12
+ def __init__(self,
13
+ raw: SimpleRaw,
14
+ events=None,
15
+ event_id=None,
16
+ tmin: float = -0.2,
17
+ tmax: float = 1.0,
18
+ baseline: tuple | None = None,
19
+ reject: dict | None = None):
20
+ """
21
+ Parameters
22
+ ----------
23
+ raw : SimpleRaw
24
+ The continuous EEG data to epoch.
25
+ events : np.ndarray, shape (n_events, 3)
26
+ Event array as returned by SimpleRaw.get_events().
27
+ event_id : dict
28
+ Mapping of event labels to integer IDs, as returned by
29
+ SimpleRaw.get_events().
30
+ tmin : float
31
+ Start of the epoch in seconds, relative to the event onset.
32
+ tmax : float
33
+ End of the epoch in seconds, relative to the event onset.
34
+ baseline : tuple or None
35
+ Time interval for baseline correction as (tmin, tmax) in seconds.
36
+ None applies no baseline correction.
37
+ reject : dict or None
38
+ Peak-to-peak amplitude rejection thresholds, e.g.
39
+ {'eeg': 100e-6} rejects epochs exceeding 100 µV.
40
+ None applies no rejection.
41
+ """
42
+ base_raw = raw._raw
43
+
44
+ epochs = mne.Epochs(base_raw, events, event_id, tmin, tmax,
45
+ baseline=baseline, reject=reject, preload=True)
46
+ self._epochs = epochs
47
+
48
+ @property
49
+ def event_types(self) -> list:
50
+ """Get the list of event type labels."""
51
+ return list(self._epochs.event_id.keys())
52
+
53
+ @property
54
+ def time_range(self) -> tuple[float, float]:
55
+ """Get the epoch time range in seconds as (tmin, tmax)."""
56
+ return self._epochs.tmin, self._epochs.tmax
57
+
58
+ @property
59
+ def dropped_epochs(self) -> int:
60
+ """Get the number of dropped epochs."""
61
+ return len([d for d in self._epochs.drop_log if len(d) > 0])
62
+
63
+ def __str__(self):
64
+ event_labels = ", ".join(self.event_types)
65
+ tmin, tmax = self.time_range
66
+ return (f"Epoch object | {len(self)} epochs; "
67
+ f"event types: {event_labels}; "
68
+ f"time range: {tmin}–{tmax} s; "
69
+ f"{self.dropped_epochs} dropped")
70
+
71
+ def __repr__(self):
72
+ return repr(self._epochs)
73
+
74
+ def __len__(self):
75
+ return len(self._epochs)
76
+
77
+ def __iter__(self):
78
+ return iter(self._epochs)
79
+
80
+ def __dir__(self):
81
+ return ['event_types', 'time_range', 'dropped_epochs', 'average', 'plot']
82
+
83
+ @classmethod
84
+ def _from_epochs(cls, epochs: mne.Epochs) -> "SimpleEpoch":
85
+ """Wrap an existing mne.Epochs object without going through __init__."""
86
+ instance = cls.__new__(cls)
87
+ instance._epochs = epochs
88
+ return instance
89
+
90
+ def __getitem__(self, item) -> "SimpleEpoch":
91
+ return SimpleEpoch._from_epochs(self._epochs[item])
92
+
93
+ def average(self, by_event_type: bool = False) -> SimpleEvoked | list[SimpleEvoked]:
94
+ """Compute the ERP by averaging across epochs.
95
+
96
+ Parameters
97
+ ----------
98
+ by_event_type : bool
99
+ If False (default), average across all epochs regardless of
100
+ condition, returning a single SimpleEvoked. If True, average
101
+ separately per condition, returning a list of SimpleEvoked
102
+ (one per event type).
103
+
104
+ Returns
105
+ -------
106
+ SimpleEvoked or list of SimpleEvoked
107
+ """
108
+ evoked = self._epochs.average(by_event_type=by_event_type)
109
+ if isinstance(evoked, list):
110
+ return [SimpleEvoked(e) for e in evoked]
111
+ else:
112
+ return SimpleEvoked(evoked)
113
+
114
+ def plot(self, *args, **kwargs):
115
+ return self._epochs.plot(*args, **kwargs)
116
+
117
+
118
+
@@ -0,0 +1,79 @@
1
+ import mne
2
+
3
+
4
+ class SimpleEvoked:
5
+ """
6
+ A simplified wrapper around MNE's Evoked object for teaching purposes.
7
+ Exposes only essential attributes and methods.
8
+ """
9
+ def __init__(self, evoked: mne.Evoked):
10
+ """
11
+ Parameters
12
+ ----------
13
+ evoked : mne.Evoked
14
+ The underlying MNE Evoked object, typically obtained via
15
+ SimpleEpoch.average().
16
+ """
17
+ self._evoked = evoked
18
+
19
+ @property
20
+ def comment(self) -> str:
21
+ """Get the event type label."""
22
+ return self._evoked.comment
23
+
24
+ @property
25
+ def nave(self) -> int:
26
+ """Get the number of averaged epochs."""
27
+ return self._evoked.nave
28
+
29
+ @property
30
+ def time_range(self) -> tuple[float, float]:
31
+ """Get the time range in seconds as (tmin, tmax)."""
32
+ return self._evoked.times[0], self._evoked.times[-1]
33
+
34
+ @property
35
+ def channel_names(self) -> list:
36
+ """Get the list of channel names."""
37
+ return self._evoked.ch_names
38
+
39
+ def __str__(self):
40
+ tmin, tmax = self.time_range
41
+ return (f"Evoked object | condition: {self.comment}; "
42
+ f"{self.nave} epochs averaged; "
43
+ f"time range: {tmin:.3f}–{tmax:.3f} s; "
44
+ f"{len(self.channel_names)} channels")
45
+
46
+ def __repr__(self):
47
+ return repr(self._evoked)
48
+
49
+ def __dir__(self):
50
+ return ['comment', 'nave', 'time_range', 'channel_names', 'plot']
51
+
52
+ def plot(self, *args, **kwargs):
53
+ return self._evoked.plot(*args, **kwargs)
54
+
55
+
56
+ def _unwrap(e: "SimpleEvoked | mne.Evoked") -> mne.Evoked:
57
+ return e._evoked if isinstance(e, SimpleEvoked) else e
58
+
59
+
60
+ def plot_evokeds(
61
+ evokeds: "SimpleEvoked | mne.Evoked | list | dict",
62
+ *args,
63
+ **kwargs
64
+ ):
65
+ """Plot and compare evoked responses.
66
+
67
+ Parameters
68
+ ----------
69
+ evokeds : SimpleEvoked, mne.Evoked, list, or dict of str -> evoked
70
+ The evoked response(s) to plot. Accepts SimpleEvoked wrappers or
71
+ raw mne.Evoked objects interchangeably.
72
+ """
73
+ if isinstance(evokeds, (SimpleEvoked, mne.Evoked)):
74
+ unwrapped = _unwrap(evokeds)
75
+ elif isinstance(evokeds, dict):
76
+ unwrapped = {label: _unwrap(e) for label, e in evokeds.items()}
77
+ else:
78
+ unwrapped = [_unwrap(e) for e in evokeds]
79
+ return mne.viz.plot_compare_evokeds(unwrapped, *args, **kwargs)
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ import mne
4
+ import numpy as np
5
+ from pathlib import Path
6
+ from .raw_io import DefaultImportRawHandler, FileImportRawHandler
7
+
8
+ class SimpleRaw:
9
+ """
10
+ A simplified wrapper around MNE's Raw object for teaching purposes.
11
+ Exposes only essential attributes and methods.
12
+ """
13
+ def __init__(self, raw_input: mne.io.BaseRaw | Path | None = None):
14
+ """
15
+ Parameters
16
+ ----------
17
+ raw_input : mne.io.BaseRaw, Path, or None
18
+ The data source. Pass an MNE Raw object to wrap it directly, a
19
+ Path to load from a BrainVision file, or None to download and
20
+ use the MNE ERP-CORE example dataset.
21
+ """
22
+ # If no raw object and no path is provided, download and use an MNE example file instead
23
+ if raw_input is None:
24
+ handler = DefaultImportRawHandler()
25
+ self._raw = handler.load_raw()
26
+ elif isinstance(raw_input, mne.io.BaseRaw):
27
+ self._raw = raw_input
28
+ else:
29
+ file_path = Path(raw_input)
30
+ handler = FileImportRawHandler(file_path)
31
+ self._raw = handler.load_raw()
32
+
33
+ # Attributes (read-only properties)
34
+ @property
35
+ def sampling_frequency(self) -> float:
36
+ """Get the sampling frequency in Hz."""
37
+ return self._raw.info['sfreq']
38
+
39
+ @property
40
+ def channel_names(self) -> list:
41
+ """Get the list of channel names."""
42
+ return self._raw.ch_names
43
+
44
+ @property
45
+ def duration(self) -> float:
46
+ """Get the duration in seconds."""
47
+ return self._raw.duration
48
+
49
+ @property
50
+ def frequency_range(self) -> tuple[float, float]:
51
+ """Get the frequency range in Hz: tuple of lowpass and highpass frequencies. """
52
+ return self._raw.info['highpass'], self._raw.info['lowpass']
53
+
54
+ @property
55
+ def custom_reference(self) -> bool:
56
+ """Has a custom reference been applied"""
57
+ return bool(self._raw.info['custom_ref_applied'])
58
+
59
+ def __str__(self):
60
+ return f"Raw (continuous) EEG object | length: {self._raw.duration} seconds; {len(self._raw.ch_names)} channels"
61
+
62
+ def __repr__(self):
63
+ return f"MiniRaw(raw={self._raw})"
64
+
65
+ def __dir__(self):
66
+ return ['sampling_frequency', 'channel_names', 'duration', 'custom_reference',
67
+ 'plot', 'plot_psd', 'filter', 're_reference', 'get_events']
68
+
69
+ # Methods
70
+ def plot(self, *args, **kwargs):
71
+ return self._raw.plot(*args, **kwargs)
72
+
73
+ def plot_psd(self, fmin: float = 0, fmax: float = np.inf,
74
+ tmin: float | None = None, tmax: float | None = None, **kwargs):
75
+ """Plot the power spectral density (PSD) using Welch's method.
76
+
77
+ Parameters
78
+ ----------
79
+ fmin : float
80
+ Lower frequency bound in Hz.
81
+ fmax : float
82
+ Upper frequency bound in Hz.
83
+ tmin : float or None
84
+ Start of the time window in seconds. None uses the start of the recording.
85
+ tmax : float or None
86
+ End of the time window in seconds. None uses the end of the recording.
87
+ **kwargs
88
+ Additional arguments passed to the MNE PSD plot function.
89
+ """
90
+ return self._raw.compute_psd(method='welch', fmin=fmin, fmax=fmax,
91
+ tmin=tmin, tmax=tmax).plot(**kwargs)
92
+
93
+ def filter(self, l_freq: float, h_freq: float) -> SimpleRaw:
94
+ """
95
+ Apply a bandpass filter to the data.
96
+
97
+ Parameters
98
+ ----------
99
+ l_freq : float
100
+ Low frequency cutoff in Hz (e.g., 0.1 for high-pass at 0.1 Hz)
101
+ h_freq : float
102
+ High frequency cutoff in Hz (e.g., 40 for low-pass at 40 Hz)
103
+
104
+ Returns
105
+ -------
106
+ SimpleRaw
107
+ A new SimpleRaw object with filtered data
108
+ """
109
+ filtered_raw = self._raw.copy().filter(l_freq=l_freq, h_freq=h_freq)
110
+ return SimpleRaw(filtered_raw)
111
+
112
+ def re_reference(self, ref_channels: list | None = None) -> SimpleRaw:
113
+ """
114
+ Re-reference the data.
115
+
116
+ Parameters
117
+ ----------
118
+ ref_channels : list or None
119
+ Channel(s) to use as reference. If None, uses average reference.
120
+
121
+ Returns
122
+ -------
123
+ SimpleRaw
124
+ A new SimpleRaw object with re-referenced data
125
+ """
126
+ reref_raw = self._raw.copy()
127
+ if ref_channels is None:
128
+ # Average reference
129
+ reref_raw.set_eeg_reference('average')
130
+ else:
131
+ reref_raw.set_eeg_reference(ref_channels)
132
+ return SimpleRaw(reref_raw)
133
+
134
+ def get_events(self) -> tuple[np.ndarray, dict]:
135
+ """Extract events from annotations.
136
+
137
+ Returns
138
+ -------
139
+ events : np.ndarray, shape (n_events, 3)
140
+ Array of events (sample, duration, event_id).
141
+ event_id : dict
142
+ Mapping of event labels to integer IDs.
143
+ """
144
+ return mne.events_from_annotations(self._raw)
145
+
146
+
@@ -0,0 +1,56 @@
1
+ import mne
2
+ import os.path as op
3
+ from pathlib import Path
4
+ from typing import Protocol
5
+
6
+ class ImportRawHandler(Protocol):
7
+ """Protocol for reading data into the SimpleRaw class"""
8
+
9
+ def load_raw(self, directory: Path | None) -> mne.io.Raw:
10
+ """Read data from file and return a mne.io.Raw object"""
11
+ ...
12
+
13
+ # def save_raw(self, directory: Path, raw: mne.io.Raw) -> None:
14
+ # """Save data to file"""
15
+ # ...
16
+
17
+ class DefaultImportRawHandler:
18
+ """Default implementation of ImportRawHandler Protocol using MNE example data"""
19
+
20
+ FIF_FILE = "ERP-CORE_Subject-001_Task-Flankers_eeg.fif"
21
+
22
+ def load_raw(self, directory: Path | None = None) -> mne.io.Raw:
23
+ if directory is None:
24
+ directory = Path(".")
25
+ else:
26
+ directory = Path(directory)
27
+ directory.mkdir(parents=True, exist_ok=True)
28
+ # extend this later so that the user can abort if this is not what they want
29
+ # could also give them a choice of path?
30
+ print("No raw data provided, downloading MNE sample data to {directory}.")
31
+ data_path = mne.datasets.erp_core.data_path(directory)
32
+
33
+ erp_core_file = data_path / self.FIF_FILE
34
+ print(f"Downloaded {erp_core_file}.")
35
+ raw = mne.io.read_raw_fif(erp_core_file, preload=True)
36
+
37
+ return raw
38
+
39
+ # def save_raw(self, directory: Path, raw: mne.io.Raw) -> None:
40
+ # """Save data to file"""
41
+
42
+ class FileImportRawHandler:
43
+ """
44
+ Implementation of ImportRawHandler Protocol that reads raw data from file.
45
+ Only equipped to handle BrainVision at the moment.
46
+ """
47
+ # Extend this to handle fif and BIDS
48
+ # perhaps use a dispatch dictionary?
49
+ def __init__(self, directory: Path):
50
+ directory = Path(directory)
51
+ self.directory = directory
52
+
53
+ def load_raw(self) -> mne.io.Raw:
54
+ raw = mne.io.read_raw_brainvision(self.directory, preload=True)
55
+ return raw
56
+