Skip to content

Commit 1e1574b

Browse files
Add notch filter (#702)
1 parent a304266 commit 1e1574b

File tree

6 files changed

+99
-18
lines changed

6 files changed

+99
-18
lines changed

docs/source/settings/preprocessing/filter.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ of tips! 😇
3232
- h_freq
3333
- l_trans_bandwidth
3434
- h_trans_bandwidth
35+
- notch_freq
36+
- notch_trans_bandwidth
37+
- notch_widths

docs/source/v1.1.md.inc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
## v1.1.0-dev (unreleased)
22

3-
[//]: # (### :new: New features & enhancements)
3+
### :new: New features & enhancements
44

5-
[//]: # (- Whatever (#000 by @whoever) )
5+
- Add support for notch filtering (#702 by @hoechenberger and @larsoner)
66

77
[//]: # (### :warning: Behavior changes)
88

@@ -12,4 +12,3 @@
1212

1313
[//]: # (### :bug: Bug fixes)
1414

15-
- Nothing yet

mne_bids_pipeline/_config.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -683,13 +683,32 @@
683683
l_freq: Optional[float] = None
684684
"""
685685
The low-frequency cut-off in the highpass filtering step.
686-
Keep it None if no highpass filtering should be applied.
686+
Keep it `None` if no highpass filtering should be applied.
687687
"""
688688

689689
h_freq: Optional[float] = 40.0
690690
"""
691691
The high-frequency cut-off in the lowpass filtering step.
692-
Keep it None if no lowpass filtering should be applied.
692+
Keep it `None` if no lowpass filtering should be applied.
693+
"""
694+
695+
notch_freq: Optional[Union[float, Iterable[float]]] = None
696+
"""
697+
Notch filter frequency. More than one frequency can be supplied, e.g. to remove
698+
harmonics. Keep it `None` if no notch filter should be applied.
699+
700+
Note: Note
701+
The notch filter will be applied before high- and lowpass filtering.
702+
703+
???+ example "Example"
704+
Remove line noise at 50 Hz:
705+
```python
706+
notch_freq = 50
707+
```
708+
Remove line noise at 50 Hz and its (sub-)harmonics
709+
```python
710+
notch_freq = [25, 50, 100, 150]
711+
```
693712
"""
694713

695714
l_trans_bandwidth: Union[float, Literal["auto"]] = "auto"
@@ -706,6 +725,16 @@
706725
parameters.
707726
"""
708727

728+
notch_trans_bandwidth: float = 1.0
729+
"""
730+
Specifies the transition bandwidth of the notch filter. The default is `1.`.
731+
"""
732+
733+
notch_widths: Optional[Union[float, Iterable[float]]] = None
734+
"""
735+
Specifies the width of each stop band. `None` uses the MNE default.
736+
"""
737+
709738
raw_resample_sfreq: Optional[float] = None
710739
"""
711740
Specifies at which sampling frequency the data should be resampled.

mne_bids_pipeline/steps/preprocessing/_03_frequency_filter.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import numpy as np
1818
from types import SimpleNamespace
19-
from typing import Optional, Union, Literal
19+
from typing import Optional, Union, Literal, Iterable
2020

2121
import mne
2222

@@ -58,7 +58,36 @@ def get_input_fnames_frequency_filter(
5858
)
5959

6060

61-
def filter(
61+
def notch_filter(
62+
raw: mne.io.BaseRaw,
63+
subject: str,
64+
session: Optional[str],
65+
run: str,
66+
freqs: Optional[Union[float, Iterable[float]]],
67+
trans_bandwidth: Union[float, Literal["auto"]],
68+
notch_widths: Optional[Union[float, Iterable[float]]],
69+
data_type: Literal["experimental", "empty-room", "resting-state"],
70+
) -> None:
71+
"""Filter data channels (MEG and EEG)."""
72+
if freqs is None:
73+
msg = f"Not applying notch filter to {data_type} data."
74+
else:
75+
msg = f"Notch filtering {data_type} data at {freqs} Hz."
76+
77+
logger.info(**gen_log_kwargs(message=msg))
78+
79+
if freqs is None:
80+
return
81+
82+
raw.notch_filter(
83+
freqs=freqs,
84+
trans_bandwidth=trans_bandwidth,
85+
notch_widths=notch_widths,
86+
n_jobs=1,
87+
)
88+
89+
90+
def bandpass_filter(
6291
raw: mne.io.BaseRaw,
6392
subject: str,
6493
session: Optional[str],
@@ -89,10 +118,6 @@ def filter(
89118
h_freq=h_freq,
90119
l_trans_bandwidth=l_trans_bandwidth,
91120
h_trans_bandwidth=h_trans_bandwidth,
92-
filter_length="auto",
93-
phase="zero",
94-
fir_window="hamming",
95-
fir_design="firwin",
96121
n_jobs=1,
97122
)
98123

@@ -152,7 +177,17 @@ def filter_data(
152177
split=None,
153178
)
154179
raw.load_data()
155-
filter(
180+
notch_filter(
181+
raw=raw,
182+
subject=subject,
183+
session=session,
184+
run=run,
185+
freqs=cfg.notch_freq,
186+
trans_bandwidth=cfg.notch_trans_bandwidth,
187+
notch_widths=cfg.notch_widths,
188+
data_type="experimental",
189+
)
190+
bandpass_filter(
156191
raw=raw,
157192
subject=subject,
158193
session=session,
@@ -179,10 +214,10 @@ def filter_data(
179214
split_size=cfg._raw_split_size,
180215
)
181216
_update_for_splits(out_files, in_key)
217+
fmax = 1.5 * cfg.h_freq if cfg.h_freq is not None else np.inf
182218
if exec_params.interactive:
183219
# Plot raw data and power spectral density.
184220
raw.plot(n_channels=50, butterfly=True)
185-
fmax = 1.5 * cfg.h_freq if cfg.h_freq is not None else np.inf
186221
raw.plot_psd(fmax=fmax)
187222

188223
del raw
@@ -227,7 +262,17 @@ def filter_data(
227262
)
228263

229264
raw_noise.load_data()
230-
filter(
265+
notch_filter(
266+
raw=raw_noise,
267+
subject=subject,
268+
session=session,
269+
run=task,
270+
freqs=cfg.notch_freq,
271+
trans_bandwidth=cfg.notch_trans_bandwidth,
272+
notch_widths=cfg.notch_widths,
273+
data_type=data_type,
274+
)
275+
bandpass_filter(
231276
raw=raw_noise,
232277
subject=subject,
233278
session=session,
@@ -257,7 +302,6 @@ def filter_data(
257302
if exec_params.interactive:
258303
# Plot raw data and power spectral density.
259304
raw_noise.plot(n_channels=50, butterfly=True)
260-
fmax = 1.5 * cfg.h_freq if cfg.h_freq is not None else np.inf
261305
raw_noise.plot_psd(fmax=fmax)
262306

263307
assert len(in_files) == 0, in_files.keys()
@@ -300,8 +344,11 @@ def get_config(
300344
deriv_root=config.deriv_root,
301345
l_freq=config.l_freq,
302346
h_freq=config.h_freq,
347+
notch_freq=config.notch_freq,
303348
l_trans_bandwidth=config.l_trans_bandwidth,
304349
h_trans_bandwidth=config.h_trans_bandwidth,
350+
notch_trans_bandwidth=config.notch_trans_bandwidth,
351+
notch_widths=config.notch_widths,
305352
raw_resample_sfreq=config.raw_resample_sfreq,
306353
crop_runs=config.crop_runs,
307354
rename_events=config.rename_events,

mne_bids_pipeline/tests/configs/config_ERP_CORE.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@
5757
eog_channels = ["HEOG", "VEOG"]
5858

5959
l_freq = 0.1
60-
h_freq = 40
60+
h_freq = None
61+
notch_freq = 60
6162

6263
decode = True
6364
decoding_time_generalization = True
@@ -148,6 +149,7 @@
148149
"theta": [4, 7],
149150
"alpha": [8, 12],
150151
"beta": [13, 20, 30],
152+
"gamma": [50, 63],
151153
}
152154
decoding_csp_times = [-0.2, 0.0, 0.2, 0.4]
153155
elif task == "LRP":

mne_bids_pipeline/tests/test_documented.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ def test_options_documented():
5050
if val not in allowed_duplicates:
5151
assert val not in in_doc, "Duplicate documentation"
5252
in_doc.add(val)
53-
assert in_doc.difference(in_config) == set(), "Extra values in doc"
54-
assert in_config.difference(in_doc) == set(), "Values missing from doc"
53+
what = "docs/source/settings doc"
54+
assert in_doc.difference(in_config) == set(), f"Extra values in {what}"
55+
assert in_config.difference(in_doc) == set(), f"Values missing from {what}"
5556

5657

5758
def test_datasets_in_doc():

0 commit comments

Comments
 (0)