Skip to content

Commit d47cbbc

Browse files
authored
Merge pull request #203 from fooof-tools/linen
[DOC] - Add a line noise example
2 parents 083cc08 + 33e4d71 commit d47cbbc

File tree

5 files changed

+277
-18
lines changed

5 files changed

+277
-18
lines changed

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
'examples_dirs': ['../examples', '../tutorials', '../motivations'],
127127
'gallery_dirs': ['auto_examples', 'auto_tutorials', 'auto_motivations'],
128128
'subsection_order' : ExplicitOrder(['../examples/manage',
129+
'../examples/processing',
129130
'../examples/plots',
130131
'../examples/sims',
131132
'../examples/analyses',

examples/processing/README.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Processing
2+
----------
3+
4+
Examples on how to process data related to spectral parameterization.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
Dealing with Line Noise
3+
=======================
4+
5+
This example covers strategies for dealing with line noise.
6+
"""
7+
8+
###################################################################################################
9+
10+
# sphinx_gallery_thumbnail_number = 2
11+
12+
# Import the spectral parameterization object and utilities
13+
from fooof import FOOOF
14+
from fooof.plts import plot_spectrum, plot_spectra
15+
from fooof.utils import trim_spectrum, interpolate_spectrum
16+
17+
# Import simulation functions to create some example data
18+
from fooof.sim.gen import gen_power_spectrum
19+
20+
# Import NeuroDSP functions for simulating & processing time series
21+
from neurodsp.sim import sim_combined
22+
from neurodsp.filt import filter_signal
23+
from neurodsp.spectral import compute_spectrum
24+
25+
###################################################################################################
26+
# Line Noise Peaks
27+
# ----------------
28+
#
29+
# Neural recordings typically have power line artifacts, at either 50 or 60 Hz, depending on
30+
# where the data were collected, which can impact spectral parameterization.
31+
#
32+
# In this example, we explore some options for dealing with line noise artifacts.
33+
#
34+
# Interpolating Line Noise Peaks
35+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
36+
#
37+
# One approach is to interpolate away line noise peaks, in the frequency domain. This
38+
# approach simply gets rid of the peaks, interpolating the data to maintain the 1/f
39+
# character of the data, allowing for subsequent fitting.
40+
#
41+
# The :func:`~fooof.utils.interpolate_spectrum` function allows for doing simple
42+
# interpolation. Given a narrow frequency region, this function interpolates the spectrum,
43+
# such that the 'peak' of the line noise is removed.
44+
#
45+
46+
###################################################################################################
47+
48+
# Generate an example power spectrum, with line noise
49+
freqs1, powers1 = gen_power_spectrum([3, 75], [1, 1],
50+
[[10, 0.75, 2], [60, 1, 0.5]])
51+
52+
# Visualize the generated power spectrum
53+
plot_spectrum(freqs1, powers1, log_powers=True)
54+
55+
###################################################################################################
56+
#
57+
# In the plot above, we have an example spectrum, with some power line noise.
58+
#
59+
# To prepare this data for fitting, we can interpolate away the line noise region.
60+
#
61+
62+
###################################################################################################
63+
64+
# Interpolate away the line noise region
65+
interp_range = [58, 62]
66+
freqs_int1, powers_int1 = interpolate_spectrum(freqs1, powers1, interp_range)
67+
68+
###################################################################################################
69+
70+
# Plot the spectra for the power spectra before and after interpolation
71+
plot_spectra(freqs1, [powers1, powers_int1], log_powers=True,
72+
labels=['Original Spectrum', 'Interpolated Spectrum'])
73+
74+
###################################################################################################
75+
#
76+
# As we can see in the above, the interpolation removed the peak from the data.
77+
#
78+
# We can now go ahead and parameterize the spectrum.
79+
#
80+
81+
###################################################################################################
82+
83+
# Initialize a power spectrum model
84+
fm1 = FOOOF(verbose=False)
85+
fm1.report(freqs_int1, powers_int1)
86+
87+
###################################################################################################
88+
# Multiple Interpolation Regions
89+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
90+
#
91+
# Line noise artifacts often also display harmonics, such that when analyzing broader
92+
# frequency ranges, there may be multiple peaks that need to be interpolated.
93+
#
94+
# This can be done by passing in multiple interpolation regions to
95+
# :func:`~fooof.utils.interpolate_spectrum`, which we will do in the next example.
96+
#
97+
98+
###################################################################################################
99+
100+
# Generate an example power spectrum, with line noise & harmonics
101+
freqs2, powers2 = gen_power_spectrum([1, 150], [1, 500, 1.5],
102+
[[10, 0.5, 2], [60, 0.75, 0.5], [120, 0.5, 0.5]])
103+
104+
# Interpolate away the line noise region & harmonics
105+
interp_ranges = [[58, 62], [118, 122]]
106+
freqs_int2, powers_int2 = interpolate_spectrum(freqs2, powers2, interp_ranges)
107+
108+
###################################################################################################
109+
110+
# Plot the power spectrum before and after interpolation
111+
plot_spectra(freqs2, [powers2, powers_int2], log_powers=True,
112+
labels=['Original Spectrum', 'Interpolated Spectrum'])
113+
114+
###################################################################################################
115+
116+
# Parameterize the interpolated power spectrum
117+
fm2 = FOOOF(aperiodic_mode='knee', verbose=False)
118+
fm2.report(freqs2, powers_int2)
119+
120+
###################################################################################################
121+
# Fitting Line Noise as Peaks
122+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~
123+
#
124+
# In some cases, you may also be able to simply allow the parameterization to peaks to the
125+
# line noise and harmonics. By simply fitting the line noise as peaks, the model can deal
126+
# with the peaks in order to accurately fit the aperiodic component.
127+
#
128+
# These peaks are of course not to be analyzed, but once the model has been fit, you can
129+
# simply ignore them. There should generally be no issue with fitting and having them in
130+
# the model, and allowing the model to account for these peaks typically helps the model
131+
# better fit the rest of the data.
132+
#
133+
# Below we can see that the model does indeed work when fitting data with line noise peaks.
134+
#
135+
136+
###################################################################################################
137+
138+
# Fit power spectrum models to original spectra
139+
fm1.report(freqs1, powers1)
140+
fm2.report(freqs2, powers2)
141+
142+
###################################################################################################
143+
# The Problem with Bandstop Filtering
144+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
145+
#
146+
# A common approach for getting rid of line noise activity is to use bandstop filtering to
147+
# remove activity at the line noise frequencies. Such a filter effectively set the power
148+
# of these frequencies to be approximately zero.
149+
#
150+
# Unfortunately, this doesn't work very well with spectral parameterization, since the
151+
# parameterization algorithm tries to fit each power value as either part of the aperiodic
152+
# component, or as an overlying peak. Frequencies that have filtered out are neither, and
153+
# the model has trouble, as it and has no concept of power values below the aperiodic component.
154+
#
155+
# In practice, this means that the "missing" power will impact the fit, and pull down the
156+
# aperiodic component. One way to think of this is that the power spectrum model can deal with,
157+
# and even expects, 'positive outliers' above the aperiodic (these are considered 'peaks'), but
158+
# not 'negative outliers', or values below the aperiodic, as there is no expectation of this
159+
# happening in the model.
160+
#
161+
# In the following example, we can see how bandstop filtering negatively impacts fitting.
162+
# Because of this, for the purposes of spectral parameterization, bandstop filters are not
163+
# recommended as a way to remove line noise.
164+
#
165+
# Note that if one has already applied a bandstop filter, then you can still
166+
# apply the interpolation from above.
167+
#
168+
169+
###################################################################################################
170+
171+
# General settings for the simulation
172+
n_seconds = 30
173+
fs = 1000
174+
175+
# Define the settings for the simulated signal
176+
components = {'sim_powerlaw' : {'exponent' : -1.5},
177+
'sim_oscillation' : [{'freq' : 10}, {'freq' : 60}]}
178+
comp_vars = [0.5, 1, 1]
179+
180+
# Simulate a time series
181+
sig = sim_combined(n_seconds, fs, components, comp_vars)
182+
183+
###################################################################################################
184+
185+
# Bandstop filter the signal to remove line noise frequencies
186+
sig_filt = filter_signal(sig, fs, 'bandstop', (57, 63),
187+
n_seconds=2, remove_edges=False)
188+
189+
###################################################################################################
190+
191+
# Compute a power spectrum of the simulated signal
192+
freqs, powers_pre = trim_spectrum(*compute_spectrum(sig, fs), [3, 75])
193+
freqs, powers_post = trim_spectrum(*compute_spectrum(sig_filt, fs), [3, 75])
194+
195+
###################################################################################################
196+
197+
# Plot the spectrum of the data, pre and post bandstop filtering
198+
plot_spectra(freqs, [powers_pre, powers_post], log_powers=True,
199+
labels=['Pre-Filter', 'Post-Filter'])
200+
201+
###################################################################################################
202+
#
203+
# In the above, we can see that the the bandstop filter removes power in the filtered range,
204+
# leaving a "dip" in the power spectrum. This dip causes issues with subsequent fitting.
205+
#
206+
207+
###################################################################################################
208+
209+
# Initialize and fit a power spectrum model
210+
fm = FOOOF()
211+
fm.report(freqs, powers_post)
212+
213+
###################################################################################################
214+
#
215+
# In order to try and capture the data points in the "dip", the power spectrum model
216+
# gets 'pulled' down, leading to an inaccurate fit of the aperiodic component. This is
217+
# why fitting frequency regions that included frequency regions that have been filtered
218+
# out is not recommended.
219+
#

fooof/tests/utils/test_data.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,31 @@ def test_trim_spectrum():
2121

2222
def test_interpolate_spectrum():
2323

24+
# Test with single buffer exclusion zone
2425
freqs, powers = gen_power_spectrum(\
2526
[1, 75], [1, 1], [[10, 0.5, 1.0], [60, 2, 0.1]])
2627

27-
freqs_out, powers_out = interpolate_spectrum(freqs, powers, [58, 62])
28+
exclude = [58, 62]
29+
30+
freqs_out, powers_out = interpolate_spectrum(freqs, powers, exclude)
2831

2932
assert np.array_equal(freqs, freqs_out)
3033
assert np.all(powers)
3134
assert powers.shape == powers_out.shape
35+
mask = np.logical_and(freqs >= exclude[0], freqs <= exclude[1])
36+
assert powers[mask].sum() > powers_out[mask].sum()
37+
38+
# Test with multiple buffer exclusion zones
39+
freqs, powers = gen_power_spectrum(\
40+
[1, 150], [1, 100, 1], [[10, 0.5, 1.0], [60, 1, 0.1], [120, 0.5, 0.1]])
41+
42+
exclude = [[58, 62], [118, 122]]
43+
44+
freqs_out, powers_out = interpolate_spectrum(freqs, powers, exclude)
45+
assert np.array_equal(freqs, freqs_out)
46+
assert np.all(powers)
47+
assert powers.shape == powers_out.shape
48+
49+
for f_range in exclude:
50+
mask = np.logical_and(freqs >= f_range[0], freqs <= f_range[1])
51+
assert powers[mask].sum() > powers_out[mask].sum()

fooof/utils/data.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Utilities for working with data and models."""
22

3+
from itertools import repeat
4+
35
import numpy as np
46

57
###################################################################################################
@@ -60,9 +62,10 @@ def interpolate_spectrum(freqs, powers, interp_range, buffer=3):
6062
Frequency values for the power spectrum.
6163
powers : 1d array
6264
Power values for the power spectrum.
63-
interp_range : list of float
65+
interp_range : list of float or list of list of float
6466
Frequency range to interpolate, as [lowest_freq, highest_freq].
65-
buffer : int
67+
If a list of lists, applies each as it's own interpolation range.
68+
buffer : int or list of int
6669
The number of samples to use on either side of the interpolation
6770
range, that are then averaged and used to calculate the interpolation.
6871
@@ -101,23 +104,35 @@ def interpolate_spectrum(freqs, powers, interp_range, buffer=3):
101104
>>> freqs, powers = interpolate_spectrum(freqs, powers, [58, 62])
102105
"""
103106

104-
# Get the set of frequency values that need to be interpolated
105-
interp_mask = np.logical_and(freqs >= interp_range[0], freqs <= interp_range[1])
106-
interp_freqs = freqs[interp_mask]
107+
# If given a list of interpolation zones, recurse to apply each one
108+
if isinstance(interp_range[0], list):
109+
buffer = repeat(buffer) if isinstance(buffer, int) else buffer
110+
for interp_zone, cur_buffer in zip(interp_range, buffer):
111+
freqs, powers = interpolate_spectrum(freqs, powers, interp_zone, cur_buffer)
112+
113+
# Assuming list of two floats, interpolate a single frequency range
114+
else:
115+
116+
# Take a copy of the array, to not change original array
117+
powers = np.copy(powers)
118+
119+
# Get the set of frequency values that need to be interpolated
120+
interp_mask = np.logical_and(freqs >= interp_range[0], freqs <= interp_range[1])
121+
interp_freqs = freqs[interp_mask]
107122

108-
# Get the indices of the interpolation range
109-
ii1, ii2 = np.flatnonzero(interp_mask)[[0, -1]]
123+
# Get the indices of the interpolation range
124+
ii1, ii2 = np.flatnonzero(interp_mask)[[0, -1]]
110125

111-
# Extract & log the requested range of data to use around interpolated range
112-
xs1 = np.log10(freqs[ii1-buffer:ii1])
113-
xs2 = np.log10(freqs[ii2:ii2+buffer])
114-
ys1 = np.log10(powers[ii1-buffer:ii1])
115-
ys2 = np.log10(powers[ii2:ii2+buffer])
126+
# Extract & log the requested range of data to use around interpolated range
127+
xs1 = np.log10(freqs[ii1-buffer:ii1])
128+
xs2 = np.log10(freqs[ii2:ii2+buffer])
129+
ys1 = np.log10(powers[ii1-buffer:ii1])
130+
ys2 = np.log10(powers[ii2:ii2+buffer])
116131

117-
# Linearly interpolate, in log-log space, between averages of the extracted points
118-
vals = np.interp(np.log10(interp_freqs),
119-
[np.median(xs1), np.median(xs2)],
120-
[np.median(ys1), np.median(ys2)])
121-
powers[interp_mask] = np.power(10, vals)
132+
# Linearly interpolate, in log-log space, between averages of the extracted points
133+
vals = np.interp(np.log10(interp_freqs),
134+
[np.median(xs1), np.median(xs2)],
135+
[np.median(ys1), np.median(ys2)])
136+
powers[interp_mask] = np.power(10, vals)
122137

123138
return freqs, powers

0 commit comments

Comments
 (0)