EEGToolkit.jl
Computational EEG analysis with emphasis in sleep neuroscience.
Developed at the Laboratory for the Study of Sleep Slow-wave activity
The Gods of the earth and sea
Sought thro' Nature to find this Tree,
But their search was all in vain:
There grows one in the Human Brain.— William Blake
This package has three aims:
- Simplicity
- Transparency
- Efficiency
Simplicity means that a person with only basic programming skills should be able to use it. Transparency means that any methodology implemented by the package should be accessible enough so as to be reported in a scientific paper. Efficiency means that large EEGs (e.g. sleep EEGs) should be processed and analyzed in seconds.
This package is free software—free as in freedom. You are free to use the code as you wish and for any purpose. You are free to study the code and change it to make it do what you wish. You are free to redistribute copies of this package to help others. You are free to distribute copies of any modified version of this package.
Proprietary software hinders the liberty of its users. In science, it obscures the scientific process, difficulting replication and collaboration. If you are a scientist, use free software whenever possible.
Package Features
- Loading and processing EEG data
- EEG visualization
- Sleep stage handling
- NREM Period detection
- Power spectral analysis
- Spindle detection algorithms
- Automated artifact detection
- Resampling
- Hypnograms
Time series
EEGToolkit.TimeSeries
— TypeA struct representing time series data.
Fields
x::Vector{<:AbstractFloat}
: Time series data.fs::Integer
: Sampling rate.
EEGToolkit.segment
— Functionsegment(v::Vector{T}, L::Int; overlap::Union{<:AbstractFloat,Integer}=0, symmetric=false) where {T}
Splits a vector v
into segments of length L
with an overlap overlap
expressed as a fraction of L. The overlap
defaults to 0
(no overlap). Returns a vector $v$ of vectors - i.e. Vector{Vector{T}}
- with $\vec{v_i}$ the $i$th segment in the split.
The function always attempts to capture the whole vector, even if the final split is not of length L. For example,
> x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
> segment(x, 5)
2-element Vector{Vector{Int64}}:
[1, 2, 3, 4, 5]
[6, 7, 8, 9, 0]
> segment(x, 7)
2-element Vector{Vector{Int64}}:
[1, 2, 3, 4, 5, 6, 7]
[8, 9, 0]
Set symmetric=true
to ensure that, if this occurs, the last split is dropped.
> segment(x, 3; symmetric=true)
3-element Vector{Vector{Int64}}:
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
If L
is equal to the segment length, segment
raises a warning and returns a vector with only the original vector: [v]
. The return value ensures type-safety but the warning is raised because splitting a vector over its length is potentially a programming mistake.
segment(ts::TimeSeries, L::Int; kargs...)
Wrapper to segment the vector ts.x
in the time series ts
.
EEGToolkit.epoch
— Functionepoch(signal::TimeSeries, n::Integer; epoch_length::Integer=30)
Returns a vector [x₁, …, xₖ]
with all values xᵢ
corresponding to the n
th epoch in the signal.
epoch(signal::TimeSeries, n::Integer, m::Integer)
Returns a vector [x₁, …, xₖ]
with all indexes corresponding to epochs n, n+1, …, m
of the EEG. The default sampling rate is used to compute the indexes.
EEGToolkit.plot_ts
— Functionplot_ts(ts::TimeSeries, s::Integer, e::Integer; norm=false, ylab="Amplitude (uV)")
Plots TimeSeries
from epoch s
to epoch e
. The series many be normalized.
plot_ts(ts::TimeSeries, s::Integer; kargs...)
Plots TimeSeries
at epoch s
.
plot_ts(ts::TimeSeries; norm=false, ylab="Amplitude (uV)")
Plots TimeSeries
. The series may be normalized.
EEGToolkit.seconds_to_time
— Functionseconds_to_time(seconds::Union{AbstractFloat, Integer})
Helper function: maps a time in seconds to a Time object.
EEG
EEGToolkit.EEG
— TypeA struct for the EEG data type. An EEG is simply conceived as a collection of labeled time series.
Fields
signals::Dict{String, TimeSeries}
: A dictionary mapping signal labels (strings) to arrays of floating-point values.
Constructors
EEG(file::String; id::String="")
: Instantiates an EEG
from an EDF file (file
).
Example
eeg_data = EEG("path/to/edf_data/data.edf")
EEGToolkit.remove_channel!
— Functionremove_channel!(eeg::EEG, channel::String)
Removes a channel from the EEG.
remove_channel!(eeg::EEG, channels::Vector{String})
Removes a list of channels from the EEG.
EEGToolkit.plot_eeg
— Functionplot_eeg(eeg::EEG, s::Integer, e::Integer; channels::Vector{String}=[""], spacing::AbstractFloat=1.5)
Plots EEG channels from epoch s
to epoch e
. Specific channels may be selected with the channels
karg. The spacing
argument is an added factor in the normalization of the EEG signals - the vertical distance between each signal in the plot grows proportionally to spacing
.
EEGToolkit.artifact_reject
— Functionfunction artifactreject(signal::TimeSeries, anomdict::Dict{Int, Vector{Int}}; epochlength::Integer=30, subepochlength::Integer=5)::Vector{Vector{<:AbstractFloat}};
This function removes from a signal the sub-epochs which contain artifacts. It requires a TimeSeries
and a Dict{Int, Vector{Int}}
, hereby termed anom_dict
(for anomaly dictionary).
anom_dict
is understood to be such that anom_dict[i] = [n₁, …, nₖ]
means the i
th epoch has artifacts at sub-epochs n₁, ..., nₖ
.
The return value is a segmented signal (Vector{Vector<:AbstractFloat}}
), each of whose segments corresponds to an epoch with its artifcat-contaminated sub-epochs removed. In other words, if the result
holds the return value of this function, result[i]
contains what is left from the i
th epoch after removing its contaminated sub-epochs. It is possible that result[i]
is empty, if all sub-epochs of epoch i
contained artifacts.
artifact_reject(signal::TimeSeries, anoms::Vector{Integer}; epoch_length::Integer=30)
An anomaly vector $\vec{x} \in \mathbb{N}^{n}$ is a sorted vector whose values are those epochs in an TimeSeries
that contain anomalies or artifacts. This function segments the TimeSeries and filters out all epochs containing artifacts.
NREM Period detection
NREM period definition
Following Feinberg & Floyed and Dijk, a NREM period is a sequence of epochs satisfying the following conditions:
- It starts with stages 2, 3 or 4.
- It contains at least 15 minutes of stages 2, 3 or 4 in total.
- It ends with 5 or more minutes of REM, or with 5 or more minutes of wakefulness.
Epochs in the sequence are allowed to contain occurrences of REM sleep or wakefulness in between, as long as the duration of this occurrences is less than 5 minutes. But the epochs corresponding to these occurrences will not be part of the NREM period. For example, in a stage sequence of the form
... - 10m of stage two - 1m of REM - 5m of stage three - 5m of REM - ...
the NREM period consists of the first 10 minutes of stage 2 and the 5 minutes of stage 3, ignoring the 1 minute of REM in-between them.
Importantly, the restriction that ending REM periods must last at least 5 minutes is not imposed when detecting the first and the last NREM period in a night of sleep.
NREM detection algorithm
Let $n$ be the number of epochs corresponding to $15$ minutes and $m$ the number of epochs corresponding to $5$ minutes. (In 30 second epochs, $n = 30, m = 10$).
The algorithm assumes that the staging
field of an EEG
has been set to a vector $\vec{s}$ that contains only the strings $1, \ldots, 6, ?$ (with $5$ marking REM, $6$ wakefulness, $?$ unknown/unstaged).
The algorithm works by mapping $\vec{s}$ to $\alpha = s_1 \ldots s_q$ a word over the language generated by $\Sigma = \{1, \ldots, 6, ?\}$.
Observe that the language $[(5+6)^*(2+3+4)^*]^*$ is partitioned into $U$ and $U’$, where $U$ is the set of words containing at least $n$ symbols $2, 3, 4$ where neither $5$ nor $6$ occur consecutively $m$ times. Then $\alpha$ can be decomposed into
\[\alpha = \psi_1 \phi_1 \psi_2 \phi_2 \ldots \psi_k \phi_k \psi_{k+1}\]
where $\phi_i = \varphi_i (5^m5^* + 6^m6^*)$ and $\varphi_i \in U$. Such a decomposition readily provides the number of NREM periods in the EEG (i.e. $k$). Furthermore, the epochs which comprise these periods are easily inferable from the decomposition.
EEGToolkit.nrem
— Functionfunction nrem(staging::Staging, n::Integer=30, m::Integer=10)
Finds the k
underlying NREM periods in a staging vector. Returns a vector of vectors V
s.t. the i
th vector in V
contains the epochs which comprise the i
th NREM period. Thus, the length of V
is k
the number of NREM periods.
The n
parameter is the number of (not necessarily consecutive) epochs which comprise a NREM period. The m
parameter is the number of REM or wakefulness epochs required to mark the end of a NREM period (if n
NREM epochs were parsed before) or to disregard the current sequence and begin parsing from the next NREM epoch.
The staging
parameter is a vector containing only the symbols 1, …, 6, ?
where 5
denotes REM, 6
denotes wakefulness, and ?
denotes unscored/unstaged.
Spindle detection
This package implements two spindle detection algorithms discussed in O'Reilly and Nielsen (2015). We give a brief overview of them here but refer to their original publications for further detail.
EEGToolkit.sigma_index
— Functionsigma_index(x::Vector{<:AbstractFloat}, fs::Integer)
The $\sigma$-index algorithm (Huupponen et al., 2007) find abnormally high amplitude values in the spindle frequency band. Per each 1 second window of the EEG, it computes
- the maximum amplitude in the spindle frequency band, which we call $S_{max}$
- the average amplitude in the low alpha and theta frequencies, which we call
$\alpha_{mean}, \theta_{mean}$
- the maximum alpha amplitude $\alpha_{max}$
The $\sigma$-index of each window is defined to be zero if $\alpha_max > S_{max}$, and otherwise
\[f(S_{max}, \alpha_{mean}, \phi_{mean}) = \frac{2S_{max}}{ \alpha_{mean} + \theta_{mean} } \]
Higher values are indicative of a higher spindle probability. The rejection threshold recommended in the original paper is $\lambda = 4.5$.
EEGToolkit.relative_spindle_power
— Functionrelative_spindle_power(x::Vector{<:AbstractFloat}, fs::Integer)
The Relative Spindle Power (RSP) algorithm (Devuyst et al., 2011) also detects abnormal values along the spindle frequency band. For every 1 second window, the amplitude spectrum $S(t)$ is computed, and the RSP is defined as
\[RSP(t) = \frac{\int_{11}^{16} S(t, f) df}{\int_{0.5}^{40} S(t, f) df}\]
This definition is more intelligible than the that of the $\sigma$-index, insofar as it represents the ratio of the total power in the spindle band with respect to the total power in the $[0.5, 40]$ frequency range. It is evident by definition that $0 \leq RSP \leq 1$. Higher values are indicative of a higher spindle probability (it should be clear that $RSP$ is not a probability itself). The rejection threshold recommended in the original paper is $\lambda = 0.22$.
Power spectral analysis
EEGToolkit.AmplitudeSpectrum
— TypeStructure for amplitude spectrum estimations. Estimations are by default one sided, with frequencies ranging from [0, fₛ/2]. The formula used is
\[\frac{2|H(f)|}{\sum_i w_i}\]
with $w_i$ a Hanning window.
Fields
freq::Vector{<:AbstractFloat}
: Frequency range of the spectrumspectrum::Vector{<:AbstractFloat}
: Estimated spectral amplitude
Constructors
AmplitudeSpectrum(x::Vector{<:AbstractFloat}, sampling_rate::Integer, pad::Integer)
: Computes a direct PSD over a signal x
with a given sampling_rate
.
EEGToolkit.PSD
— TypeStructure for PSD estimations. Estimations are by default one sided, with frequencies ranging from [0, fₛ/2].
The default formula is
\[\frac{2|H(f)|^2}{\zeta \sum_i w_i^2}\]
with $w_1, \ldots, w_n$ a Hanning window and $\zeta$ a normalization factor which defaults to $1$.
Barlett or Welch's mehtod can be used, where the formula becomes
\[\frac{1}{M K \varphi} \sum_i^M \left[ \frac{2|H_i(f)|^2}{ \sum_i w_i^2} \right]\]
where $w_1, \ldots, w_n$ a Hanning window, $M$ the number of segments, $K$ the number of samples per segment, $H_i(f)$ the FFT of the $i$th segment of the signal, and $\varphi$ a normalization factor defaulting to 1
.
Fields
freq::Vector{<:AbstractFloat}
: Frequency range of the spectrumspectrum::Vector{<:AbstractFloat}
: Estimated spectral density in dB.
Constructors
PSD(x::Vector{<:AbstractFloat}, fs::Integer; window_function::Function = hanning, pad::Integer=0, normalization::Real=1)
: Computes PSD estimation of a signalx
with sampling ratefs
. Awindow_function
is applied to the signal, defaulting to a Hanning window. The signal may be padded to an optional lengthpad
(defaults to zero, i.e. no padding). AReal
normalization
is added to the denominator, which defaults to1
.PSD(segs::Vector{Vector{T}}, fs::Integer; window_function=hanning, normalization::Real=1) where {T<:AbstractFloat}
: Computes the average spectrum of the segment vectorssegs
. The estimation is normalized with anormalization
that defaults to $1$. Thewindow_function
is applied to all segments prior to their PSD estimation.
PSD(x::Vector{<:AbstractFloat}, fs::Integer, seg_length::Integer; overlap::Union{<:AbstractFloat,Integer} = 0.5, window_function::Function=hanning, normalization::Real=1)
: Segments the signal x
into segments of length seg_length
, with an overlap of overlap
which defaults to 0.5 (half the segment length). After signal segmentation, the average spectrum of the resulting segments is returned. A window_function
is applied to all segments prior to their PSD estimation, defaulting to a Hanning window. The final estimation is normalized with a normalization
factor that defaults to 1
.
PSD(ts::TimeSeries; kargs...)
: Wrapper to apply the first or second constructor to a TimeSeries signal.PSD(ts::TimeSeries, seg_length::Integer; kargs...)
: Wrapper to apply the third constructor to a TimeSeries signal.PSD(freq::Vector{<:AbstractFloat}, spectrum::Vector{<:AbstractFloat})
: Direct constructor.
EEGToolkit.plot_psd
— Functionplot_psd(psd::PSD; freq_lim=30.0)
Plot a PSD with x-axis being frequency and y-axis being estimated power spectrum.
EEGToolkit.Spectrogram
— TypeA spectrogram is a matrix $S^{M \times F}$ where $M$ is the number of windows in the windowing of a signal and $F$ is the length of the spectrum vector in any given window (i.e. the frequency resolution). It is useful to observe spectral changes in time or to compute the spectrum of time-regions of interest (e.g. only NREM periods in a sleep EEG). The information available in direct PSD can be inferred from the spectrogram with ease.
For instance, let $f_1, f_2, \ldots, f_k$ be a strictly increasing sequence of frequencies. Assume these frequencies correspond to the column indexes $c_1, c_2, \ldots, c_k$ of $S$. Then the mean power in the frequency range $[f_1, f_k]$ is
\[\frac{1}{M} \sum_{i=1}^{M}\left[\frac{1}{c_k - c_1}\sum_{j=c_1}^{c_k} S_{ij}\right] = \frac{1}{M\big(c_k - c_1\big)}\sum_{i=1}^{M}\sum_{j=c_1}^{c_k} S_{ij}\]
In this package, mean power in a frequency range is computed with the mean_band_power
function.
Fields
time::Vector
: Time domainfreq::Vector{<:AbstractFloat}
: Frequency domainspectrums::Matrix{<:AbstractFloat}
: Power spectrum. Rows are time and columns are frequency; the value inspectrums[row, freq]
is the power at time windowrow
for frequencyfreq
.segment_length::Integer
: Length of each segment in time.aggregated_spectra::Vector{<:AbstractFloat}
: Spectral average (mean of rows)
Constructors
Spectrogram(segs::Vector{Vector{T}}, psd_function::Function; dB = false) where {T<:AbstractFloat}
: Given a sequence of windows $w_1, \ldots, w_k$ contained in thesegs
argument, computes the PSD within each window using a custompsd_function
.Spectrogram(signal::Vector{<:AbstractFloat}, window_length::Integer, psd_function::Function; overlap::Union{AbstractFloat, Integer}=0, dB=false)
: Splits a signal into (potentially overlapping) segments of lengthwindow_length
and computes theSpectrogram
over this windowing using the first constructor. A custompsd_function
is used within each window. Symmetry is enforced over the split signal, meaning that if the last segment is of length not equal to the rest, it is dropped. Thus, all windows are of equal length.function Spectrogram(ts::TimeSeries, window_length::Integer, psd_function::Function; kargs...)
: Wrapper constructor for aTimeSeries
object.
EEGToolkit.plot_spectrogram
— Functionplot_spectrogram(spec::Spectrogram; freq_lim::AbstractFloat=30.0, type::Int=1, color=:nipy_spectral)
Plots a spectogram spec
either in 2d (type = 1
) or 3d (type = 2
). An optional frequency limit (freq_lim
) may be set (defaults to 30Hz). The color palette color
may be set; defaults to nipy_spectral
.
EEGToolkit.freq_band
— Functionfreq_band(spec::Union{PSD}, lower::AbstractFloat, upper::AbstractFloat)
Given a PSD
, returns a Vector{<:AbstractFloat}
with the powers within the frequency band [lower, upper]
.
freq_band(spec::Spectrogram, lower::AbstractFloat, upper::AbstractFloat, window::Integer)
Given a Spectrogram
, returns a Vector{<:AbstractFloat}
with the powers within a frequency band [lower, upper]
of a specific window (row of the spectrogram).
freq_band(spec::Spectrogram, lower::AbstractFloat, upper::AbstractFloat)
Given a Spectrogram
, returns a Matrix{<:AbstractFloat}
with the powers within a frequency band [lower, upper] across all time windows.
EEGToolkit.mean_band_power
— Functionmean_band_power(spec::Spectrogram, lower::AbstractFloat, upper::AbstractFloat)
Given a Spectrogram
, returns the mean power in a given frequency band [lower, upper]
. This function effectively computes
\[\frac{1}{M\big(c_k - c_1\big)}\sum_{i=1}^{M}\sum_{j=c_1}^{c_k} S_{ij}\]
mean_band_power(spec::PSD, lower::AbstractFloat, upper::AbstractFloat)
Given a PSD
, returns the mean power in a given frequency band [lower, upper]
.
EEGToolkit.total_band_power
— Functiontotal_band_power(psd::PSD, lower::AbstractFloat, upper::AbstractFloat)
Given a PSD, computes the total power in the frequency band [lower, upper]
.
EEGToolkit.analyze_eeg
— Functionanalyze_eeg(signal::Vector{<:AbstractFloat}, fs::Integer)::Spectrogram
Perform a standardized analysis of an EEG signal. This analysis procedure succesfully replicated results from Washington State University in collaboration with the developer's laboratory at UPenn.
The standardized procedure is as follows: split the signal into 30-sec epochs, each of which is split into 5-sec sub-epochs. Each epoch's spectrum is the aggregated spectra from its sub-epochs; the signal's spectrum is the aggregated spectra from its epochs. Rectangular window is used. Overlap is set to zero. No normalization is performed.
analyze_eeg(signal::Vector{<:AbstractFloat}, fs::Integer, epoch_indexes::Vector{<:Integer})::Spectrogram
Perform a standardized analysis of the specified epochs of an EEG signal. This analysis procedure succesfully replicated results from Washington State University in collaboration with the developer's laboratory at UPenn.
The standardized procedure is as follows: split the signal into 30-sec epochs, each of which is split into 5-sec sub-epochs. Each epoch's spectrum is the aggregated spectra from its sub-epochs; the signal's spectrum is the aggregated spectra from its epochs.
Artifact detection
This package provides an interface of the CAPA statistical method (Fisch, Eckley & Fearnhead, 2021) via the RCall
package.
It is not a requirement to have
RCall
installed to use other features of this package, but it is a requirement for artifact detection. Calling artifact detection functions withoutRCall
installed will result in an error and a prompt to installRCall
.
CAPA is an automated anomaly detection algorithm which performs in linear time and is specifically designed for time series analysis. The adaptation provided in this package detects epidemic changes in the mean of each segment of the EEG, where the segment length is a parameter. The algorithm is relatively fast, considering the high complexity of EEG recordings.
For instance, on a 15.5 million samples EEG record with 8 channels (all artifact-detected), with a segment length of
30 * 5
seconds, the algorithm took ≈8.6 minutes on a computer with an I3 processor and 8GB of RAM. That's 8.6/8 = 1.075 minutes per channel, i.e. practically a minute per each 15.5 million-sized vector, on a mediocre computer.
EEGToolkit.detect_artifacts
— Functionfunction detect_artifacts(eeg::EEG, channel_name::String, seg_length::Int)::Nothing
Performs artifact detection in the eeg
channel named channel_name
. Stores resulting vector of Artifact
objects in the _artifacts
dictionary of the eeg
with key channel_name
.
Artifact detection is performed on each segment of the signal segmented with seg_length
. On each segment, the CAPA algorithm (Fisch et. al 2022) is used to detect epidemic distributional changes in the mean value of the segment. The penalty
is an integer value β such that β ln(n) (with n
the segment's length) is the penalty used by the CAPA algorithm to penalize the introduction of artifacts. The β penalty
defaults to 24
, which is much higher than the value recommended in Fisch et. al but matched human supervision on 78Hz sleep EEGs at the developer's laboratory.
EEGToolkit.plot_artifacts_in_epochs
— Functionfunction plot_artifacts_in_epochs(signal::TimeSeries, artifacts::ArtifactData, from::Integer, to::Integer; annotate::Bool=false)
Given a signal
and a non-empty vector of artifacts, plots the signal and highlights the existing artifacts from epoch from
to epoch to
. If annotate
is set to true, artifacts are annotated with their mean change and test statistic.
function plotartifactsinepochs(eeg::EEG, channelname::String,
Given an eeg
and an artifact-detected channel_name
, plots the existing artifacts in the channel from epoch from
to epoch to
. If annotate
is set to true, artifacts are annotated with their mean change and test statistic.
Resampling
EEGToolkit.resample
— Functionresample(signal::TimeSeries, new_fs::Float64)::TimeSeries
Resamples the input signal
to the given new_fs
, using rational factor resampling (L/M). Returns a new TimeSeries
with the resampled signal and updated sampling rate.
Helpers
EEGToolkit.next_power_of_two
— FunctionGiven an integer $n$, finds the least $m = 2^k$ s.t. $m \geq n$.
EEGToolkit.zero_pad
— Functionzero_pad(v::Vector{T}, desired_length::Integer) where {T<:AbstractFloat}
Zero-pads a numeric vector v
to a desired_length
Examples
Resampling
file = "myeeg.edf"
eeg = EEG(file)
signal = get_channel(eeg, "EEG6")
resampled = resample(signal, 10.0) # Resample signal to 10Hz.
p1 = plot_ts(signal, 30, 30) # Plot epoch 30
p2 = plot_ts(resampled, 30, 30) # Plot epoch 30
p = plot(p1, p2, layout=(2, 1), link=:x, legend=false)
Hypnogram
staging = CSV.read("staging.csv", DataFrame) # Some staging data
replace!(staging, "NS" => "?", "REM" => "5", "WK" => "6", "N1" => "1", "N2" => "2", "N3" => "3", "N4" => "4") # Convert it to appropriate format (see Staging data type)
staging = Staging(staging) # Convert to Staging data type
p = plot_hypnogram(staging) # Plot
Artifact detection
Consider the REPL commands below, which read an EEG from an EDF file, perform artifact detection on each 5min segment of the EEG channel EEG6
, and plots the artifacts from epochs 30 to 60 with annotations.
julia> file = "SWAIVF004.edf"
"SWAIVF004.edf"
julia> eeg = EEG(file)
julia> detect_artifacts(eeg, "EEG6", 60*5) # epoch length = 5 minutes
julia> get_artifacts(eeg) # Just to show what changes after running `detect artifacts` on channel EEG6.
Dict{String, Union{Nothing, Vector{Artifact}}} with 11 entries:
"Light" => nothing
"Sound" => nothing
"HR" => nothing
"PAT" => nothing
"Head Pos" => nothing
"EEG6" => Artifact[Artifact((1, 152), 33.1202, 5034.28, 1, 1), Artifact((173, 283), 9.70824, 1077.61, 1, 1), Artifact((23575, 23610), 32.582, 1172.95, 11, 1), Artifact((24197, …
"EEG5" => nothing
⋮ => ⋮
julia> plot_artifacts_in_epochs(eeg, "EEG6", 30, 60; annotate=true)
Spectrogram
file = "myeeg.edf"
eeg = EEG(file)
signal = get_channel(eeg, "EEG6")
psd = x -> PSD(x, signal.fs, signal.fs * 5)
S = Spectrogram(signal, x -> custom_psd(x, signal.fs))
plot_spectrogram(S; type::Int=2) # Type 2 for 3D plot.
NREM delta power
This is an example script for computing the mean $\delta$ (delta) power in each of the NREM periods of a sleep EEG. We will use the C3 channel.
# Assuming we have the stage data in a .csv
staging_df = CSV.read("my_staging_data.csv", DataFrame)
# Assuming the csv had a column named STAGES with the stage of each epoch.
staging = staging_df.STAGES
# We read an EEG that has channels C3-A2 and F3-A1. We assume the CSV had a
# column called STAGES with the stages of each epoch.
eeg = EEG(edf_file)
# We extract the TimeSeries object corresponding to C3-A2
signal = get_channel(eeg, "C3-A2")
# Detect the NREM periods with default parameters.
nrems = nrem(staging)
# Split the C3 signal into 30-second windows (not-overlapping).
epochs = segment(signal, signal.fs * 30)
# PSD function to be used within each window in the spectrograms
psd = x -> PSD(x, signal.fs, signal.fs * 5)
# Compute the spectrogram of the full signal
S = Spectrogram(signal, x -> custom_psd(x, signal.fs))
# Retrieve the indexes (rows) which correspond to delta power.
delta_band = findall(x -> x >= 0.3 && x <= 3.9, S.freq)
powers = []
for nrem_period in nrems
# Extract the portion of the signal corresponding to this NREM period
# This is a vector of vectors [vector_1, ..., vector_k], with the ith
# vector being the ith epoch in this NREM period.
nrem_epochs = epochs[nrem_period]
# From the spectrogram, retrieve the epochs corresponding to this NREM
# period and rows corresponding to delta power.
sub_spectrogram = S.spectrums[period, delta_band]
# Compute the mean power within that NREM period.
power = mean( sum(sub_spectrogram, dims=2) )
# Compute spectrogram with each window being an epoch of this nrem period.
spec = Spectrogram(nrem_epochs, .fs*30, psd)
# Add the computed mean delta power to the powers vector.
push!(powers, power)
end
# Now the ith element in `powers` is the mean delta power # of the ith NREM
period.