-
Notifications
You must be signed in to change notification settings - Fork 105
[DOC] - Add a line noise example #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Processing | ||
---------- | ||
|
||
Examples on how to process data related to spectral parameterization. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
""" | ||
Dealing with Line Noise | ||
======================= | ||
|
||
This example covers strategies for dealing with line noise. | ||
""" | ||
|
||
################################################################################################### | ||
|
||
# sphinx_gallery_thumbnail_number = 2 | ||
|
||
# Import the spectral parameterization object and utilities | ||
from fooof import FOOOF | ||
from fooof.plts import plot_spectrum, plot_spectra | ||
from fooof.utils import trim_spectrum, interpolate_spectrum | ||
|
||
# Import simulation functions to create some example data | ||
from fooof.sim.gen import gen_power_spectrum | ||
|
||
# Import NeuroDSP functions for simulating & processing time series | ||
from neurodsp.sim import sim_combined | ||
from neurodsp.filt import filter_signal | ||
from neurodsp.spectral import compute_spectrum | ||
|
||
################################################################################################### | ||
# Line Noise Peaks | ||
# ---------------- | ||
# | ||
# Neural recordings typically have power line artifacts, at either 50 or 60 Hz, depending on | ||
# where the data were collected, which can impact spectral parameterization. | ||
# | ||
# In this example, we explore some options for dealing with line noise artifacts. | ||
# | ||
# Interpolating Line Noise Peaks | ||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
# | ||
# One approach is to interpolate away line noise peaks, in the frequency domain. This | ||
# approach simply gets rid of the peaks, interpolating the data to maintain the 1/f | ||
# character of the data, allowing for subsequent fitting. | ||
# | ||
# The :func:`~fooof.utils.interpolate_spectrum` function allows for doing simple | ||
# interpolation. Given a narrow frequency region, this function interpolates the spectrum, | ||
# such that the 'peak' of the line noise is removed. | ||
# | ||
|
||
################################################################################################### | ||
|
||
# Generate an example power spectrum, with line noise | ||
freqs1, powers1 = gen_power_spectrum([3, 75], [1, 1], | ||
[[10, 0.75, 2], [60, 1, 0.5]]) | ||
|
||
# Visualize the generated power spectrum | ||
plot_spectrum(freqs1, powers1, log_powers=True) | ||
|
||
################################################################################################### | ||
# | ||
# In the plot above, we have an example spectrum, with some power line noise. | ||
# | ||
# To prepare this data for fitting, we can interpolate away the line noise region. | ||
# | ||
|
||
################################################################################################### | ||
|
||
# Interpolate away the line noise region | ||
interp_range = [58, 62] | ||
freqs_int1, powers_int1 = interpolate_spectrum(freqs1, powers1, interp_range) | ||
|
||
################################################################################################### | ||
|
||
# Plot the spectra for the power spectra before and after interpolation | ||
plot_spectra(freqs1, [powers1, powers_int1], log_powers=True, | ||
labels=['Original Spectrum', 'Interpolated Spectrum']) | ||
|
||
################################################################################################### | ||
# | ||
# As we can see in the above, the interpolation removed the peak from the data. | ||
# | ||
# We can now go ahead and parameterize the spectrum. | ||
# | ||
|
||
################################################################################################### | ||
|
||
# Initialize a power spectrum model | ||
fm1 = FOOOF(verbose=False) | ||
fm1.report(freqs_int1, powers_int1) | ||
|
||
################################################################################################### | ||
# Multiple Interpolation Regions | ||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
# | ||
# Line noise artifacts often also display harmonics, such that when analyzing broader | ||
# frequency ranges, there may be multiple peaks that need to be interpolated. | ||
# | ||
# This can be done by passing in multiple interpolation regions to | ||
# :func:`~fooof.utils.interpolate_spectrum`, which we will do in the next example. | ||
# | ||
|
||
################################################################################################### | ||
|
||
# Generate an example power spectrum, with line noise & harmonics | ||
freqs2, powers2 = gen_power_spectrum([1, 150], [1, 500, 1.5], | ||
[[10, 0.5, 2], [60, 0.75, 0.5], [120, 0.5, 0.5]]) | ||
|
||
# Interpolate away the line noise region & harmonics | ||
interp_ranges = [[58, 62], [118, 122]] | ||
freqs_int2, powers_int2 = interpolate_spectrum(freqs2, powers2, interp_ranges) | ||
|
||
################################################################################################### | ||
|
||
# Plot the power spectrum before and after interpolation | ||
plot_spectra(freqs2, [powers2, powers_int2], log_powers=True, | ||
labels=['Original Spectrum', 'Interpolated Spectrum']) | ||
|
||
################################################################################################### | ||
|
||
# Parameterize the interpolated power spectrum | ||
fm2 = FOOOF(aperiodic_mode='knee', verbose=False) | ||
fm2.report(freqs2, powers_int2) | ||
|
||
################################################################################################### | ||
# Fitting Line Noise as Peaks | ||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
# | ||
# In some cases, you may also be able to simply allow the parameterization to peaks to the | ||
# line noise and harmonics. By simply fitting the line noise as peaks, the model can deal | ||
# with the peaks in order to accurately fit the aperiodic component. | ||
# | ||
# These peaks are of course not to be analyzed, but once the model has been fit, you can | ||
# simply ignore them. There should generally be no issue with fitting and having them in | ||
# the model, and allowing the model to account for these peaks typically helps the model | ||
# better fit the rest of the data. | ||
# | ||
# Below we can see that the model does indeed work when fitting data with line noise peaks. | ||
# | ||
|
||
################################################################################################### | ||
|
||
# Fit power spectrum models to original spectra | ||
fm1.report(freqs1, powers1) | ||
fm2.report(freqs2, powers2) | ||
|
||
################################################################################################### | ||
# The Problem with Bandstop Filtering | ||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
# | ||
# A common approach for getting rid of line noise activity is to use bandstop filtering to | ||
# remove activity at the line noise frequencies. Such a filter effectively set the power | ||
# of these frequencies to be approximately zero. | ||
# | ||
# Unfortunately, this doesn't work very well with spectral parameterization, since the | ||
# parameterization algorithm tries to fit each power value as either part of the aperiodic | ||
# component, or as an overlying peak. Frequencies that have filtered out are neither, and | ||
# the model has trouble, as it and has no concept of power values below the aperiodic component. | ||
# | ||
# In practice, this means that the "missing" power will impact the fit, and pull down the | ||
# aperiodic component. One way to think of this is that the power spectrum model can deal with, | ||
# and even expects, 'positive outliers' above the aperiodic (these are considered 'peaks'), but | ||
# not 'negative outliers', or values below the aperiodic, as there is no expectation of this | ||
# happening in the model. | ||
# | ||
# In the following example, we can see how bandstop filtering negatively impacts fitting. | ||
# Because of this, for the purposes of spectral parameterization, bandstop filters are not | ||
# recommended as a way to remove line noise. | ||
# | ||
# Note that if one has already applied a bandstop filter, then you can still | ||
# apply the interpolation from above. | ||
# | ||
|
||
################################################################################################### | ||
|
||
# General settings for the simulation | ||
n_seconds = 30 | ||
fs = 1000 | ||
|
||
# Define the settings for the simulated signal | ||
components = {'sim_powerlaw' : {'exponent' : -1.5}, | ||
'sim_oscillation' : [{'freq' : 10}, {'freq' : 60}]} | ||
comp_vars = [0.5, 1, 1] | ||
|
||
# Simulate a time series | ||
sig = sim_combined(n_seconds, fs, components, comp_vars) | ||
|
||
################################################################################################### | ||
|
||
# Bandstop filter the signal to remove line noise frequencies | ||
sig_filt = filter_signal(sig, fs, 'bandstop', (57, 63), | ||
n_seconds=2, remove_edges=False) | ||
|
||
################################################################################################### | ||
|
||
# Compute a power spectrum of the simulated signal | ||
freqs, powers_pre = trim_spectrum(*compute_spectrum(sig, fs), [3, 75]) | ||
freqs, powers_post = trim_spectrum(*compute_spectrum(sig_filt, fs), [3, 75]) | ||
|
||
################################################################################################### | ||
|
||
# Plot the spectrum of the data, pre and post bandstop filtering | ||
plot_spectra(freqs, [powers_pre, powers_post], log_powers=True, | ||
labels=['Pre-Filter', 'Post-Filter']) | ||
|
||
################################################################################################### | ||
# | ||
# In the above, we can see that the the bandstop filter removes power in the filtered range, | ||
# leaving a "dip" in the power spectrum. This dip causes issues with subsequent fitting. | ||
# | ||
|
||
################################################################################################### | ||
|
||
# Initialize and fit a power spectrum model | ||
fm = FOOOF() | ||
fm.report(freqs, powers_post) | ||
|
||
################################################################################################### | ||
# | ||
# In order to try and capture the data points in the "dip", the power spectrum model | ||
# gets 'pulled' down, leading to an inaccurate fit of the aperiodic component. This is | ||
# why fitting frequency regions that included frequency regions that have been filtered | ||
# out is not recommended. | ||
# |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
"""Utilities for working with data and models.""" | ||
|
||
from itertools import repeat | ||
|
||
import numpy as np | ||
|
||
################################################################################################### | ||
|
@@ -60,9 +62,10 @@ def interpolate_spectrum(freqs, powers, interp_range, buffer=3): | |
Frequency values for the power spectrum. | ||
powers : 1d array | ||
Power values for the power spectrum. | ||
interp_range : list of float | ||
interp_range : list of float or list of list of float | ||
Frequency range to interpolate, as [lowest_freq, highest_freq]. | ||
buffer : int | ||
If a list of lists, applies each as it's own interpolation range. | ||
buffer : int or list of int | ||
The number of samples to use on either side of the interpolation | ||
range, that are then averaged and used to calculate the interpolation. | ||
|
||
|
@@ -101,23 +104,35 @@ def interpolate_spectrum(freqs, powers, interp_range, buffer=3): | |
>>> freqs, powers = interpolate_spectrum(freqs, powers, [58, 62]) | ||
""" | ||
|
||
# Get the set of frequency values that need to be interpolated | ||
interp_mask = np.logical_and(freqs >= interp_range[0], freqs <= interp_range[1]) | ||
interp_freqs = freqs[interp_mask] | ||
# If given a list of interpolation zones, recurse to apply each one | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After working on the #198, do you think it would be useful to generalize There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a great idea! Just like the other util function, Do you want to try out generalizing the interpolation to 2D? It can go into a new PR I think. |
||
if isinstance(interp_range[0], list): | ||
buffer = repeat(buffer) if isinstance(buffer, int) else buffer | ||
for interp_zone, cur_buffer in zip(interp_range, buffer): | ||
freqs, powers = interpolate_spectrum(freqs, powers, interp_zone, cur_buffer) | ||
TomDonoghue marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Assuming list of two floats, interpolate a single frequency range | ||
else: | ||
|
||
# Take a copy of the array, to not change original array | ||
powers = np.copy(powers) | ||
|
||
# Get the set of frequency values that need to be interpolated | ||
interp_mask = np.logical_and(freqs >= interp_range[0], freqs <= interp_range[1]) | ||
interp_freqs = freqs[interp_mask] | ||
|
||
# Get the indices of the interpolation range | ||
ii1, ii2 = np.flatnonzero(interp_mask)[[0, -1]] | ||
# Get the indices of the interpolation range | ||
ii1, ii2 = np.flatnonzero(interp_mask)[[0, -1]] | ||
|
||
# Extract & log the requested range of data to use around interpolated range | ||
xs1 = np.log10(freqs[ii1-buffer:ii1]) | ||
xs2 = np.log10(freqs[ii2:ii2+buffer]) | ||
ys1 = np.log10(powers[ii1-buffer:ii1]) | ||
ys2 = np.log10(powers[ii2:ii2+buffer]) | ||
# Extract & log the requested range of data to use around interpolated range | ||
xs1 = np.log10(freqs[ii1-buffer:ii1]) | ||
xs2 = np.log10(freqs[ii2:ii2+buffer]) | ||
ys1 = np.log10(powers[ii1-buffer:ii1]) | ||
ys2 = np.log10(powers[ii2:ii2+buffer]) | ||
|
||
# Linearly interpolate, in log-log space, between averages of the extracted points | ||
vals = np.interp(np.log10(interp_freqs), | ||
[np.median(xs1), np.median(xs2)], | ||
[np.median(ys1), np.median(ys2)]) | ||
powers[interp_mask] = np.power(10, vals) | ||
# Linearly interpolate, in log-log space, between averages of the extracted points | ||
vals = np.interp(np.log10(interp_freqs), | ||
[np.median(xs1), np.median(xs2)], | ||
[np.median(ys1), np.median(ys2)]) | ||
powers[interp_mask] = np.power(10, vals) | ||
|
||
return freqs, powers |
Uh oh!
There was an error while loading. Please reload this page.