|
65 | 65 | # -----------------
|
66 | 66 | # Each basis type may necessitate specific hyperparameters for instantiation. For a comprehensive description,
|
67 | 67 | # please refer to the [Code References](../../../reference/neurostatslib/basis). After instantiation, all classes
|
68 |
| -# share the same syntax for basis evaluation. The following is an example of how to instantiate and |
69 |
| -# evaluate a log-spaced cosine raised function basis. |
| 68 | +# share the same syntax for basis evaluation. |
| 69 | +# |
| 70 | +# ### The Log-Spaced Raised Cosine Basis |
| 71 | +# The following is an example of how to instantiate and evaluate a log-spaced cosine raised function basis. |
70 | 72 |
|
71 | 73 | # Instantiate the basis noting that the `RaisedCosineBasisLog` does not require an `order` parameter
|
72 | 74 | raised_cosine_log = nsl.basis.RaisedCosineBasisLog(n_basis_funcs=10)
|
|
81 | 83 | plt.plot(samples, eval_basis)
|
82 | 84 | plt.show()
|
83 | 85 |
|
| 86 | +# %% |
| 87 | +# ### The Fourier Basis |
| 88 | +# Another type of basis available is the Fourier Basis. Fourier basis are ideal to capture periodic and |
| 89 | +# quasi-periodic patterns. Such oscillatory, rhythmic behavior is a common signature of many neural signals. |
| 90 | +# Additionally, the Fourier basis has the advantage of being orthogonal, which simplifies the estimation and |
| 91 | +# interpretation of the model parameters, each of which will represent the relative contribution of a specific |
| 92 | +# oscillation frequency to the overall signal. |
| 93 | + |
| 94 | + |
| 95 | +# A Fourier basis can be instantiated with the usual syntax. |
| 96 | +# The user can pass the desired frequencies for the basis or |
| 97 | +# the frequencies will be set to np.arange(n_basis_funcs//2). |
| 98 | +# The number of basis function is required to be even. |
| 99 | +fourier_basis = nsl.basis.FourierBasis(n_freqs=4) |
| 100 | + |
| 101 | +# evaluate on equi-spaced samples |
| 102 | +samples, eval_basis = fourier_basis.evaluate_on_grid(1000) |
| 103 | + |
| 104 | +# plot the `sin` and `cos` separately |
| 105 | +plt.figure(figsize=(6, 3)) |
| 106 | +plt.subplot(121) |
| 107 | +plt.title("Cos") |
| 108 | +plt.plot(samples, eval_basis[:, :4]) |
| 109 | +plt.subplot(122) |
| 110 | +plt.title("Sin") |
| 111 | +plt.plot(samples, eval_basis[:, 4:]) |
| 112 | +plt.tight_layout() |
| 113 | + |
| 114 | +# %% |
| 115 | +# !!! note "Fourier basis convolution and Fourier transform" |
| 116 | +# The Fourier transform of a signal $ s(t) $ restricted to a temporal window $ [t_0,\;t_1] $ is |
| 117 | +# $$ \\hat{x}(\\omega) = \\int_{t_0}^{t_1} s(\\tau) e^{-j\\omega \\tau} d\\tau. $$ |
| 118 | +# where $ e^{-j\\omega \\tau} = \\cos(\\omega \\tau) - j \\sin (\\omega \\tau) $. |
| 119 | +# |
| 120 | +# When computing the cross-correlation of a signal with the Fourier basis functions, |
| 121 | +# we essentially measure how well the signal correlates with sinusoids of different frequencies, |
| 122 | +# within a specified temporal window. This process mirrors the operation performed by the Fourier transform. |
| 123 | +# Therefore, it becomes clear that computing the cross-correlation of a signal with the Fourier basis defined here |
| 124 | +# is equivalent to computing the discrete Fourier transform on a sliding window of the same size |
| 125 | +# as that of the basis. |
| 126 | + |
| 127 | +n_samples = 1000 |
| 128 | +n_freqs = 20 |
| 129 | + |
| 130 | +# define a signal |
| 131 | +signal = np.random.normal(size=n_samples) |
| 132 | + |
| 133 | +# evaluate the basis |
| 134 | +_, eval_basis = nsl.basis.FourierBasis(n_freqs=n_freqs).evaluate_on_grid(n_samples) |
| 135 | + |
| 136 | +# compute the cross-corr with the signal and the basis |
| 137 | +# Note that we are inverting the time axis of the basis because we are aiming |
| 138 | +# for a cross-correlation, while np.convolve compute a convolution which would flip the time axis. |
| 139 | +xcorr = np.array( |
| 140 | + [ |
| 141 | + np.convolve(eval_basis[::-1, k], signal, mode="valid")[0] |
| 142 | + for k in range(2 * n_freqs - 1) |
| 143 | + ] |
| 144 | +) |
| 145 | + |
| 146 | +# compute the power (add back sin(0 * t) = 0) |
| 147 | +fft_complex = np.fft.fft(signal) |
| 148 | +fft_amplitude = np.abs(fft_complex[:n_freqs]) |
| 149 | +fft_phase = np.angle(fft_complex[:n_freqs]) |
| 150 | +# compute the phase and amplitude from the convolution |
| 151 | +xcorr_phase = np.arctan2(np.hstack([[0], xcorr[n_freqs:]]), xcorr[:n_freqs]) |
| 152 | +xcorr_aplitude = np.sqrt(xcorr[:n_freqs] ** 2 + np.hstack([[0], xcorr[n_freqs:]]) ** 2) |
| 153 | + |
| 154 | +fig, ax = plt.subplots(1, 2) |
| 155 | +ax[0].set_aspect("equal") |
| 156 | +ax[0].set_title("Signal amplitude") |
| 157 | +ax[0].scatter(fft_amplitude, xcorr_aplitude) |
| 158 | +ax[0].set_xlabel("FFT") |
| 159 | +ax[0].set_ylabel("cross-correlation") |
| 160 | + |
| 161 | +ax[1].set_aspect("equal") |
| 162 | +ax[1].set_title("Signal phase") |
| 163 | +ax[1].scatter(fft_phase, xcorr_phase) |
| 164 | +ax[1].set_xlabel("FFT") |
| 165 | +ax[1].set_ylabel("cross-correlation") |
| 166 | +plt.tight_layout() |
0 commit comments