Generate multi channel data

Here you will learn how to simulate EEG data for multiple channels/electrodes. The idea is to specify a signal on source level and then use a head model or a manual projection matrix to project the source signal to a number of electrodes.

Setup

Click to expand
# Load required packages
using UnfoldSim
using UnfoldMakie
using CairoMakie
using DataFrames
using Random

Specifying a design

We are using a one-level design for testing here.

design = SingleSubjectDesign(conditions = Dict(:condA => ["levelA"]));

Next we generate two simple components at two different times without any formula attached (we have a single condition anyway)

c = LinearModelComponent(; basis = p100(), formula = @formula(0 ~ 1), β = [1]);
c2 = LinearModelComponent(; basis = p300(), formula = @formula(0 ~ 1), β = [1]);

The multichannel component

Next, similar to the nested design above, we can nest the component in a MultichannelComponent. We could either provide the projection matrix manually, e.g.:

mc = UnfoldSim.MultichannelComponent(c, [1, 2, -1, 3, 5, 2.3, 1])
MultichannelComponent
  component: LinearModelComponent
  projection: Array{Float64}((7,)) [1.0, 2.0, -1.0, 3.0, 5.0, 2.3, 1.0]
  noise: NoNoise NoNoise()

or maybe more convenient: use the pair-syntax: Headmodel=>Label which makes use of a headmodel (HaRTmuT is currently easily available in UnfoldSim)

hart = Hartmut()
mc = UnfoldSim.MultichannelComponent(c, hart => "Left Postcentral Gyrus")
mc2 = UnfoldSim.MultichannelComponent(c2, hart => "Right Occipital Pole")
MultichannelComponent
  component: LinearModelComponent
  projection: Array{Float64}((227,)) [-0.03461859471337842, -0.04321094803502425, 0.0037088347968313525, -0.014722528968861278, -0.0234889834534478, 0.02731807504242923, 0.038863688452528036, 0.1190531258070562, -0.09956890221613562, -0.0867729334438599  …  0.37435404409695094, -0.020863789022627935, 0.25627478723535513, -0.05777985212119245, 0.37104376432271147, -0.19446620423767172, 0.2590764703721097, -0.12923837607416555, 0.1732886690359311, 0.4703016561960567]
  noise: NoNoise NoNoise()
Hint

You could also specify a noise-specific component which is applied prior to projection & summing with other components.

finally we need to define the onsets of the signal

onset = UniformOnset(; width = 20, offset = 4);

Simulation

Now as usual we simulate data. Inspecting data shows our result is now indeed ~230 Electrodes large! Nice!

data, events =
    simulate(MersenneTwister(1), design, [mc, mc2], onset, PinkNoise(noiselevel = 0.05));

size(data)
(227, 61)
Hint

The noise declared in the simulate function is added after mixing to channels, each channel receives independent noise. It is also possible to add noise to each individual component+source prior to projection. This would introduce correlated noise.

Plotting

Let's plot using Butterfly & Topoplot first we convert the electrodes to positions usable in TopoPlots.jl

pos3d = hart.electrodes["pos"]
pos2d = to_positions(pos3d')
pos2d = [Point2f(p[1] + 0.5, p[2] + 0.5) for p in pos2d];

now plot!

f = Figure()
df = DataFrame(
    :estimate => data[:],
    :channel => repeat(1:size(data, 1), outer = size(data, 2)),
    :time => repeat(1:size(data, 2), inner = size(data, 1)),
)
plot_butterfly!(f[1, 1:2], df; positions = pos2d)
plot_topoplot!(
    f[2, 1],
    df[df.time.==28, :];
    positions = pos2d,
    visual = (; enlarge = 0.5, label_scatter = false),
    axis = (; limits = ((0, 1), (0, 0.9))),
)
plot_topoplot!(
    f[2, 2],
    df[df.time.==48, :];
    positions = pos2d,
    visual = (; enlarge = 0.5, label_scatter = false),
    axis = (; limits = ((0, 1), (0, 0.9))),
)
f
Example block output

This page was generated using Literate.jl.