Initialize NeuroAnalyzer
using NeuroAnalyzer
using DSP
using Plots
using Statistics
eeg = load("files/eeg.hdf")
e10 = epoch(eeg; ep_len = 10)using NeuroAnalyzer
using DSP
using Plots
using Statistics
eeg = load("files/eeg.hdf")
e10 = epoch(eeg; ep_len = 10)Coherence measures the consistency of the phase relationship between two signals across trials or time windows - quantifying how reliably two channels oscillate together at a given frequency.
Where cross-correlation operates in the time domain, coherence operates in the frequency domain: it asks not just whether two signals are related, but at which frequencies their relationship is stable.
Coherence is computed from the Cross-Power Spectral Density (CPSD) - a frequency-domain measure of the relationship between two signals, describing how the power of one signal relates to another at each frequency.
In practice, for discrete signals \(x[n]\) and \(y[n]\), the CPSD is estimated by multiplying the Fast Fourier Transform (FFT) of one signal by the complex conjugate of the FFT of the second signal:
\[ P_{xy}(f) = X(f) \times Y^*(f) \]
where:
\(X(f)\) is the Fourier transform of \(x\),
\(Y^*(f)\) is the complex conjugate of the Fourier transform of \(y\).
The result \(P_{xy}\) is complex-valued at each frequency, encoding two distinct pieces of information:
Coherence is then obtained by normalizing the CPSD by the auto-spectral densities of both signals:
\[ C_{xy}(f) = \frac{\vert P_{xy}(f) \vert^2}{P_{xx}(f) \times P_{yy}(f)} \]
which scales the result to the range \([0, 1]\), removing the influence of each signal’s individual power and isolating the consistency of their phase relationship.
Computing CPSD:
cp, f = cpsd(e10,
e10,
ch1 = "Fp1",
ch2 = "Fp2",
ep1 = 10,
ep2 = 10,
method = :mt)Three estimation methods are available via the method parameter:
:fft - Fast Fourier transform; fast and straightforward, suitable for stationary signals:mt - multi-taper; reduces spectral leakage by averaging across multiple orthogonal tapers, producing a more stable estimate:stft - short-time Fourier transform; computes CPSD in sliding time windows, suitable for non-stationary signals where the frequency content changes over timePlotting magnitudes:
plot_psd(f,
abs.(cp[1, :, 1]),
xlabel = "Frequency [Hz]",
ylabel = "Magnitude")Magnitude peaks in the CPSD indicate strong connectivity between the two signals at those specific frequencies - the larger the magnitude, the more consistently the two signals co-vary at that frequency across the observation window.
Plotting phases:
plot_phase(DSP.angle.(cp[1, :, 1]),
f)Phase information reveals the temporal relationship between the two signals at each frequency - a consistent non-zero phase indicates that one region systematically leads or lags the other, which can imply a directional influence between them.
CPSD can also be compared across conditions at the same electrode location - for example, rest vs. task, or healthy vs. patient groups - to identify frequency-specific changes in inter-regional connectivity associated with cognitive state or pathology.
Definition
Coherence is defined as the normalized cross-spectral density between two signals \(x\) and \(y\):
\[ C_{xy}(f) = \frac{\vert S_{xy}(f)\vert ^2}{S_{xx}(f) \times S_{yy}(f)} \]
where:
\(S_{xy}(f)\) is the cross-spectral density of \(x\) and \(y\),
\(S_{xx}(f)\) and \(S_{yy}(f)\) are the auto-spectral densities (power spectra) of \(x\) and \(y\) respectively.
Key properties:
Magnitude-squared coherence (MSC) - the squared absolute value of coherence, as defined above, emphasizing strong relationships
Both coherence and magnitude-squared coherence (MSC) measure the linear relationship between two signals in the frequency domain - differing primarily in whether phase information is retained.
Coherence is complex-valued, encoding both the magnitude and the phase of the relationship between two signals at each frequency:
\[ C_{xy}(f) = \frac{P_{xy}(f)}{\sqrt{P_{xx}(f) \times P_{yy}(f)}} \]
where:
\(P_{xy}\) is the cross-power spectral density,
\(P_{xx}\), \(P_{yy}\) are the auto-power spectral densities of \(x\) and \(y\).
Magnitude-squared coherence (MSC) discards the phase and retains only the strength of the relationship. It is always a real number in \([0, 1]\):
\[ MSC_{xy}(f) = \frac{\vert P_{xy}(f) \vert^2}{P_{xx}(f) \times P_{yy}(f)} = \vert C_{xy}(f)\vert^2 \]
Interpretation is the same as coherence magnitude: 0 indicates no linear relationship at that frequency, 1 indicates a perfect linear relationship.
Imaginary Coherence (IC) uses only the imaginary component of the cross-spectrum, measures coherence while ignoring zero-lag synchronization (which may be due to volume conduction), making it insensitive to volume-conducted signals that appear with zero phase lag.
Formula:
\[ \text{IC} = \vert \Im(C_{xy}(f)) \vert \]
Interpretation:
Advantages:
Which to use:
Computing coherence, imaginary part of coherence and magnitude-squared coherence using multi-taper method:
coh_data = NeuroAnalyzer.coherence(
e10,
e10,
ch1 = "Fp1",
ch2 = "Fp2",
method = :mt
)Three estimation methods are available via the method parameter:
:fft - Fast Fourier transform; fast and straightforward, suitable for stationary signals:mt - multi-taper; reduces spectral leakage by averaging across multiple orthogonal tapers, producing a more stable estimate:stft - short-time Fourier transform; computes CPSD in sliding time windows, suitable for non-stationary signals where the frequency content changes over timeGetting phases:
coh_ph = DSP.angle.(coh_data.coh)Getting magnitudes:
coh = abs.(coh_data.coh)Plotting coherence between O1 and O2 in the alpha band:
coh_data = NeuroAnalyzer.coherence(
e10,
e10,
ch1 = "O1",
ch2 = "O2",
ep1 = 1,
ep2 = 1,
flim = band_frq(eeg, band = :alpha)
)
# magnitude
coh_mag = abs.(coh_data.coh)plot_psd(coh_data.f,
coh_mag[1, :, 1],
ylabel = "",
title = "Coherence (magnitude)")plot_psd(coh_data.f,
coh_data.msc[1, :, 1],
ylabel = "",
title = "MSC")plot_psd(coh_data.f,
coh_data.imcoh[1, :, 1],
ylabel = "",
title = "Coherence (imaginary part)")Also, a dedicated plot_coherence() function is available:
coh_data = NeuroAnalyzer.coherence(
eeg,
eeg,
ch1 = get_channel(eeg, type = "eeg")[1:4],
ch2 = get_channel(eeg, type = "eeg")[5:8],
ep1 = 1,
ep2 = 1,
flim = band_frq(eeg, band = :alpha)
)
# get magnitude and simplify values
coh, _ = areduce(abs.(coh_data.coh), coh_data.f, n = 0.1)
mscoh, f = areduce(coh_data.msc, coh_data.f, n = 0.1)
# generate label pairs
l = paired_labels(get_channel(eeg, type = "eeg")[1:4],
get_channel(eeg, type = "eeg")[5:8])
# plot coherence values along frequencies
plot_coherence(coh[1, :, 1],
f,
title = "Coherence $(l[1])")For multi-channel data:
plot_coherence(mscoh[:, :, 1],
f,
clabels = l,
title = "Magnitude-squared coherence")plot_coherence(mscoh[:, :, 1],
f,
clabels = l,
avg = true,
title = "Averaged magnitude-squared coherence")plot_coherence(mscoh[:, :, 1],
f,
clabels = l,
ci95 = true,
title = "Magnitude-squared coherence")Plotting averaged coherence values as connections:
c = zeros(19, 19)
coh_avg = round.(mean(coh[:, :, 1], dims = 2), digits = 2)
c[1, 5] = coh_avg[1]
c[2, 6] = coh_avg[2]
c[3, 7] = coh_avg[3]
c[4, 8] = coh_avg[4]
plot_locs(eeg,
ch = "eeg",
connections = c,
gui = false)Tip: Plotting connections requires channel locations.
Tip: Line thickness represents connectivity value. If mono=true, positive values are represented with gray lines, negative - with dotted gray lines. If mono=false, positive values are represented with red lines, negative - with blue lines.
To present values numerically, set the option weights to false.
plot_locs(eeg,
ch = "eeg",
ch_labels = false,
connections = c,
weights = false,
gui = false)Tip: If mono=false, positive values are in red and negative values are in blue.