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])+2*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/2f463dfe64e934d4014bc013abca3af966006091c8d9c1a9111cd76a88a12a47.png

Computing FFT#

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

fft = nap.compute_fft(sig, norm=True)

Pynapple returns a pandas DataFrame.

print(fft)
                          0
0.000    0.000423+0.000000j
0.005   -0.003626-0.005606j
0.010    0.004407-0.007650j
0.015   -0.002246-0.002429j
0.020   -0.004841+0.002058j
...                     ...
999.975 -0.010809-0.005787j
999.980 -0.001202-0.006112j
999.985  0.005226-0.007678j
999.990 -0.005019-0.005575j
999.995 -0.004901+0.004531j

[200000 rows x 1 columns]

It is then easy to plot it.

plt.figure()
plt.plot(np.abs(fft))
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude")
Text(0, 0.5, 'Amplitude')
../_images/a66d378817aee991a61977463b7eee350324afaea42d3263087960911d9c4b63.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(fft))
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude")
plt.xlim(0, 20)
(0.0, 20.0)
../_images/005cfc04200586b2f5bc127f4dedb9d7fe36d795bd644a797946fab463d3b721.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_fft(sig, ep=double_ep)
except ValueError as e:
    print(e)
Given epoch (or signal time_support) must have length 1

Computing power spectral density (PSD)#

Power spectral density can be returned through the function compute_power_spectral_density. Contrary to compute_fft, the output is real-valued.

psd = nap.compute_power_spectral_density(sig, fs=Fs)

The output is also a pandas DataFrame.

plt.figure()
plt.plot(psd)
plt.xlabel("Frequency (Hz)")
plt.ylabel("Power/Frequency")
plt.xlim(0, 20)
(0.0, 20.0)
../_images/40390adf92382b5a4eca52066c964ffa231692faf24d111a903bcff4f258c2cc.png

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 20 seconds.

mean_psd = nap.compute_mean_power_spectral_density(sig, interval_size=20.0, fs=Fs)

Let’s compare mean_psd to psd. In both cases, the output is normalized and converted to dB.

Hide code cell source

plt.figure()
plt.plot(10*np.log10(psd), label='PSD')
plt.plot(10*np.log10(mean_psd), label='Mean PSD (20s)')

plt.ylabel("Power/Frequency (dB/Hz)")
plt.legend()
plt.xlim(0, 20)
plt.xlabel("Frequency (Hz)")
Text(0.5, 0, 'Frequency (Hz)')
../_images/ab2be36630d974fa7386ee654bb25f57fc41b180c4ba4194d80f9cc82644c3eb.png

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