pyprep 0.6.0__tar.gz → 0.7.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.
@@ -1,3 +1,6 @@
1
+ .claude
2
+ CLAUDE.md
3
+
1
4
  .vscode
2
5
  .idea/*
3
6
  /idea
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyprep
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: PyPREP: A Python implementation of the preprocessing pipeline (PREP) for EEG data.
5
5
  Project-URL: Bug Tracker, https://github.com/sappelhoff/pyprep/issues/
6
6
  Project-URL: Documentation, https://pyprep.readthedocs.io/en/latest
@@ -42,6 +42,7 @@ Classifier: Programming Language :: Python :: 3.10
42
42
  Classifier: Programming Language :: Python :: 3.11
43
43
  Classifier: Programming Language :: Python :: 3.12
44
44
  Classifier: Programming Language :: Python :: 3.13
45
+ Classifier: Programming Language :: Python :: 3.14
45
46
  Classifier: Topic :: Scientific/Engineering
46
47
  Requires-Python: >=3.10
47
48
  Requires-Dist: mne>=1.3.0
@@ -175,7 +175,7 @@ class NoisyChannels:
175
175
  ch_names = np.asarray(self.raw_mne.info["ch_names"])
176
176
  self.ch_names_original = ch_names
177
177
  self.n_chans_original = len(ch_names)
178
- self.n_samples_original = raw.get_data().shape[1]
178
+ self.n_samples_original = raw.n_times
179
179
 
180
180
  # Before anything else, flag bad-by-NaNs and bad-by-flats
181
181
  self.find_bad_by_nan_flat()
@@ -378,7 +378,9 @@ class NoisyChannels:
378
378
 
379
379
  This method is run automatically when a ``NoisyChannels`` object is
380
380
  initialized, preventing flat or NaN-containing channels from interfering
381
- with the detection of other types of bad channels.
381
+ with the detection of other types of bad channels. The
382
+ ``reject_by_annotation`` setting of the :class:`NoisyChannels` instance
383
+ is respected when retrieving the data.
382
384
 
383
385
  Parameters
384
386
  ----------
@@ -388,7 +390,7 @@ class NoisyChannels:
388
390
  10e-10 µV in MATLAB PREP).
389
391
  """
390
392
  # Get all EEG channels from original copy of data
391
- EEGData = self.raw_mne.get_data()
393
+ EEGData = self.raw_mne.get_data(reject_by_annotation=self.reject_by_annotation)
392
394
 
393
395
  # Detect channels containing any NaN values
394
396
  nan_channel_mask = np.isnan(np.sum(EEGData, axis=1))
@@ -638,6 +640,7 @@ class NoisyChannels:
638
640
  This is a PyPREP-only method not present in the original MATLAB PREP.
639
641
 
640
642
  A channel is considered "bad-by-psd" if:
643
+
641
644
  1. Its power in any frequency band (low: 1-15 Hz, mid: 15-30 Hz,
642
645
  high: 30-45 Hz) is abnormally HIGH compared to other channels, OR
643
646
  2. Its high-frequency band has more power than its low-frequency band
@@ -3,12 +3,13 @@
3
3
  # Authors: The PyPREP developers
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
+ import warnings
7
+
6
8
  import mne
7
9
  from mne.utils import check_random_state
8
10
 
9
11
  from pyprep.reference import Reference
10
12
  from pyprep.removeTrend import removeTrend
11
- from pyprep.utils import _set_diff, _union # noqa: F401
12
13
 
13
14
 
14
15
  class PrepPipeline:
@@ -182,6 +183,18 @@ class PrepPipeline:
182
183
  self.filter_kwargs = filter_kwargs
183
184
  self.matlab_strict = matlab_strict
184
185
 
186
+ # Initialize attributes to be filled in later
187
+ self._line_noise_removed = False
188
+ self.noisy_channels_original = None
189
+ self.noisy_channels_before_interpolation = None
190
+ self.noisy_channels_after_interpolation = None
191
+ self.bad_before_interpolation = None
192
+ self.EEG_before_interpolation = None
193
+ self.reference_before_interpolation = None
194
+ self.reference_after_interpolation = None
195
+ self.interpolated_channels = None
196
+ self.still_noisy_channels = None
197
+
185
198
  @property
186
199
  def raw(self):
187
200
  """Return a version of self.raw_eeg that includes the non-eeg channels."""
@@ -191,39 +204,96 @@ class PrepPipeline:
191
204
  else:
192
205
  return full_raw.add_channels([self.raw_non_eeg], force_update_info=True)
193
206
 
194
- def fit(self):
195
- """Run the whole PREP pipeline."""
196
- # Step 1: 1Hz high pass filtering
197
- if len(self.prep_params["line_freqs"]) != 0:
198
- self.EEG_new = removeTrend(
199
- self.EEG_raw, self.sfreq, matlab_strict=self.matlab_strict
207
+ def remove_line_noise(self, line_freqs=None):
208
+ """Remove line noise from all EEG channels.
209
+
210
+ Line noise is removed by detrending the signal, applying a notch filter,
211
+ and adding the slow drifts back. By default the notch filter uses MNE's
212
+ ``spectrum_fit`` method, which attempts to isolate and remove line noise
213
+ while preserving unrelated background signal in the same frequency ranges
214
+ (to minimize distortions in the power-spectral density). The filter can be
215
+ configured via the ``filter_kwargs`` argument of :class:`PrepPipeline`.
216
+
217
+ Parameters
218
+ ----------
219
+ line_freqs : {np.ndarray, list, None}, optional
220
+ A list of the frequencies (in Hz) at which line noise should be removed
221
+ (e.g., ``np.arange(60, sfreq / 2, 60)`` for a recording with a powerline
222
+ noise of 60 Hz). If ``None`` (default), the ``"line_freqs"`` entry of the
223
+ ``prep_params`` passed to :class:`PrepPipeline` is used.
224
+
225
+ """
226
+ if line_freqs is None:
227
+ line_freqs = self.prep_params["line_freqs"]
228
+
229
+ # Remove slow drifts from the recording prior to filtering
230
+ self.EEG_new = removeTrend(
231
+ self.EEG_raw, self.sfreq, matlab_strict=self.matlab_strict
232
+ )
233
+
234
+ # Remove line noise. When no filter kwargs are given, fall back to PREP's
235
+ # default ``spectrum_fit`` settings; otherwise use the provided kwargs as-is.
236
+ if self.filter_kwargs is None:
237
+ self.EEG_clean = mne.filter.notch_filter(
238
+ self.EEG_new,
239
+ Fs=self.sfreq,
240
+ freqs=line_freqs,
241
+ method="spectrum_fit",
242
+ mt_bandwidth=2,
243
+ p_value=0.01,
244
+ filter_length="10s",
245
+ )
246
+ else:
247
+ self.EEG_clean = mne.filter.notch_filter(
248
+ self.EEG_new,
249
+ Fs=self.sfreq,
250
+ freqs=line_freqs,
251
+ **self.filter_kwargs,
252
+ )
253
+
254
+ # Add the slow drifts back
255
+ self.EEG = self.EEG_raw - self.EEG_new + self.EEG_clean
256
+ self.raw_eeg._data = self.EEG
257
+ self._line_noise_removed = True
258
+
259
+ def robust_reference(self, max_iterations=None, interpolate_bads=True):
260
+ """Perform robust referencing on the EEG signal and detect bad channels.
261
+
262
+ This method uses an iterative approach to estimate a robust average
263
+ reference signal free of contamination from bad channels, as detected
264
+ automatically using the methods of :class:`~pyprep.NoisyChannels`. Once
265
+ estimated, the robust average reference is applied to the data and bad
266
+ channel detection is re-run to flag any noisy or unusable channels
267
+ post-reference.
268
+
269
+ By default, this method will also interpolate the signals of any channels
270
+ detected as bad following robust referencing, re-reference the data
271
+ accordingly, and re-detect any remaining bad channels.
272
+
273
+ Parameters
274
+ ----------
275
+ max_iterations : {int, None}, optional
276
+ The maximum number of iterations of noisy channel removal to perform
277
+ during robust referencing. If ``None`` (default), the ``"max_iterations"``
278
+ entry of the ``prep_params`` passed to :class:`PrepPipeline` is used.
279
+ interpolate_bads : bool, optional
280
+ Whether or not any remaining bad channels following robust referencing
281
+ should be interpolated. Defaults to ``True``.
282
+
283
+ """
284
+ if max_iterations is None:
285
+ max_iterations = self.prep_params["max_iterations"]
286
+
287
+ if not self._line_noise_removed:
288
+ warnings.warn(
289
+ "Robust referencing is being performed without prior line-noise "
290
+ "removal. If this is intentional, you can safely ignore this "
291
+ "warning; otherwise, call `remove_line_noise` first or use `fit`.",
292
+ UserWarning,
293
+ stacklevel=2,
200
294
  )
201
295
 
202
- # Step 2: Removing line noise
203
- linenoise = self.prep_params["line_freqs"]
204
- if self.filter_kwargs is None:
205
- self.EEG_clean = mne.filter.notch_filter(
206
- self.EEG_new,
207
- Fs=self.sfreq,
208
- freqs=linenoise,
209
- method="spectrum_fit",
210
- mt_bandwidth=2,
211
- p_value=0.01,
212
- filter_length="10s",
213
- )
214
- else:
215
- self.EEG_clean = mne.filter.notch_filter(
216
- self.EEG_new,
217
- Fs=self.sfreq,
218
- freqs=linenoise,
219
- **self.filter_kwargs,
220
- )
221
-
222
- # Add Trend back
223
- self.EEG = self.EEG_raw - self.EEG_new + self.EEG_clean
224
- self.raw_eeg._data = self.EEG
225
-
226
- # Step 3: Referencing
296
+ # Perform robust referencing on the signal
227
297
  reference = Reference(
228
298
  self.raw_eeg,
229
299
  self.prep_params,
@@ -231,7 +301,8 @@ class PrepPipeline:
231
301
  matlab_strict=self.matlab_strict,
232
302
  **self.ransac_settings,
233
303
  )
234
- reference.perform_reference(self.prep_params["max_iterations"])
304
+ reference.perform_reference(max_iterations, interpolate_bads)
305
+
235
306
  self.raw_eeg = reference.raw
236
307
  self.noisy_channels_original = reference.noisy_channels_original
237
308
  self.noisy_channels_before_interpolation = (
@@ -247,4 +318,17 @@ class PrepPipeline:
247
318
  self.interpolated_channels = reference.interpolated_channels
248
319
  self.still_noisy_channels = reference.still_noisy_channels
249
320
 
321
+ def fit(self):
322
+ """Run the whole PREP pipeline."""
323
+ # Step 1: Adaptive line noise removal
324
+ if len(self.prep_params["line_freqs"]) != 0:
325
+ self.remove_line_noise(self.prep_params["line_freqs"])
326
+ else:
327
+ # No line noise to remove: mark the stage as deliberately skipped so
328
+ # that `robust_reference` does not emit a spurious warning.
329
+ self._line_noise_removed = True
330
+
331
+ # Step 2: Robust Referencing
332
+ self.robust_reference(self.prep_params["max_iterations"])
333
+
250
334
  return self
@@ -12,9 +12,6 @@ from pyprep.find_noisy_channels import NoisyChannels
12
12
  from pyprep.removeTrend import removeTrend
13
13
  from pyprep.utils import _eeglab_interpolate_bads, _set_diff, _union
14
14
 
15
- logging.basicConfig(
16
- level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
17
- )
18
15
  logger = logging.getLogger(__name__)
19
16
 
20
17
 
@@ -102,42 +99,59 @@ class Reference:
102
99
  "reject_by_annotation": reject_by_annotation,
103
100
  }
104
101
  self.random_state = check_random_state(random_state)
105
- self._extra_info = {}
106
102
  self.matlab_strict = matlab_strict
107
103
 
108
- def perform_reference(self, max_iterations=4):
104
+ # Initialize attributes that get filled in during referencing
105
+ self.bad_before_interpolation = None
106
+ self.EEG_before_interpolation = None
107
+ self.noisy_channels_before_interpolation = None
108
+ self.reference_signal_new = None
109
+ self.interpolated_channels = None
110
+ self.still_noisy_channels = None
111
+ self.noisy_channels_after_interpolation = None
112
+ self._extra_info = {
113
+ "initial_bad": None,
114
+ "interpolated": None,
115
+ "remaining_bad": None,
116
+ }
117
+
118
+ def perform_reference(self, max_iterations=4, interpolate_bads=True):
109
119
  """Estimate the true signal mean and interpolate bad channels.
110
120
 
121
+ This function implements the functionality of the `performReference` function
122
+ as part of the PREP pipeline on mne raw object.
123
+
111
124
  Parameters
112
125
  ----------
113
126
  max_iterations : int | None
114
127
  The maximum number of iterations of noisy channel removal to perform
115
128
  during robust referencing. Defaults to ``4``.
116
-
117
- This function implements the functionality of the `performReference` function
118
- as part of the PREP pipeline on mne raw object.
129
+ interpolate_bads : bool, optional
130
+ Whether or not any remaining bad channels following robust referencing
131
+ should be interpolated or left as-is. Defaults to ``True``.
119
132
 
120
133
  Notes
121
134
  -----
122
135
  This function calls ``robust_reference`` first.
123
- Currently this function only implements the functionality of default
124
- settings, i.e., ``doRobustPost``.
125
136
 
126
137
  """
127
- # Phase 1: Estimate the true signal mean with robust referencing
138
+ # Estimate the true signal mean with robust referencing
128
139
  self.robust_reference(max_iterations)
129
140
  # If we interpolate the raw here we would be interpolating
130
141
  # more than what we later actually account for (in interpolated channels).
131
142
  dummy = self.raw.copy()
132
143
  dummy.info["bads"] = self.noisy_channels["bad_all"]
133
- if self.matlab_strict:
134
- _eeglab_interpolate_bads(dummy)
135
- else:
136
- dummy.interpolate_bads()
144
+ if len(dummy.info["bads"]) > 0:
145
+ if self.matlab_strict:
146
+ _eeglab_interpolate_bads(dummy)
147
+ else:
148
+ dummy.interpolate_bads()
137
149
  self.reference_signal = np.nanmean(
138
150
  dummy.get_data(picks=self.reference_channels), axis=0
139
151
  )
140
152
  del dummy
153
+
154
+ # Re-reference the data using the calculated robust average reference
141
155
  rereferenced_index = [
142
156
  self.ch_names_eeg.index(ch) for ch in self.rereferenced_channels
143
157
  ]
@@ -145,7 +159,7 @@ class Reference:
145
159
  self.EEG, self.reference_signal, rereferenced_index
146
160
  )
147
161
 
148
- # Phase 2: Find the bad channels and interpolate
162
+ # Detect which channels are still bad following robust referencing
149
163
  self.raw._data = self.EEG
150
164
  noisy_detector = NoisyChannels(
151
165
  self.raw,
@@ -154,33 +168,65 @@ class Reference:
154
168
  reject_by_annotation=self.ransac_settings.get("reject_by_annotation"),
155
169
  )
156
170
  noisy_detector.find_all_bads(**self.ransac_settings)
157
-
158
- # Record Noisy channels and EEG before interpolation
159
171
  self.bad_before_interpolation = noisy_detector.get_bads(verbose=True)
160
172
  self.EEG_before_interpolation = self.EEG.copy()
161
173
  self.noisy_channels_before_interpolation = noisy_detector.get_bads(as_dict=True)
162
174
  self.noisy_channels_before_interpolation["bad_by_manual"] = self.bads_manual
163
175
  self._extra_info["interpolated"] = noisy_detector._extra_info
164
176
 
177
+ # Update bad channels in MNE raw object
165
178
  bad_channels = _union(self.bad_before_interpolation, self.unusable_channels)
166
179
  self.raw.info["bads"] = bad_channels
167
- if self.matlab_strict:
168
- _eeglab_interpolate_bads(self.raw)
169
- else:
170
- self.raw.interpolate_bads()
180
+
181
+ # If enabled, interpolate all bad channels and detect any remaining bads
182
+ if interpolate_bads:
183
+ self.interpolate_bads()
184
+
185
+ return self
186
+
187
+ def interpolate_bads(self):
188
+ """Interpolate any remaining bad channels following robust referencing.
189
+
190
+ This method can only be called if :meth:`~.perform_reference` has already
191
+ been run with the ``interpolate_bads`` parameter set to ``False``. It cannot
192
+ be run more than once per instance of :class:`~pyprep.Reference`.
193
+
194
+ """
195
+ if self.bad_before_interpolation is None:
196
+ raise RuntimeError(
197
+ "Robust referencing must be performed before remaining bad channels "
198
+ "can be interpolated."
199
+ )
200
+ elif self.interpolated_channels is not None:
201
+ raise RuntimeError(
202
+ "Bad channel interpolation cannot be performed more than once - "
203
+ "interpolating signals using other interpolated signals is likely "
204
+ "to have poor results."
205
+ )
206
+
207
+ # Interpolate any channels flagged as bad following robust referencing
208
+ bad_channels = self.raw.info["bads"]
209
+ if len(bad_channels) > 0:
210
+ if self.matlab_strict:
211
+ _eeglab_interpolate_bads(self.raw)
212
+ else:
213
+ self.raw.interpolate_bads()
214
+
215
+ # Calculate and remove the new average reference following interpolation
171
216
  reference_correct = np.nanmean(
172
217
  self.raw.get_data(picks=self.reference_channels), axis=0
173
218
  )
219
+ rereferenced_index = [
220
+ self.ch_names_eeg.index(ch) for ch in self.rereferenced_channels
221
+ ]
174
222
  self.EEG = self.raw.get_data()
175
223
  self.EEG = self.remove_reference(
176
224
  self.EEG, reference_correct, rereferenced_index
177
225
  )
178
- # reference signal after interpolation
179
226
  self.reference_signal_new = self.reference_signal + reference_correct
180
- # MNE Raw object after interpolation
181
- self.raw._data = self.EEG
227
+ self.raw._data = self.EEG # Update the MNE Raw object
182
228
 
183
- # Still noisy channels after interpolation
229
+ # Detect any remaining noisy channels following interpolation
184
230
  self.interpolated_channels = bad_channels
185
231
  noisy_detector = NoisyChannels(
186
232
  self.raw,
@@ -15,6 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.11",
16
16
  "Programming Language :: Python :: 3.12",
17
17
  "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
18
19
  "Programming Language :: Python",
19
20
  "Topic :: Scientific/Engineering",
20
21
  ]
@@ -86,8 +87,7 @@ exclude = [
86
87
  "/.github/**",
87
88
  "/docs",
88
89
  "/examples",
89
- "matprep_artifacts",
90
- "matprep_artifacts/**",
90
+ "/tools",
91
91
  "tests/**",
92
92
  ]
93
93
 
@@ -103,11 +103,6 @@ addopts = """. --cov=pyprep/ --cov-report=xml --cov-config=pyproject.toml --verb
103
103
  filterwarnings = [
104
104
  ]
105
105
 
106
- [tool.ruff]
107
- extend-exclude = [
108
- "matprep_artifacts/**",
109
- ]
110
-
111
106
  [tool.ruff.lint]
112
107
  ignore = ["A002"]
113
108
  select = ["A", "D", "E", "F", "I", "UP", "W"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes