Initialize NeuroAnalyzer
using NeuroAnalyzer
using CairoMakie
CairoMakie.activate!(type = "png")
using StatsKit
using FFTWusing NeuroAnalyzer
using CairoMakie
CairoMakie.activate!(type = "png")
using StatsKit
using FFTWA practical guide to signals, systems, and the mathematical foundations of neural signal processing.
A system transforms an input signal into an output signal:
\[ \text{input} \rightarrow \text{H} \rightarrow \text{output} \]
For continuous signal: \(y[t] = H(x[t])\). For a discrete signal: \(y[n] = H(x[n])\).
The snippet below overlays a continuous sine with its sampled (discrete) version:
t = linspace(0, 2pi, 100)
s = sin.(t)
fig, ax = lines(t, s)
stem!(ax, t[1:2:end], s[1:2:end])
figA system is linear if it obeys two properties simultaneously:
A time-invariant system behaves identically regardless of when the input arrives: a temporal shift at the input produces an equal shift at the output.
\[ x[n] \rightarrow H \rightarrow y[n] \Leftrightarrow x[n - 1] \rightarrow H \rightarrow y[n - 1] \]
A system that is both linear and time-invariant is an LTI system. Most real-world systems are approximated as LTI - the model is tractable and often accurate enough for practical use.
A system is memory-less when its output at time \(n\) depends only on its input at that same instant:
Memory-less system:
\[ y[n] = x[n]^2 \]
System that has memory:
\[ y[n] = x[n - 1] \]
A causal system’s output depends only on present and past inputs - never future ones.
Causal system:
\[ y[n] = x[n] - 2 \times x[n - 1] \]
Non-causal:
\[ y[n] = x[n + 3] \]
Causal system cannot anticipate future inputs.
Example: A 3-sample moving average using samples \(n-1\), \(n\) and \(n+1\) is non-causal - it requires a future value.
A signal is any quantity measured at regular intervals. Signals are either continuous (defined at every instant) or discrete (sampled at fixed intervals).
The unit impulse, also known as the delta function or Dirac delta function, is a fundamental concept in signal processing and systems analysis. In discrete-time systems, it is defined as follows:
Mathematical Definition
\[ \delta[n] = \begin{cases} 1 & \text{if $n = 0$}\\ 0 & \text{otherwise} \end{cases} \]
where:
\(\delta[n]\) is the unit impulse function,
\(n\) is the discrete-time index.
A unit spike at \(n = 0\). Any discrete signal can be expressed as a weighted sum of shifted impulses.
Impulse Response:
In linear time-invariant (LTI) systems, the impulse response \(h[n]\) is the output of the system when the input is a unit impulse \(\delta[n]\). This response characterizes the system completely.
The unit step function, often denoted as \(u[n]\), is a fundamental concept in discrete-time signal processing.
Mathematical Definition
\[ u[n] = \begin{cases} 1 & \text{if $n \geq 0$}\\ 0 & \text{otherwise} \end{cases} \]
where:
\(u[n]\) is the unit step function,
\(n\) is the discrete-time index.
Equivalently, a running sum of shifted deltas:
\[ u[n] = \sum_{k=0}^{\infty} \delta[n - k] = \sum_{k=-\infty}^{n} \delta[k] \]
\[ \text{time series} = \text{signal} + \text{noise} \]
A time series is the raw sequence of samples; a signal is the meaningful component within it. In EEG, the signal is voltage (µV) varying over time (ms).
Sampling rate: Samples per second. Also the number of zero-crossings per second × 0.5.
\[ f = 1 / T \]
where:
\(f\) is the frequency: cycles per second (Hz),
\(T\) is the period: length of one period (cycle) (s).
Sampling interval: Time between consecutive samples: \(1 / f\).
Fundamental frequency: The lowest frequency component present.
Harmonics: Integer multiples of the fundamental frequency.
Octave: A doubling in frequency.
Dominant frequency: The component with the highest amplitude.
x = -1:0.1:1
# unit step
us = Int64.(x .>= 0)
# reversed unit step
reversed_us = Int64.(x .<= 0)
# use US to gate a signal on/off
new_signal = signal .* us
# impulse
imp = Int64.(x .== 0)
# ramp
ramp = Int64.(x .* us)
# quadratic
quad = Int64.(x .^ 2 .* us)
# exponential (x >= 0)
y = exp.(-x)
# half-wave rectifier
hwr = y .* Int64.(y .> 0)
# full-wave rectifier
fwr = abs.(y)Every signal can be categorized along the following axes:
Periodic signal: Repeats with the smallest period \(T\) satisfying \(s(t + T) = s(t)\). Its frequency is a rational number.
Even signal: Symmetric about the origin: \(s(t) = s(-t)\).
Odd signal: Anti-symmetric about the origin: \(s(t) = -s(-t)\).
Amplification: \(k \times s(t)\) for \(k > 1\) - scales magnitude up.
Attenuation: \(k \times s(t)\) for \(0 < k < 1\) - scales magnitude down.
Shifting: \(s(t - t_0)\) - shifts right; \(s(t + t_0)\) - shifts left.
Scaling: \(s(k \times t)\) squeezes the signal when \(0 < k < 1\) and stretches when \(k > 1\).
Combination: Signals can be added \(a(t) + a(t)\) or multiplied \(a(t) \times a(t)\) sample-by-sample.
A feedback loop routes part of a system’s output back to its input. Feedback can stabilise an unstable system - but it can equally introduce instability if the loop gain is not carefully managed. Real-world designs must be analyzed to verify stability before deployment.
Any oscillation is fully characterized by three quantities:
Amplitude measures the extent of oscillation. Four common measures:

Peak amplitude measures the strength of individual EEG components (e.g., amplitude of an ERP or spike). It is useful for detecting abnormal spikes (e.g., epileptic discharges).
Peak-to-peak amplitude reflects the overall variability of the EEG signal (e.g., alpha or beta wave amplitude). It is used to assess signal-to-noise ratio or amplitude modulation in brain rhythms.
RMS amplitude of a non-pure sinusoidal signal is a statistical measure of the magnitude of a varying signal, such as EEG data. It calculates the square root of the average of the squared values of the signal over a given time window. RMS amplitude provides a single value that represents the effective amplitude of the signal, accounting for both positive and negative fluctuations.
For a discrete signal \(s\) with \(N\) samples:
\[ \text{RMS Amplitude} = \sqrt{\frac{1}{N} \sum_{i=1}^{N} s_i^2} \]
where:
\(s_i\) is the amplitude of the signal at sample \(i\),
\(N\) is the total number of samples in the signal.
Phase is 0° when the sine wave’s peaks align with the edges of the analysis window - matching a cosine shape. Other phases are measured relative to this. A rightward time shift decreases phase; a leftward shift increases it.
The general sine wave is:
\[ y = a \times \sin(2\pi \times f \times t + \theta) \]
Replacing \(t\) with \(t + t_0\) shifts the waveform in time, which is equivalent to adding a phase offset.
t = 0:0.01:2pi
s1 = sin.(2pi * 0.5 .* t)
# shifted left in time
s2 = sin.(2pi * 0.5 .* (t .+ 0.5))
# shifted right in time
s3 = sin.(2pi * 0.5 .* (t .- 0.5))
fig, ax = lines(t, s1, color = :black, label = "original")
lines!(ax, t, s2, color = :red, label = "t + 0.5")
lines!(ax, t, s3, color = :blue, label = "t - 0.5")
axislegend(ax, position = :rt)
figWhite noise: Equal power at all frequencies (flat spectrum). Can be uniformly or normally distributed.
Gaussian white noise:
fs = 100
t = 0:1/fs:10
gwn = randn(length(t))
NeuroAnalyzer.plot(t, gwn, title = "Gaussian White Noise")Amplitude histogram:
plot_histogram(gwn,
title = "Amplitude Distribution",
xlabel = "Amplitude",
ylabel = "Count",
draw_median = false)PSD:
# Fourier transform
X = ftransform(gwn)
# f will contain vector of frequencies
f, _ = freqs(gwn, fs)
plot_psd(f, X.p, flim = (1, 40))Uniform white noise:
fs = 100
t = 0:1/fs:10
uwn = rand(length(t))
NeuroAnalyzer.plot(t, uwn, title = "Uniform White Noise")Amplitude histogram:
plot_histogram(uwn,
title = "Amplitude Distribution",
xlabel = "Amplitude",
ylabel = "Count",
draw_median = false)PSD:
# Fourier transform
X = ftransform(uwn)
# f will contain vector of frequencies
f, _ = freqs(uwn, fs)
plot_psd(f, X.p, flim = (1, 40))Pink noise: Power decreases with increasing frequency (\(1/f\) characteristic). Common in biological signals.
fs = 100
t = 0:1/fs:10
n = length(t)
# white noise in frequency domain (complex)
wn = randn(ComplexF64, n) .* randn(n)
# normalised frequencies
freq = FFTW.fftfreq(n)
# scale amplitude by 1/√|f|, skip DC (index 1)
scaling = ones(n)
scaling[2:end] = 1.0 ./ sqrt.(abs.(freq[2:end]))
shaped = wn .* scaling
# back to time domain, keep real part, normalize
pn = real(ifft(shaped))
pn ./= std(pn)
NeuroAnalyzer.plot(t, pn, title = "Pink Noise")Amplitude histogram:
plot_histogram(pn,
title = "Amplitude Distribution",
xlabel = "Amplitude",
ylabel = "Count",
draw_median = false)PSD:
# Fourier transform
X = ftransform(pn)
# f will contain vector of frequencies
f, _ = freqs(pn, fs)
plot_psd(f, X.p, flim = (1, 40))Resolution: Number of distinct recordable values: 2^bits. A 16-bit ADC stores 65,536 levels.
Quantization: Converting an analogue signal to digital requires rounding each sample to the nearest level - introducing quantization noise.
Dynamic range: bits × 6.02 dB. A 16-bit system covers ~96 dB.
Minimum detectable resolution: Signal range ÷ number of steps. Example: 16 bits, 2^16 = 65536, maximum signal is 100 mV, resolution is 0.001 mV.
64 bit floating operations: -2.23459 × 10^(-7).
Sampling rate (temporal resolution) 500-2000 Hz covers most EEG analysis. 1000 Hz is convenient: 1 sample = 1 ms. Above 2000 Hz is used for simultaneous EEG + fMRI.
Temporal precision How much information is encoded across the current, previous, and next time points.
Frequency resolution: Determined by the total number of time points - more samples yields finer frequency bins.
Frequency precision: Information density per frequency bin relative to the whole signal.
General formula of the sine wave is:
\[ y = a \times \sin(2\pi \times f \times t + \theta) \]
where:
\(a\) is the amplitude,
\(f\) is the frequency (1/s) (Hz),
\(t\) is the time (s),
\(\theta\) is the phase angle (rad), \(\theta = 0\): sine; \(\theta = \pi/2\): cosine.
Frequency: time to get from one peak to the next peak.
Sine and cosine are orthogonal - their inner product over a full period is zero. Both arise naturally from complex exponentials via Euler’s formula:
\[ \sin(t) = (e^{1i \times t} - e^{-1i \times t}) / 2i = \Im(e^{1i \times t}) \]
\[ \cos(t) = (e^{1i \times t} + e^{-1i \times t}) / 2 = \Re(e^{1i \times t}) \]
fs = 256
# 5 seconds
t = collect(0:(1 / fs):5)
n = length(t)
# amplitude
a = 10
# frequency (Hz)
f = 3
# phase (rad)
p = pi / 2
s = generate_sine(f, t, a, rad2deg(p))
# or
# s = @. a * sin(2 * pi * f * t + p)
NeuroAnalyzer.plot(t, s)fs = 256
t = collect(0:(1 / fs):2pi)
# amplitudes
a = [10, 5, 2]
# frequencies (Hz)
f = [2, 5, 10]
# phase (rad)
p = [0, pi, pi / 2]
s = zeros(length(t))
for idx in 1:length(a)
s = s .+ generate_sine(f[idx], t, a[idx], p[idx])
# or
# s = @. s + a[idx] * sin(2 * pi * f[idx] * t + p[idx])
end
NeuroAnalyzer.plot(t, s)Euler’s formula links trigonometry and complex exponentials:
\[ e^{1ik} = \cos(k) + 1i × \sin(k) \]
Substituting the standard oscillation formula gives the complex sine wave:
\[ y = \exp(1i \times 2 \times \pi \times f \times t) = \underbrace{\cos(2 \times \pi \times f \times t)}_{\text{real part}} + 1i \underbrace{\times \sin(2 \times \pi \times f \times t)}_{\text{imaginary part}} \]
The real and imaginary parts are a cosine and a sine, offset by \(\pi / 2\). Phase is conventionally set to zero when working with complex sinusoids.
t = collect(0:0.01:2pi)
a = 1
f = 2
s = generate_csine(f, t, a);
# or
# s = @. a * exp(1im * 2 * pi * f * t)NeuroAnalyzer.plot(t, real(s), title = "real(s)")NeuroAnalyzer.plot(t, imag(s), title = "imag(s)")lines(t, real.(s), imag.(s), axis=(type=Axis3,))The sinc function is the Fourier transform of a rectangular window and forms the basis of ideal signal reconstruction from discrete samples.
\[ sinc(x) = \sin(x) / x \]
In the time domain, centered at peak time \(m\):
\[ sinc(x) = \sin(2 \times \pi \times f \times (t - m)) / (t - m) \]
t = -1:0.001:1
s = generate_sinc(t, f = 20)
NeuroAnalyzer.plot(t, s)xs = LinRange(-10, 10, 100)
ys = LinRange(-10, 10, 100)
zs = [sin(complex(x, y)) / complex(x, y) for x in xs, y in ys]
surface(xs, ys, real.(zs) .- abs.(zs), axis=(type=Axis3,))The Gaussian function is a symmetric, bell-shaped curve that is widely used in various fields, including time-frequency analysis in EEG (Electroencephalography).
Mathematical Formulation
The general form of the Gaussian function is given by:
\[ f(x) = a \times e^{-\frac{(x - b)^2}{2c^2}} \]
where:
\(a\) controls the peak height of the Gaussian function,
\(b\) determines the position of the peak along the x-axis,
\(c\) is related to the standard deviation and controls the width of the Gaussian curve. It is sometimes referred to as the Gaussian RMS (Root Mean Square) width.
Applications in EEG Analysis
Time-Frequency Analysis:
Smoothing and Filtering:
Modeling:
Probability Density Function:
Example in EEG Analysis
Understanding and utilizing the Gaussian function can significantly enhance the analysis of EEG signals, providing insights into brain dynamics and aiding in the diagnosis and study of neurological conditions.
With the width parameter \(w = 2s^2\) (and \(m = 0\) for EEG analysis):
\[ y = a \times e^{-\frac{t^2}{w}} \]
Full-width at half-maximum (\(h\)) is often more intuitive than standard deviation:
\[ y = a \times e^{-4 \ln(2) \times \frac{(t - c)^2}{h^2}} \]
\[ y = e^{-\frac{(f - c)^2}{2s^2}} \]
\[ s = \frac{h \times (2 \pi - 1)}{4 \pi} \]
where:
\(c\) is the center frequency,
\(h\) is FWHM.
Generate a standard 100 Hz time vector:
fs = 100
t = 0:(1 / fs):10A pulse wave with 50% duty cycle. In general, duty cycle = pulse width ÷ period.
# offset: 0.02
# peak: 0.25
# width: 1.5
s = @. mod(0.25 + t, 2) > 1.5
NeuroAnalyzer.plot(t, s)s = @. abs(mod(t, 2) - 1)
NeuroAnalyzer.plot(t, s)A non-stationary signal whose instantaneous frequency changes linearly over time:
n = length(t)
f = [2, 10]
ff = linspace(f[1], f[2] * mean(f) / f[2], n)
s = @. sin(2 * pi * ff * t)
NeuroAnalyzer.plot(t, s)n = length(t)
a = linspace(1, 10, n)
f = 3
s = @. a * sin(2 * pi * f * t)
NeuroAnalyzer.plot(t, s)a = [2, 5, 10]
f = [5, 10, 20]
tchunks = round.(Int64, range(1, length(t), length = length(a)+1))
s = []
for idx in 1:length(a)
s = cat(s,
a[idx] .*
sin.(2 .* pi .* f[idx] .* t[tchunks[idx]:tchunks[idx + 1]]),
dims = 1)
end
NeuroAnalyzer.plot(t, s[1:length(t)])Instantaneous power is defined as the square of the signal’s amplitude:
\[ P = s^2(t) \]
The total energy of the signal over a time interval is the integral of the instantaneous power - equivalently, the area under the squared magnitude:
\[ E = \int s^2(t)\ \mathrm{d}t = \vert s(t) \vert^2 \]
Decibels express ratios logarithmically. A factor-of-2 increase in power equals +3 dB.
\[ \text{Power (dB)} = 10 \times \log_{10}(P_1 / P_2) \]
\[ \text{Amplitude (dB)} = 20 \times \log_{10}(A_1 / A_2) \]
The factor of 20 (instead of 10) arises because power is proportional to amplitude squared: \(\log_{10}(A^2) = 2 \times \log_{10}(A)\).
Convert power to dB:
function pow2db(x)
x[x .< 0] .= NaN
return 10 .* log10.(x)
end
# or DSP.pow2db()Convert dB to power:
db2pow(x) = 10 .^ (x ./ 10)
# or DSP.db2pow()