usingNeuroAnalyzerusingCairoMakieCairoMakie.activate!(type="png")usingFFTW# Makie figure and axis settings matching NeuroAnalyzer plotsfig_size = (900, 450)ax_style = (titlesize =18, xlabelsize =18, ylabelsize =18, xticklabelsize =12, yticklabelsize =12, xticks =LinearTicks(10), xminorticksvisible =true, xminorticks =IntervalsBetween(10), xautolimitmargin = (0, 0), yautolimitmargin = (0.1, 0.1))# for small panels of multi-panel figuresax_style_small =merge(ax_style, (titlesize =14, xticks =LinearTicks(5), xminorticks =IntervalsBetween(5)))# amplitude spectrum of a real signalfunctionamp_spectrum(x::AbstractVector, fs::Real) n =length(x)returnrfftfreq(n, fs), 2.*abs.(rfft(x)) ./ nend
Sampling converts a continuous signal (e.g. the voltage on the scalp) into a discrete signal: the signal is measured at regular time intervals by an analog-to-digital converter (ADC). Digitization has two aspects:
Sampling (discretization in time): the signal is measured every \(\Delta t\) seconds,
Quantization (discretization in amplitude): each measurement is rounded to one of a finite set of values.
Sampling rate and sampling interval
The sampling rate\(f_s\) (Hz) is the number of samples per second; the sampling interval is the time between consecutive samples:
\[
\Delta t = \frac{1}{f_s}
\]
e.g. 256 Hz → 3.9 ms, 1000 Hz → 1 ms. The sampling interval is also the precision with which the timing of events (e.g. ERP latencies) can be measured.
Nyquist Frequency and Aliasing
Nyquist–Shannon sampling theorem: a signal containing no frequencies above \(f_{max}\) is completely determined by its samples if
\[
f_s > 2 f_{max}
\]
The highest frequency that can be represented at a given sampling rate is the Nyquist frequency:
\[
f_N = \frac{f_s}{2}
\]
e.g. 128 Hz for \(f_s = 256\) Hz.
Aliasing: a frequency \(f\) above the Nyquist frequency is not lost, but appears in the sampled signal as a lower frequency (an alias): the frequency is “folded” back into the range 0 to \(f_N\):
\[
f_{alias} = \vert f - k f_s \vert
\]
where \(k\) is the integer for which \(f_{alias} \le f_N\). For example, a 100 Hz sine sampled at 80 Hz appears as a 20 Hz sine, and a 90 Hz sine sampled at 64 Hz as a 26 Hz sine.
A 9 Hz sine sampled at 50 Hz (above the Nyquist rate) and at 10 Hz (below it):
f =9# "continuous" signal: very fine time gridt_c =0:0.0005:1fig =Figure(; size = (900, 600))for (idx, fs) inenumerate((50, 10)) t_s =0:(1/ fs):1 f_alias =round(Int, abs(f -round(f / fs) * fs)) ttl ="f_s = $fs Hz"* (fs >2* f ? "":": the samples show a $f_alias Hz sine") ax =Axis(fig[idx, 1]; ax_style_small..., title = ttl, xlabel ="Time [s]", ylabel ="Amplitude")lines!(ax, t_c, sin.(2π * f .* t_c); color =:gray, label ="$f Hz signal") fs <2* f &&lines!(ax, t_c, sin.(2π * (f - fs) .* t_c); color =:red, linestyle =:dash, label ="alias ($f_alias Hz)")scatter!(ax, t_s, sin.(2π * f .* t_s); color =:black, markersize =8, label ="samples")axislegend(ax; position =:rt)endfig
At 10 Hz, the samples of the 9 Hz sine are indistinguishable from the samples of a 1 Hz sine. Once a signal has been sampled, an alias cannot be distinguished from a real low-frequency component: aliasing is irreversible.
Anti-aliasing filter: to prevent aliasing, frequencies above the Nyquist frequency must be removed before sampling, with an analog low-pass filter. EEG amplifiers have such a filter built in; its cutoff is usually set well below the Nyquist frequency (e.g. 1/3–1/4 of the sampling rate), because analog filters have a gradual roll-off.
Choosing the sampling rate
The sampling rate must exceed twice the highest frequency of interest; in practice it is set to at least 2.5–4× this frequency, to leave room for the roll-off of the anti-aliasing filter.
For EEG analyses up to ≈ 40–50 Hz, 200–256 Hz is sufficient; high-frequency activity (high gamma, ripples) requires 1000 Hz or more, and spike recordings tens of kHz.
Higher sampling rates also give a finer timing of events (e.g. 1 ms at 1000 Hz), at the cost of larger files and longer computations.
Quantization
The ADC represents each sample with a finite number of bits: an \(N\)-bit converter distinguishes \(2^N\) levels. The step between levels (the least significant bit, LSB) is:
\[
\text{LSB} = \frac{\text{input range}}{2^N}
\]
Rounding to the nearest level introduces a quantization error of at most ±½ LSB. For a full-scale sine, the resulting signal-to-quantization-noise ratio is ≈ \(6.02 N + 1.76\) dB.
A sine quantized with 3 bits (8 levels):
t =0:0.001:1x =sin.(2π *2.* t)n_bits =3lsb =2/2^n_bits# 2^n_bits levels, at the centers of the quantization stepsx_q =clamp.((floor.(x ./ lsb) .+0.5) .* lsb, -1+ lsb /2, 1- lsb /2)fig, ax =lines(t, x; color =:gray, label ="signal", figure = (size = fig_size,), axis = (ax_style..., xlabel ="Time [s]", ylabel ="Amplitude"))stairs!(ax, t, x_q; color =:red, step =:center, label ="quantized ($n_bits bits)")lines!(ax, t, x .- x_q; color =:blue, label ="quantization error")axislegend(ax; position =:rt)fig
Modern EEG amplifiers use 16–24-bit converters: with a 24-bit ADC and an input range of ±400 mV, 1 LSB is ≈ 0.05 μV, far below the amplitude of the EEG and the noise of the electrodes.
Frequency Resolution
The spectrum of a signal of \(N\) samples (duration \(T = N / f_s\)) contains frequencies from 0 to the Nyquist frequency in steps of:
\[
\Delta f = \frac{f_s}{N} = \frac{1}{T}
\]
so it has \(N/2 + 1\) frequency bins (for even \(N\)). The frequency resolution depends only on the duration of the signal, not on the sampling rate: 2 s of data give 0.5 Hz, 10 s give 0.1 Hz. The lowest non-zero frequency, \(1/T\), is also called the Rayleigh frequency: a signal must contain at least one full cycle of a frequency for that frequency to be resolved.
freqs() returns the frequencies of the spectrum and the Nyquist frequency, for a number of samples and a sampling rate or for a NEURO object:
Resampling changes the sampling rate of a recorded signal, e.g. to:
reduce the size of the data and the computation time (downsampling),
give recordings from different devices a common sampling rate,
match the sampling rate expected by a method or a model.
Downsampling
Keeping every \(k\)-th sample reduces the sampling rate \(k\) times, and with it the Nyquist frequency. Frequencies between the new and the old Nyquist frequency are then aliased, exactly as in sampling. Downsampling must therefore be preceded by a digital anti-aliasing low-pass filter with the cutoff at (or below) the new Nyquist frequency.
A signal with a 10 Hz and a 90 Hz component, sampled at 256 Hz and downsampled to 64 Hz (new Nyquist frequency: 32 Hz):
Without the filter, the 90 Hz component appears at \(\vert 90 - 64 \vert = 26\) Hz, a frequency that is not present in the signal; resample() removes it.
Upsampling
Upsampling inserts new samples between the existing ones (in practice: zeros are inserted and the result is low-pass filtered, which interpolates the signal). Upsampling does not add information: the spectrum above the original Nyquist frequency stays empty.
Resampling in NeuroAnalyzer
resample() changes the sampling rate of all channels to new_sr, calling upsample() or downsample() as needed. Resampling is done with a polyphase filter for the exact ratio of the sampling rates (e.g. 256 → 250 Hz is the ratio 125/128), with a built-in anti-aliasing low-pass filter. The time axes are recalculated; markers are stored in seconds and do not change.
for obj in (eeg, eeg_128, eeg_512, eeg_250)println(lpad(sr(obj), 4), " Hz: ",lpad(signal_len(obj), 6), " samples, ",signal_duration(obj), " s, Nyquist frequency ",freqs(obj)[2], " Hz")end
256 Hz: 308480 samples, 1205.0 s, Nyquist frequency 128.0 Hz
128 Hz: 154240 samples, 1205.0 s, Nyquist frequency 64.0 Hz
512 Hz: 616960 samples, 1205.0 s, Nyquist frequency 256.0 Hz
250 Hz: 301250 samples, 1205.0 s, Nyquist frequency 125.0 Hz
Up- and downsampling back to the original rate restores the signal (except for the frequencies above the lower Nyquist frequency):
x = eeg.data[1, :, 1]x_back =resample(eeg_512; new_sr =256).data[1, :, 1]# middle of the signal (edges excluded)m =1000:(length(x) -1000)println("relative error 256 → 512 → 256 Hz: ",round(sqrt(sum(abs2, x_back[m] .- x[m]) /sum(abs2, x[m])); sigdigits =2))
relative error 256 → 512 → 256 Hz: 0.00029
resample!(), upsample!() and downsample!() modify the object in place.
Practical notes
Resample the continuous signal, before epoching: each epoch is resampled separately, and the filter edges distort the beginning and the end of short epochs.
When downsampling, check that the frequencies of interest stay well below the new Nyquist frequency (the anti-aliasing filter attenuates frequencies near it).
Downsampling early in a pipeline (e.g. from 1000 Hz to 250 Hz for analyses below 50 Hz) speeds up all subsequent steps, e.g. ICA.
When combining recordings with different sampling rates, downsample to the lowest rate (upsampling the others adds no information) or resample all to a common rate.
Event timing is limited by the sampling interval of the resampled signal: marker times are kept, but samples exist only every \(1 / f_s\).