Convolution combines a signal with a kernel (a short weighting function): each sample of the result is a weighted sum of the neighboring samples of the signal, with the kernel as the weights. Convolution is the operation behind filtering (the kernel is the impulse response of the filter, see Filtering), smoothing, wavelet and time-frequency analysis (the kernel is a wavelet, see Wavelets) and template matching.
Definition
The convolution of a signal \(x\) (length \(N\)) with a kernel \(k\) (length \(M\)) is:
\[
(x * k)[n] = \sum_{m} x[m] \, k[n - m]
\]
For each output sample, the kernel is flipped (reversed in time), placed at position \(n\), multiplied sample by sample with the signal, and the products are summed: a sliding dot product with the flipped kernel.
For \(x = [3, 1, 4, 1, 5, 9]\) and \(k = [5, 7, 7]\), the flipped kernel is \([7, 7, 5]\):
x = [3, 1, 4, 1, 5, 9]k = [5, 7, 7]println("x * k = ", conv(x, k))println("length: ", length(conv(x, k)), " = N + M - 1 = ",length(x) +length(k) -1)
x * k = [15, 26, 48, 40, 60, 87, 98, 63]
length: 8 = N + M - 1 = 8
The kernel slides from the position where it first overlaps the signal to the position where it last overlaps it, so the full convolution has \(N + M - 1\) samples:
Signal ■ ■ ■ ■ N = 4
Kernel ◊ ◊ ◊ M = 3
◊ ◊ ◊
◊ ◊ ◊
◊ ◊ ◊
◊ ◊ ◊
◊ ◊ ◊
Result ○ ○ ● ● ○ ○ N + M - 1 = 6
● full overlap ○ partial overlap (edge effect)
“Same” convolution and edge effects
The full convolution is shifted relative to the signal: the output at the center of the kernel lies \((M - 1)/2\) samples later (the group delay of a symmetric kernel). Removing \((M - 1)/2\) samples at the start and keeping \(N\) samples gives an output of the length of the signal, aligned with it (“same” convolution). This is what tconv() and fconv() return.
Near the ends of the signal, the kernel overlaps the missing samples outside the signal (treated as zeros): the first and last \((M - 1)/2\) samples of the result are distorted (edge effects). Long kernels have long edge effects, so convolve long continuous signals and cut epochs afterwards, or pad the signal.
Convolution, cross-correlation and autocorrelation
Cross-correlation is the same sliding dot product without flipping the kernel: it measures the similarity of the signal and the kernel at each lag (e.g. template matching). For a symmetric kernel, convolution and cross-correlation are identical. Autocorrelation is the cross-correlation of a signal with itself.
Convolution as Filtering
Convolving a signal with a kernel is filtering with an FIR filter whose impulse response is the kernel. A kernel of 9 equal weights (sum 1) is a moving-average filter:
The convolution keeps the slower activity and removes fast fluctuations (a low-pass filter): the frequency response of the filter is the Fourier transform of the kernel.
Time and Frequency Domain
The convolution theorem: convolution in the time domain is multiplication in the frequency domain:
so a convolution can be computed as: FFT of the signal and of the kernel → multiply → inverse FFT. The FFT treats signals as periodic, so the product of the FFTs gives a circular convolution, in which the end of the result wraps around to its start. To get the linear convolution, both the signal and the kernel are zero-padded to at least \(N + M - 1\) samples before the FFT (padding to a power of 2 only makes the FFT faster).
NeuroAnalyzer has both:
tconv(): convolution in the time domain (direct sum),
fconv(): convolution in the frequency domain (FFT, zero-padded to \(N + M - 1\)).
Both give the same result (fconv() normalizes the kernel by default so that its maximum gain in the frequency domain is 1; with norm = false the result equals tconv()):
The time-domain convolution needs about \(N \cdot M\) operations, the frequency-domain convolution about \((N + M) \log(N + M)\): for long kernels (e.g. wavelets for low frequencies) and long signals, fconv() is much faster.
Return values
Function
Kernel
Returns
tconv()
real
a NEURO object with the convolved channels
tconv()
complex
an array of complex values (selected channels × samples × epochs)
fconv()
any
an array of complex values (selected channels × samples × epochs)
Convolution with Wavelets
A complex Morlet wavelet is a complex sine wave of frequency \(f\) multiplied by a Gaussian. Convolving a signal with it gives, at each time point, a complex number whose magnitude is proportional to the amplitude of the activity at frequency \(f\) and whose angle is its phase. generate_morlet() creates the wavelet (sampling rate, frequency, half-length in seconds, ncyc cycles):
A sine wave of amplitude \(A\) consists of two components, at \(+f\) and \(-f\), each of amplitude \(A/2\); the complex wavelet passes only the positive frequency. With the default normalization of fconv() (gain 1 at the peak of the wavelet), the magnitude is therefore \(A/2\), and the amplitude is twice the magnitude. Alpha (10 Hz) amplitude of O1:
The amplitude follows the waxing and waning of the 10 Hz activity, which is weak in this segment (the largest burst is around 12 s); angle.() gives its phase. Repeating the convolution with wavelets of different frequencies gives a time-frequency representation:
The number of cycles sets the trade-off between time and frequency resolution (more cycles: better frequency, worse time resolution). NeuroAnalyzer computes such maps directly with spectrogram() (methods :mw, :cwt …); see Wavelets.
Tip: The result of a convolution with a complex kernel is complex: use abs.() for the amplitude (scaled as described above), abs2.() for the power and angle.() for the phase. To store a convolved signal in a NEURO object, put the real part, the amplitude or the phase in its data field.