ERPs.jl

This module implements basic tools fo the analysis of Event-Related Potentials (ERPs). Many of these tools are useful for working with tagged EEG data in general, for example, with BCI data.

Extract ERPs

ERPs are EEG potentials time and phase-locked to the presentation of sensory stimuli.

They are extracted averaging EEG epochs (trials) of fixed duration starting at a fixed position in time with respect to the presentation of stimuli.

Eegle handles two ways to extract ERPs: using a stimulation vector or using marker vectors. You can swicth from one representation to the other using the stim2mark and mark2stim functions.

stimulation vector

This is an accessory channel holding as many samples as there are in the EEG recording. The value is zero everywhere, except at samples corresponding to a stimulus presentation, where the value is a natural number (1, 2, ...), each one coding a stimulus class. We name these numbers the tag of a sample. Each class label is associated to a unique non-zero tag, for example, left_hand → 1.

# Toy example of a stimulation vector for two classes
[0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0]

marker vectors

Equivalently, we can consider a vector holding $z$ vectors, each one listing the serial numbers of the samples belonging to each of the possible $z$ classes.

# Representation of the above stimulation vector as markers vector
[[13, 18], [6]]

Even if one class of stimuli only is present, the marker vectors will still be a vector of vectors (one in this case).

offset

Several Eegle methods allow setting an offset to determine the starting sample of all evoked potentials (or trials). The offset is always to be given in samples. It can be

  • zero (default): it does not affect the stimulations and corresponding markers,
  • negative: the stimulations and markers are shifted back,
  • positive: the stimulations and markers are shifted forth.
# Example with `offet=-3`; the above stimulation vector becomes
[0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
# and the corresponding marker vectors becomes
[[10, 15], [3]]
Data in NY format

When data in NY format is read, the offset is applied and reset to zero — see readNY.

overlapping

The ERPs are said overlapping if the minimum inter-stimulus interval is shorter than the ERP window length, that is, if a stimulus can be presented before the preceeding evoked response has ended. In this situation, the multivariate regression (MR) method can provide better ERP estimates as compared to the standard arithmetic average (AE) (Congedo et al., 2016).

Methods estimating ERPs in Eegle feature the overlapping boolean kwarg, by which you can switch between the AE (false) and MR method (true).

Overlapping

If the ERPs are overlapping and you set overlapping to true, all means should be estimated at once; In fact, the advantage of the MR method vanishes if the means are computed individually.

In general, do not set overlapping to true for computing only one mean, even if the stumulations are overlapping. In this case the AE and MR methods are equivalent, but the AE is faster and more accurate.

For the same reason, do not set overlapping to true if the ERPs do not actually overlap.

Resources for ERPs

For classifying ERPs using Riemannian geometry — see package PosDefManifoldML.jl.

For spatial filters increasing the signal-to-noise ratio of ERPs — see the CSP and CSTP functions in package Diagonalizations.jl and article (Congedo et al., 2016).

For the analysis of time-locked and phase-locked components of ERPs, as well as ERP synchronization measures, in the time-frequency domain — see package FourierAnalysis.jl and companion article (Congedo, 2018).

Unfold is a package dedicated to ERP analysis.

Methods

FunctionDescription
Eegle.ERPs.meancompute ERPs from an EEG recording (2 methods)
Eegle.ERPs.stim2markconvert a stimulation vector into marker vectors
Eegle.ERPs.mark2stimconvert marker vectors into a stimulation vector
Eegle.ERPs.mergemerge and reorganize marker vectors
Eegle.ERPs.trialsWeightscompute adaptive weights for average ERP estimations
Eegle.ERPs.trialsextract trials (e.g., ERPs) from a tagged EEG recording
Eegle.ERPs.rejectreject trials (e.g., ERPs) in a tagged EEG recording

📖

Statistics.meanFunction
(1) function mean(  X::Matrix{T}, 
                    wl::S, 
                    mark::Vector{Vector{S}};
        overlapping :: Bool = false,
        offset :: S = 0,
        weights :: Union{Vector{Vector{R}}, Symbol}=:none) 
    where {T<:Real, S<:Int}

(2) function mean(  o::EEG; 
        overlapping :: Bool = false,
        offset :: S = 0,
        weights :: Union{Vector{Vector{T}}, Symbol} = :none,
        mark :: Union{Vector{Vector{S}}, Nothing} = nothing) 
    where {T<:Real, S<:Int}

Estimate the weighted mean ERPs (event-related potentials), as the standard arithmetic mean (default) or using the multivariate regression method, as detailed in (Congedo et al., 2016).

Tutorials

xxx

METHOD (1)

Arguments

  • X: the whole EEG recording, a matrix of size $T×N$, where $T$ and $N$ denotes the number of samples and channels (sensors), respectively
  • wl: the window (trial or ERP) length in samples
  • mark: the marker vectors.
Empty markers vectors

If mark holds empty vectors, they will be ignored and the mean will not be computed for those vectors. The number of means therefore will be equal to the number of non-empty mark vectors.

Optional Keyword Arguments

  • overlapping: see overlapping
  • offset: see offset
  • weights: can be used to obtain weighted means. By default, equal weights are used. It can be:
    • a vector of vectors of non-negative real weights for the trials, with the same shape as mark, where the empty vectors of mark are ignored
    • :a : adaptive weights computed as the inverse of the squared Frobenius norm of the trials data, along the lines of (Congedo et al., 2016).

Return

A vector of mean ERPs, one for each non-empty vectors in mark. Each mean is a matrix of size $wl×n$, where $n$ is the number of electrodes.

METHOD (2)

The same as method (1), but taking as input an Eegle.InOut.EEG structure o, which has fields providing the recording (X), the ERP duration in samples (wl) and the markers (mark).

Different markers can be used instead by passing marker vectors with the mark kwarg.

offset

If mark has been created using an offset when reading the data using Eegle.InOut.readNY, do not provide the offset here (it is zero by default), as mark has already applied the offest.

Markers which value plus the offset exceeds $t$ minus the window length will be ignored, as they cannot define a complete ERP (or trial).

See Eegle.ERPs.stim2mark, Eegle.ERPs.trialsWeights

Examples

using Eegle # or using Eegle.ERPs

# Method (1)

# Number of channels, sampling rate and window length
N, sr, wl = 19, 128, 128

# number of tags per class
nm = [30, 45, 28]

X = randn(sr*100, N)
mark = [[rand(1:sr*100-wl) for i=1:m] for m∈nm]

# compute the arithmetic means for all classes with adaptive weights
𝐌 = mean(X, wl, mark; weights=:a)

# compute the same means for class 1 and 3 only
𝐌 = mean(X, wl, [mark[1], mark[3]]; weights=:a

# compute the same mean only for the first class
# and return it as a matrix (not as a vector of matrices)
M = mean(X, wl, [mark[1]]; weights=:a)[1]

# compute the multivariate regression means with adaptive weighting
𝐌 = mean(X, wl, mark; overlapping=true, weights=:a)

# Method (2)

# read the example file for the P300 BCI paradigm
o = readNY(EXAMPLE_P300_1)

# compute means (adaptive weights and multivariate regression)
𝐌 = mean(o; overlapping=true, weights=:a)

# target average ERP
T_ERP = 𝐌[findfirst(isequal("target"), o.clabels)] 

# nontarget average ERP
NT_ERP = 𝐌[findfirst(isequal("nontarget"), o.clabels)] 
source
Eegle.ERPs.stim2markFunction
    function stim2mark( stim::Vector{S}, 
                        wl::S;
        offset::S=0, 
        code=nothing) 
    where S <: Int

Convert a stimulation vector into marker vectors.

Arguments

  • stim: the stimulation vector to be converted
  • wl: the window (trial or ERP) length in samples.

Optional Keyword Arguments

  • code: by default, the output will hold as many marker vectors as the largest tag (integers) in stim, which may or may not hold instances of all integers up to the largest. If there are missing integers, the corresponding marker vector will be empty. Alternatively, a vector of tags coding the classes of stimulations in stim can be passed as kwarg code. In this case, arbitrary non-zero tags can be used (even negative) and the number of marker vectors will be equal to the number of unique integers in code. If code is provided, the marker vectors are arranged in the order given there, otherwise the first vector corresponds to the tag 1, the second to tag 2, etc. In any case, in each vector, the samples are sorted in ascending order.
offset

Markers which value plus the offset is non-positive or exceeds the length of stim minus $wl$ will be ignored, as they cannot define a complete ERP (or trial). If this happens, passing the output to mark2stim will not return stim back exactly. Actually, calling this function and reverting the operation with mark2stim ensures that the stimulation vector is valid.

Return

A vector of $z$ marker vectors, where $z$ is the number of classes, i.e., the highest integer in stim or the number of non-zero elements in code if it is provided.

See mark2stim

Examples

using Eegle # or using Eegle.ERPs

sr, wl = 128, 256 # sampling rate, window length of trials
ns = sr*100 # number of samples of the recording

# simulate a valid stimulations vector for three classes
stim = vcat([rand()<0.01 ? rand(1:3) : 0 for i = 1:ns-wl], zeros(Int, wl))

mark = stim2mark(stim, wl)

stim2 = mark2stim(mark, ns) # is identical to stim

stim == stim2
source
Eegle.ERPs.mark2stimFunction
    function mark2stim( mark::Vector{Vector{S}}, 
                        ns::S;
        offset::S=0, code=nothing) 
    where S <: Int

Reverse transformation of stim2mark.

Note

If an offset has been used in stim2mark, -offset must be used here in order to get back the original stimulation vector.

If code is provided, it must not contain 0.

Examples see stim2mark

source
Base.mergeFunction
    function merge( mark::Vector{Vector{S}}, 
                    mergeClasses::Vector{Vector{S}})
    where S <: Int

Merge the vectors of marker vectors mark and sort the markers within each class. Return another marker vectors. The merging pattern is determined by mergeClasses.

As an example, suppose mark holds 4 vectors of markers and mergeClasses=[[1, 2], [3, 4]], then the result will hold two markers vectors, vectors 1 and 2 of mark concatenated and sorted and vectors 3 and 4 of in mark concatenated and sorted. Empty mark vectors will be ignored.

This can be used to merge classes in ERP and BCI experiments.

Examples

using Eegle # or using Eegle.ERPs
mark =  [   [128, 367], 
            [245, 765, 986],
            [467, 880, 1025, 1456],
            [728, 1230, 1330, 1550, 1980],  
        ]

merged = merge(mark, [[1, 2], [3, 4]])

# return: 2-element Vector{Vector{Int64}}:
#           [128, 245, 367, 765, 986]
#           [467, 728, 880, 1025, 1230, 1330, 1456, 1550, 1980]
source
Eegle.ERPs.trialsFunction
    function trials(X::Matrix{R}, 
                    mark::Vector{Vector{S}}, 
                    wl::S;
        shape::Symbol = :cat
        weights::Union{Vector{R}, Nothing} = nothing,
        linComb::Union{Vector{R}, S, Nothing} = nothing,
        offset::S = 0) 
    where {R<:Real, S<:Int}

Extract trials of duration wl from a tagged EEG recording X. Optionally, multiply them by weights and compute a linear combination across sensors thereof.

Tip

To extract trials and compute their mean, see mean; for segmenting non-tagged data, see Eegle.Processing.epoching.

Arguments

  • X: the whole EEG recording, a matrix of size $T×N$, where $T$ is the number of samples and $N$ the number of channels (sensors)
  • mark: a marker vectors
  • wl: the window (trial, e.g., ERP) length in samples.

Optional Keyword Arguments

  • shape: see below
  • weights: optional weights to be multiplied to the trials. Adaptive weights can be obtained passing the Eegle.ERPs.trialsWeights function.
Weights normalization

If custom weights are provided, weights must have the same shape as mark, that is, it must be a vector of as many vectors as classes of stimulations, each one holding the weights (real numbers) for the corresponding class. The mean of the weights for each class must be 1.

  • linComb: Optional linear combination to be applied to the trials, e.g., a spatial filter. It can be:
    • an integer: extract for each (weighted) trial only the data at the electrode indexed by linComb $∈[1,..,n]$ (linear combination by a one-hot vector)
    • a vector $f$ of $N$ real elements: extract for each (weighted) trial the linear combination $X_jf$.
  • offset: see offset.

Return

  • if shape:cat: a vector of vectors of trials or of linear combinations thereof
  • if shape == :cat (default): all trials or the linear combinations thereof concatenated in a single vector.

Each extracted trial is a $wl×N$ matrix if linComb is nothing (default), otherwise it a vector of $wl$ elements holding the linear combination.

Empty marker vectors

Empty marker vectors are ignored if shape is equal to :cat, otherwise an empty vector is returned in their corresponding positions.

Examples

using Eegle # or using Eegle.ERPs

# Example P300 BCI session 
o = readNY(EXAMPLE_P300_1)

# Extract 1-s trials starting at samples specified in `mark`
mark = [ [245, 658, 987], [258, 758, 1987]  ]

# since `shape=:cat` (default),
# `𝐗` will hold the six trials concatenated 
𝐗 = trials(o.X, mark, o.sr)

# extract only the time-series at electrodes Cz
𝐗 = trials(o.X, mark, o.sr; 
            linComb = findfirst(isequal("Cz"), o.sensors))

# `𝐗[1]`, `𝐗[2]`, ... are in this case vectors, not matrices

# suppose `f` is a spatial filter
f = randn(o.ne)

# extract the filtered trials
𝐗 = trials(o.X, mark, o.sr; linComb = f)
 
# `𝐗[1]`, `𝐗[2]`, ... are vectors in this case too

# extract the trials in separated vectors for each class
𝐗 = trials(o.X, mark, o.sr; shape=:byClass)

source
Eegle.ERPs.trialsWeightsFunction
    function trialsWeights( X::Matrix{R}, 
                            mark::Vector{Vector{S}},
                            wl::S;
        M::Union{Matrix{R}, Nothing} = nothing,
        offset::S = 0) 
    where {R<:Real, S<:Int}

Compute adaptive weights for trials as the inverse of their squared Frobenius norm, along the lines of (Congedo et al., 2016). The method is unsupervised, i.e., agnostic to class labels, but a supervised version is available using the M arguments.

Mean ERPs

This function is not needed to compute weighted mean ERPs, as this function is called by mean.

Arguments

  • X: the whole EEG recording, a matrix of size $T×N$, where $T$ is the number of samples and $N$ the number of channels (sensors), respectively
  • mark: the marker vectors. For empty vectors, an empty vector is returned
  • wl: the window (trial or ERP) length in samples.

Optional Keyword Arguments

  • M: if it is a vector holding z matrices of size $T×N$, where z is the number of vectors in mark (classes of stimulations), then the weights are computed as the inverse of the squared norms of $X_{j(i)}-M_i$ for all trials $X_{j(i)}$, $j \in \{1, \ldots, k\}$, $i \in \{i, \ldots, z\}$ for each class $i$ separately.
  • offset: see offset.

Examples

using Eegle # or using Eegle.ERPs

# Example P300 BCI session 
o = readNY(EXAMPLE_P300_1)

weights = trialsWeights(o.X, o.mark, o.wl) 

# weights[1] and weights[2] are the weights for class
# "non-target" and "target", respectively.

# check that the mean of weights within each class is 1
mean(weights[1])
mean(weights[2])

# Supervised weights using the mean ERPs
M = mean(o.X, o.wl, o.mark; overlapping=true, weights=:a)
weights = trialsWeights(o.X, o.mark, o.wl; M) 
# for the keyword argument, in Julia `M`, it is the same as `M=M`
source
Eegle.ERPs.rejectFunction
    function reject(X::Matrix{R}, 
                    stim::Vector{Int}, 
                    wl::S;
        offset::S = 0,
        upperLimit::Union{R, S} = 1.2,
        returnDetails::Bool=false) 
    where {R<:Real, S<:Int}

Automatic rejection of artefacted trials in tagged EEG data by automatic amplitude thresholding.

Read data and reject artifacts

This function is called by Eegle.InOut.readNY to perform artifact rejection while reading EEG data in the NY format. In turn, this is called by several functions, for example Eegle.BCI.crval.

Arguments

  • X: the whole EEG recording, a matrix of size $T×N$, where $T$ is the number of samples and $N$ the number of electrodes
  • stim: a stimulation vector
  • wl: the trial length, i.e., the ERPs or trial duration, in samples.

Optioanl Keyword Arguments

  • offset: see offset
  • upperLimit: modulate the definition of the upper threshold (see below). a reasonable value ∈[1, 1.6]
  • upperLimit: determine the output (see below).

Description

Let $v$ be the natural logarithm of the field root mean square (FRMS, see Eegle.Processing.globalFieldRMS) of X sorted in ascending order.

The lower threshold $l$ is defined as the tenth value of $v$ (robust minimum estimator).

The upper threshold $h$ is defined as

$h=m+((m-l)u)$,

where:

  • $m$ is the mean of the $2wl$ central values of $v$, taken as a robust central tendency estimator
  • $u$ is kwarg upperlimit (default=1.2).

All trials in which at least one sample of the log-FRMS exceeds $h$ (there are samples with over-threshold amplitude) or in which $l$ exceeds the log-FRMS all over the trial (almost zero everywhere) are rejected.

Return

  • if returnDetails is false (default), a 5-tuple holding the following objects:
    • the stimulation vector stim with the tags corresponding to rejected trials set to zero (accepted trials),
    • the stimulation vector stim with the tags corresponding to accepted trials set to zero (rejected trials),
    • the first object as marker vectors,
    • the second object as marker vectors,
    • the number of rejected trials per class as a vector of integers.
  • if returnDetails is true, a 9-tuple holding the above 5 objects and, in addition:
    • the log-FMRS (not sorted),
    • the mean $m$,
    • the lower threshold $l$,
    • the upper threshold $h$.
Algebraic relation

The elemet-wise sum of the first two returned objects is equal to the input stimulation vector stim.

Examples

using Eegle # or using Eegle.ERPs

cleanstim, rejecstim, cleanmark, rejecmark, rejected = reject(X, stim, wl; upperLimit=1.5)

R = reject(X, stim, wl; upperLimit=1.5, returnDetails = true) # R is a tuple of 9 objects

norm((R[1].+R[2]).-stim)==0 # should be true

Tutorials

Tutorial ML 1, Tutorial ML 2

source