def test_spect_slope(): """Test for :func:`compute_powercurve_deviation`. We impose the power to be written as power(f) = k1/f**theta with noise and derive a signal candidate, to which we apply the function compute_powercurve_deviation and check whether the k1 and theta estimates are correct. """ # Support of the spectrum freqs = np.fft.fftfreq(n=int(sfreq), d=1. / sfreq) # parameters k1 = 5. theta = 3. # Define the magnitude of the spectrum # such that power(f) = k1/f**a with noise mag = np.zeros((freqs.shape[0], )) mag[0] = 0 noise = rng.uniform(low=-0.01, high=0.01, size=127) pos_freqs = np.arange(1, 128) mag[pos_freqs] = (np.sqrt(k1) + noise) / (pos_freqs**(theta / 2)) mag[-pos_freqs] = mag[1:128] # From the magnitude, we get the spectrum, choosing a random phase per bin. spect = np.zeros((freqs.shape[0], ), dtype=np.complex64) spect[0] = 0 phase = rng.uniform(low=-np.pi, high=np.pi, size=127) spect[pos_freqs] = mag[pos_freqs] * np.exp(phase * 1j) spect[-pos_freqs] = np.conj(spect[pos_freqs]) # Take the inverse FFT to go back to the time domain. # The imaginary part of _sig is numerically close to 0 # by hermitian symmetry. So we obtain a real signal. _sig = np.fft.ifft(spect) sig = _sig.real n_times = sig.shape[0] # We test our estimates intercept, slope, mse, r2 = \ compute_spect_slope(sfreq=sfreq, data=sig.reshape(1, -1), with_intercept=True, psd_method='fft') # obtained by the expression ps[f] = 2 * [ (spect[f]^2) / (n_times^2) ] # and plug-in: power(f) = k1/f**theta with noise k1_estimate = 10**(intercept - np.log10(2) + 2 * np.log10(n_times)) theta_estimate = -slope np.testing.assert_almost_equal(k1, k1_estimate, decimal=1) np.testing.assert_almost_equal(theta, theta_estimate, decimal=1) assert r2 > 0.95, "Explained variance is not high enough." assert mse < 0.5, "Residual has too large standard deviation."
# Compute the (one-sided) PSD using FFT. The ``mask`` variable allows to # select only the part of the PSD which corresponds to frequencies between # 0.1Hz and 40Hz (the data used in this example is already low-pass filtered # at 40Hz). psd, freqs = power_spectrum(sfreq, data) mask = np.logical_and(0.1 <= freqs, freqs <= 40) psd, freqs = psd[0, mask], freqs[mask] # Estimate the slope (and the intercept) of the PSD. The function # :func:`compute_spect_slope` assumes that the PSD of the signal is of the # form: ``psd[f] = b / (f ** a)``. The coefficients a and b are respectively # called *slope* and *intercept* of the Power Spectral Density. The values of # the variables ``slope`` and ``intercept`` differ from the values returned # by ``compute_spect_slope`` because, in the feature function, the linear # regression fit is done in the log10-log10 scale. intercept, slope, _, _ = compute_spect_slope(sfreq, data, fmin=1., fmax=40.) print('The estimated slope (respectively intercept) is: %1.2f (resp. %1.3e)' % (slope, intercept)) # Plot the PSD together with the ``b / (f ** a)`` curve (estimated decay of # the PSD with frequency). plt.figure() plt.semilogx(freqs, np.log10(psd), '-b', lw=2, label='PSD') plt.semilogx(freqs, intercept + slope * np.log10(freqs), '-r', lw=2, label='b / (f ** a)') plt.xlabel('Frequency (Hz)') plt.ylabel('PSD (dB)') plt.xlim([1, 40])