Getting started#

This notebook shows the basic usage of the PyRASA package.

[1]:
from neurodsp.sim import sim_combined
from neurodsp.utils import create_times
import numpy as np
import scipy.signal as dsp
import matplotlib.pyplot as plt
import pandas as pd

#add random seed for reproducibility of readme example
np.random.seed(42)

We first simulate a signal with a single oscillation and a spectral slope of -1

[3]:
fs = 1000
n_seconds = 60
duration=4
overlap=0.5

sim_components = {'sim_powerlaw': {'exponent' : -1},
                  'sim_oscillation': {'freq' : 10}}


sig = sim_combined(n_seconds=n_seconds, fs=fs, components=sim_components)
times = create_times(n_seconds=n_seconds, fs=fs)

max_times = times < 1
f, axes = plt.subplots(ncols=2, figsize=(8, 4))
axes[0].plot(times[max_times], sig[max_times])
axes[0].set_ylabel('Amplitude (a.u.)')
axes[0].set_xlabel('Time (s)')
freq, psd = dsp.welch(sig, fs=fs, nperseg=duration*fs, noverlap=duration*fs*overlap)
axes[1].loglog(freq, psd)
axes[1].set_ylabel('Power (a.u.)')
axes[1].set_xlabel('Frequency (Hz)')

plt.tight_layout()
../_images/examples_basic_functionality_3_0.png

Now we can use IRASA to seperate the signal in its periodic and aperiodic components

[4]:
from pyrasa import irasa

irasa_out = irasa(sig,
                    fs=fs,
                    band=(1, 100),
                    nperseg = duration*fs,
                    noverlap = duration*fs*overlap,
                    hset_info=(1, 2, 0.05))

f, axes = plt.subplots(ncols=2, figsize=(8, 4))

axes[0].plot(irasa_out.freqs, irasa_out.periodic[0,:])
axes[1].loglog(irasa_out.freqs, irasa_out.aperiodic[0,:])

for ax, title in zip(axes, ['Periodic', 'Aperiodic']):
    ax.set_title(title)
    ax.set_ylabel('Power (a.u.)')
    ax.set_xlabel('Frequency (Hz)')

f.tight_layout()

../_images/examples_basic_functionality_5_0.png

We can also use a convenience plotting function to visually inspect the separation of periodic and aperiodic activity using the IRASA method

[5]:
irasa_out.plot(log_x=False,
               freq_range=(5, 20),
               average_chs=False)
../_images/examples_basic_functionality_7_0.png

Lets check whats stored in the IrasaSpectrum returned by irasa

Now we can further analyse the periodic and aperiodic components using the get_peak_params and compute_slope functions, which will return pandas dataframes containing specific information about the slope or the oscillatory parameters.

[6]:
# %% get periodic stuff
irasa_out.get_peaks()
[6]:
ch_name cf bw pw
0 0 10.0 1.188439 0.506288
[7]:
# %% get aperiodic stuff
aperiodic_fit = irasa_out.fit_aperiodic_model()
aperiodic_fit.aperiodic_params
[7]:
Offset Exponent fit_type ch_name
0 -1.330567 1.014195 fixed 0
[8]:
aperiodic_fit.gof
[8]:
mse R2 R2_adj. BIC BIC_adj. AIC fit_type ch_name
0 0.000386 0.997517 0.997505 -35.059446 -41.405504 -43.027319 fixed 0

But how does all of this work in practice? The beauty of IRASA lies in its simplicity. Essentially, its just up/downsampling and averaging. I will deconstruct the algorithm below to show you its inner workings and highlight potential pitfalls.

We start by simply computing a psd

[9]:
kwargs_psd = {'nperseg': duration*fs,
              'noverlap': duration*fs*overlap}

freq, psd = dsp.welch(sig, fs=fs, **kwargs_psd)

f, ax = plt.subplots(figsize=(4,4))
ax.loglog(freq, psd)
ax.set_ylabel('Power')
ax.set_xlabel('Frequency (Hz)')
[9]:
Text(0.5, 0, 'Frequency (Hz)')
../_images/examples_basic_functionality_14_1.png

Now we need to create two other psds from an up-/downsampled version of the data. Note that the data is up-/downsampled by the same factor

[10]:
import fractions

resampling_factor = 1.5

def simple_rasa(resampling_factor):
    rat = fractions.Fraction(str(resampling_factor))
    up, down = rat.numerator, rat.denominator

    # Much faster than FFT-based resampling
    data_up = dsp.resample_poly(sig, up, down, axis=-1)
    data_down = dsp.resample_poly(sig, down, up, axis=-1)

    # Calculate an up/downsampled version of the PSD using same params as original
    _, psd_up = dsp.welch(data_up, fs * resampling_factor,  **kwargs_psd)
    _, psd_dw = dsp.welch(data_down, fs / resampling_factor, **kwargs_psd)

    return psd_up, psd_dw


psd_up, psd_dw = simple_rasa(resampling_factor)

f, ax = plt.subplots(figsize=(4,4))
f_max = freq < 100
ax.loglog(freq[f_max], psd[f_max], color='k', label='original')
ax.loglog(freq[f_max], psd_up[f_max], color='b', label='upsampled (factor = 1.5)')
ax.loglog(freq[f_max], psd_dw[f_max], color='r', label='downsampled (factor = 1.5)')
ax.set_ylabel('Power')
ax.set_xlabel('Frequency (Hz)')
plt.legend()
[10]:
<matplotlib.legend.Legend at 0x164f67c50>
../_images/examples_basic_functionality_16_1.png

We can see that up-/downsampling shifted the peak (oscillation) in the spectrum relative to the original data. Now we compute the geometric mean of the up-/downsampled version of the data and repeat the procedure for a different resampling factor. We can see that this creates a version of the original data with 2 peaks that are shifted around the original peak by a factor of x.

[11]:

psd_up_19, psd_dw_19 = simple_rasa(1.9) psd_up_11, psd_dw_11 = simple_rasa(1.1) gmean = lambda x, y : np.sqrt(x * y) f, ax = plt.subplots(figsize=(4,4)) f_max = freq < 100 ax.loglog(freq[f_max], psd[f_max], label='original') ax.loglog(freq[f_max], gmean(psd_up_11, psd_dw_11)[f_max], label='factor 1.1') ax.loglog(freq[f_max], gmean(psd_up, psd_dw)[f_max], label='factor 1.5') ax.loglog(freq[f_max], gmean(psd_up_19, psd_dw_19)[f_max], label='factor 1.9') ax.set_ylabel('Power') ax.set_xlabel('Frequency (Hz)') plt.legend()
[11]:
<matplotlib.legend.Legend at 0x16612fd90>
../_images/examples_basic_functionality_18_1.png

Now we can compute the median, between our 3 geometric means to obtain our aperiodic spectrum. In reality we use a bit more than 3 up-/downsampling factors, but on this data its enough to get a decent spectrum

[12]:
aperiodic_spectrum = np.median([gmean(psd_up_11, psd_dw_11),
                                gmean(psd_up, psd_dw),
                                gmean(psd_up_19, psd_dw_19)], axis=0)

f, ax = plt.subplots(figsize=(4,4))
f_max = freq < 100
ax.loglog(freq[f_max], psd[f_max], label='original')
ax.loglog(freq[f_max], aperiodic_spectrum[f_max], label='aperiodic spectrum')
ax.set_ylabel('Power')
ax.set_xlabel('Frequency (Hz)')

plt.legend()
[12]:
<matplotlib.legend.Legend at 0x166397c50>
../_images/examples_basic_functionality_20_1.png

But how do we get the periodic spectrum? Its actually quite simple. We just need to subtract our aperiodic spectrum from the original spectrum.

[13]:
f, ax = plt.subplots(figsize=(4,4))
f_max = freq < 100
ax.plot(freq[f_max], psd[f_max] - aperiodic_spectrum[f_max], label='periodic spectrum')
ax.set_ylabel('Power')
ax.set_xlabel('Frequency (Hz)')

plt.legend()
[13]:
<matplotlib.legend.Legend at 0x16576bc90>
../_images/examples_basic_functionality_22_1.png

Now we can do slope fits on the aperiodic spectrum or do peak detection on the periodic spectrum. However, this was quite a simple spectrum and reality is usually much messsier and noisier. For instance we might get spectra that dont linearly decrease with frequency by the same value but have a deflection point (spectral knee) at which the slope starts changing. We can also deal with those using IRASA. See below for how we would do this.

[14]:
# %% Lets check the knee
knee_freq = 15
exp = 1.5
knee = knee_freq ** exp
duration=4
overlap=0.99

sim_components = {'sim_knee': {'exponent1' : -.0, 'exponent2': -1*exp, 'knee':knee },
                  'sim_oscillation': {'freq' : 10}}

sig = sim_combined(n_seconds=n_seconds, fs=fs, components=sim_components)

# %%
max_times = times < 1
f, axes = plt.subplots(ncols=2, figsize=(8, 4))
axes[0].plot(times[max_times], sig[max_times])
axes[0].set_ylabel('Amplitude (a.u.)')
axes[0].set_xlabel('Time (s)')
freq, psd = dsp.welch(sig, fs=fs, nperseg=duration*fs, noverlap=duration*fs*overlap)
axes[1].loglog(freq, psd)
axes[1].set_ylabel('Power (a.u.)')
axes[1].set_xlabel('Frequency (Hz)')

plt.tight_layout()
../_images/examples_basic_functionality_24_0.png
[52]:
irasa_out = irasa(sig,
                    fs=fs,
                    band=(.1, 100),
                    nperseg=duration*fs,
                    noverlap=duration*fs*overlap,
                    average='mean',
                    detrend='linear',
                    hset_info=(1, 2., 0.05))
[53]:
f, axes = plt.subplots(ncols=2, figsize=(8, 4))
axes[0].plot(irasa_out.freqs, irasa_out.periodic[0,:])
axes[0].set_ylabel('Power (a.u.)')
axes[0].set_xlabel('Frequency (Hz)')
axes[0].set_title('Periodic Spectrum')


axes[1].loglog(freq[1:], psd[1:], label='psd')
axes[1].loglog(irasa_out.freqs, irasa_out.aperiodic[0,:])
axes[1].set_ylabel('Power (a.u.)')
axes[1].set_xlabel('Frequency (Hz)')
axes[1].set_title('Original + \n Aperiodic Spectrum')

f.tight_layout()
f.savefig('../simulations/example_knee.png')
../_images/examples_basic_functionality_26_0.png
[54]:
# %% get periodic stuff
irasa_out.get_peaks()
[54]:
ch_name cf bw pw
0 0 10.0 1.188434 0.513117
[55]:
aps = irasa_out.fit_aperiodic_model(fit_func='knee').aperiodic_params
aps
[55]:
Offset Knee Exponent_1 Exponent_2 fit_type Knee Frequency (Hz) tau ch_name
0 2.415560e-13 64.458932 0.035615 1.481816 knee 14.621432 0.010885 0
[57]:
# %% get aperiodic stuff
ap_f = irasa_out.fit_aperiodic_model(fit_func='fixed')
ap_k = irasa_out.fit_aperiodic_model(fit_func='knee')
[58]:
pd.concat([ap_f.gof, ap_k.gof])
[58]:
mse R2 R2_adj. BIC BIC_adj. AIC fit_type ch_name
0 0.014817 0.884090 0.883507 -13.253004 -19.599137 -21.235933 fixed 0
0 0.000102 0.999205 0.999197 -31.124177 -43.816442 -47.090035 knee 0
[59]:
f, ax = plt.subplots(figsize=(4,4))
ax.loglog(ap_k.model['Frequency (Hz)'], ap_k.model['aperiodic_model'])
ax.set_xlabel('Frequency (Hz)')
ax.set_ylabel('Power (a.u.)')
[59]:
Text(0, 0.5, 'Power (a.u.)')
../_images/examples_basic_functionality_31_1.png
[60]:
ap_k
[60]:
AperiodicFit(aperiodic_params=         Offset       Knee  Exponent_1  Exponent_2 fit_type  \
0  2.415560e-13  64.458932    0.035615    1.481816     knee

   Knee Frequency (Hz)       tau ch_name
0            14.621432  0.010885       0  , gof=        mse        R2   R2_adj.        BIC   BIC_adj.        AIC fit_type  \
0  0.000102  0.999205  0.999197 -31.124177 -43.816442 -47.090035     knee

  ch_name
0       0  , model=     Frequency (Hz)  aperiodic_model fit_type ch_name
0              0.25         0.016267     knee       0
1              0.50         0.015814     knee       0
2              0.75         0.015516     knee       0
3              1.00         0.015277     knee       0
4              1.25         0.015066     knee       0
..              ...              ...      ...     ...
395           99.00         0.000875     knee       0
396           99.25         0.000872     knee       0
397           99.50         0.000869     knee       0
398           99.75         0.000865     knee       0
399          100.00         0.000862     knee       0

[400 rows x 4 columns])
[ ]: