Power spectral density#

Hide code cell source
import pynapple as nap
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
custom_params = {"axes.spines.right": False, "axes.spines.top": False}
sns.set_theme(style="ticks", palette="colorblind", font_scale=1.5, rc=custom_params)

Generating a signal#

Let’s generate a dummy signal with 2Hz and 10Hz sinusoide with white noise.

F = [2, 10]

Fs = 2000
t = np.arange(0, 200, 1/Fs)
sig = nap.Tsd(
    t=t,
    d=np.cos(t*2*np.pi*F[0])+np.cos(t*2*np.pi*F[1])+np.random.normal(0, 3, len(t)),
    time_support = nap.IntervalSet(0, 200)
    )
Hide code cell source
plt.figure()
plt.plot(sig.get(0, 0.4))
plt.title("Signal")
plt.xlabel("Time (s)")
Text(0.5, 0, 'Time (s)')
../_images/f6c282171640fe970c300b57d838a8c190f580b51b9a8b92819117a3300c42f3.png

Computing power spectral density (PSD)#

To compute a PSD of a signal, you can use the function nap.compute_power_spectral_density. With norm=True, the output of the FFT is divided by the length of the signal.

psd = nap.compute_power_spectral_density(sig, norm=True)

Pynapple returns a pandas DataFrame.

print(psd)
                          0
0.000    0.000861+0.000000j
0.005   -0.003518+0.000328j
0.010    0.002092-0.000953j
0.015   -0.005350-0.002686j
0.020   -0.003940+0.000515j
...                     ...
999.975  0.000725-0.003469j
999.980  0.002271-0.003696j
999.985  0.000349+0.001222j
999.990 -0.002320+0.005219j
999.995 -0.000531-0.006738j

[200000 rows x 1 columns]

It is then easy to plot it.

plt.figure()
plt.plot(np.abs(psd))
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude")
Text(0, 0.5, 'Amplitude')
../_images/33efc4e0c8407b90bea3931d5c71804986e9da18f08b216701d2bead11821fec.png

Note that the output of the FFT is truncated to positive frequencies. To get positive and negative frequencies, you can set full_range=True. By default, the function returns the frequencies up to the Nyquist frequency. Let’s zoom on the first 20 Hz.

plt.figure()
plt.plot(np.abs(psd))
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude")
plt.xlim(0, 20)
(0.0, 20.0)
../_images/2124f974f5698b5186d73c89d9dae666ee10cba654423f5de9a697c159fc84d7.png

We find the two frequencies 2 and 10 Hz.

By default, pynapple assumes a constant sampling rate and a single epoch. For example, computing the FFT over more than 1 epoch will raise an error.

double_ep = nap.IntervalSet([0, 50], [20, 100])

try:
    nap.compute_power_spectral_density(sig, ep=double_ep)
except ValueError as e:
    print(e)
Given epoch (or signal time_support) must have length 1

Computing mean PSD#

It is possible to compute an average PSD over multiple epochs with the function nap.compute_mean_power_spectral_density.

In this case, the argument interval_size determines the duration of each epochs upon which the FFT is computed. If not epochs is passed, the function will split the time_support.

In this case, the FFT will be computed over epochs of 10 seconds.

mean_psd = nap.compute_mean_power_spectral_density(sig, interval_size=20.0, norm=True)

Let’s compare mean_psd to psd. In both cases, the ouput is normalized.

Hide code cell source
plt.figure()
plt.plot(np.abs(psd), label='PSD')
plt.plot(np.abs(mean_psd), label='Mean PSD (10s)')
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude")
plt.legend()
plt.xlim(0, 15)
(0.0, 15.0)
../_images/151958799546c3505e75b770dd35db15dfd4a38ece06a5389c1a4b8ba887de48.png

As we can see, nap.compute_mean_power_spectral_density was able to smooth out the noise.