Speed measurement
Here we will compare the speed of plotting UnfoldMakie with MNE (Python) and EEGLAB (MATLAB).
Three cases are measured:
- Single topoplot
- Topoplot series with 50 topoplots
- Topoplott animation with 50 timestamps
Note that the results of benchmarking on your computer and on Github may differ.
using UnfoldMakie
using TopoPlots
using BenchmarkTools
using Observables
using CairoMakie
using PythonPlot;
using PyMNE;
CondaPkg Found dependencies: /home/runner/.julia/packages/CondaPkg/0UqYV/CondaPkg.toml
CondaPkg Found dependencies: /home/runner/.julia/packages/PyMNE/cNGDN/CondaPkg.toml
CondaPkg Found dependencies: /home/runner/.julia/packages/PythonCall/mkWc2/CondaPkg.toml
CondaPkg Found dependencies: /home/runner/.julia/packages/PythonPlot/oS8x4/CondaPkg.toml
CondaPkg Initialising pixi
│ /home/runner/.julia/artifacts/cefba4912c2b400756d043a2563ef77a0088866b/bin/pixi
│ init
│ --format pixi
└ /home/runner/work/UnfoldMakie.jl/UnfoldMakie.jl/docs/.CondaPkg
✔ Created /home/runner/work/UnfoldMakie.jl/UnfoldMakie.jl/docs/.CondaPkg/pixi.toml
CondaPkg Wrote /home/runner/work/UnfoldMakie.jl/UnfoldMakie.jl/docs/.CondaPkg/pixi.toml
│ [dependencies]
│ openssl = ">=3, <3.6"
│ libstdcxx = ">=3.4,<15.0"
│ uv = ">=0.4"
│ libstdcxx-ng = ">=3.4,<15.0"
│ matplotlib = ">=1"
│
│ [dependencies.python]
│ channel = "conda-forge"
│ build = "*cp*"
│ version = ">=3.9,<4, >=3.4,<4"
│
│ [project]
│ name = ".CondaPkg"
│ platforms = ["linux-64"]
│ channels = ["conda-forge", "anaconda"]
│ channel-priority = "strict"
│ description = "automatically generated by CondaPkg.jl"
│
│ [pypi-dependencies]
└ mne = ">=1.4"
CondaPkg Installing packages
│ /home/runner/.julia/artifacts/cefba4912c2b400756d043a2563ef77a0088866b/bin/pixi
│ install
└ --manifest-path /home/runner/work/UnfoldMakie.jl/UnfoldMakie.jl/docs/.CondaPkg/pixi.toml
✔ The default environment has been installed.
Precompiling UnfoldMakiePyMNEExt...
4028.6 ms ? Unfold
623.7 ms ? Unfold → UnfoldBSplineKitExt
7011.7 ms ? UnfoldMakie
Info Given UnfoldMakiePyMNEExt was explicitly requested, output will be shown live
┌ Warning: Module UnfoldMakie with build ID ffffffff-ffff-ffff-707f-8a44be731188 is missing from the cache.
│ This may mean UnfoldMakie [69a5ce3b-64fb-4f22-ae69-36dd4416af2a] does not support precompilation but is imported by a module that does.
└ @ Base loading.jl:2541
2031.7 ms ? UnfoldMakie → UnfoldMakiePyMNEExt
┌ Warning: Module UnfoldMakie with build ID ffffffff-ffff-ffff-707f-8a44be731188 is missing from the cache.
│ This may mean UnfoldMakie [69a5ce3b-64fb-4f22-ae69-36dd4416af2a] does not support precompilation but is imported by a module that does.
└ @ Base loading.jl:2541
Data input
dat, positions = TopoPlots.example_data()
df = UnfoldMakie.eeg_array_to_dataframe(dat[:, :, 1], string.(1:length(positions)));
Topoplots
UnfoldMakie.jl
@benchmark plot_topoplot(dat[:, 320, 1]; positions = positions)
BenchmarkTools.Trial: 138 samples with 1 evaluation per sample.
Range (min … max): 31.841 ms … 141.431 ms ┊ GC (min … max): 0.00% … 56.76%
Time (median): 32.792 ms ┊ GC (median): 0.00%
Time (mean ± σ): 36.333 ms ± 17.686 ms ┊ GC (mean ± σ): 6.91% ± 10.26%
█
██▇▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▁▁▁▁▁▅▁▁▁▄ ▄
31.8 ms Histogram: log(frequency) by time 131 ms <
Memory estimate: 8.94 MiB, allocs estimate: 134843.
UnfoldMakie.jl with DelaunayMesh
@benchmark plot_topoplot(
dat[:, 320, 1];
positions = positions,
topo_interpolation = (; interpolation = DelaunayMesh()),
)
BenchmarkTools.Trial: 138 samples with 1 evaluation per sample.
Range (min … max): 31.876 ms … 156.802 ms ┊ GC (min … max): 0.00% … 56.43%
Time (median): 32.829 ms ┊ GC (median): 0.00%
Time (mean ± σ): 36.433 ms ± 19.176 ms ┊ GC (mean ± σ): 6.50% ± 9.39%
█ ▂
█▇█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▁▁▁▁▁▁▁▁▁▁▅ ▄
31.9 ms Histogram: log(frequency) by time 150 ms <
Memory estimate: 8.94 MiB, allocs estimate: 134850.
MNE
posmat = collect(reduce(hcat, [[p[1], p[2]] for p in positions])')
pypos = Py(posmat).to_numpy()
pydat = Py(dat[:, 320, 1])
@benchmark begin
f = PythonPlot.figure()
PyMNE.viz.plot_topomap(
pydat,
pypos,
sphere = 1.1,
extrapolate = "box",
cmap = "RdBu_r",
sensors = false,
contours = 6,
)
f.show()
end
BenchmarkTools.Trial: 305 samples with 1 evaluation per sample.
Range (min … max): 11.834 ms … 657.591 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 12.123 ms ┊ GC (median): 0.00%
Time (mean ± σ): 16.402 ms ± 49.356 ms ┊ GC (mean ± σ): 0.00% ± 0.00%
▁▄▇▇█▆▅▃▁
█████████▄██▇▄▁▁▁▄▄▁▄▁▁▁▁▁▁▁▁▁▄▁▁▁▁▁▁▁▄▇▄▆▆▄▆▄▄▆▆▆▆▁▄▄▁▁▄▆▄▄ ▇
11.8 ms Histogram: log(frequency) by time 15.6 ms <
Memory estimate: 4.12 KiB, allocs estimate: 124.
Topoplot series
Note that UnfoldMakie and MNE have different defaults for displaying topoplot series. UnfoldMakie in plot_topoplot
averages over time samples. MNE in plot_topopmap
displays single samples without averaging.
UnfoldMakie.jl
@benchmark begin
plot_topoplotseries(
df;
bin_num = 50,
positions = positions,
axis = (; xlabel = "Time windows [s]"),
)
end
BenchmarkTools.Trial: 3 samples with 1 evaluation per sample.
Range (min … max): 1.928 s … 1.970 s ┊ GC (min … max): 5.31% … 3.36%
Time (median): 1.955 s ┊ GC (median): 5.24%
Time (mean ± σ): 1.951 s ± 21.727 ms ┊ GC (mean ± σ): 4.67% ± 1.14%
█ █ █
█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ ▁
1.93 s Histogram: frequency by time 1.97 s <
Memory estimate: 386.13 MiB, allocs estimate: 5517016.
MNE
easycap_montage = PyMNE.channels.make_standard_montage("standard_1020")
ch_names = pyconvert(Vector{String}, easycap_montage.ch_names)[1:64]
info = PyMNE.create_info(PyList(ch_names), ch_types = "eeg", sfreq = 1)
info.set_montage(easycap_montage)
simulated_epochs = PyMNE.EvokedArray(Py(dat[:, :, 1]), info)
@benchmark simulated_epochs.plot_topomap(1:50)
BenchmarkTools.Trial: 6 samples with 1 evaluation per sample.
Range (min … max): 690.643 ms … 2.171 s ┊ GC (min … max): 0.00% … 0.00%
Time (median): 697.245 ms ┊ GC (median): 0.00%
Time (mean ± σ): 944.715 ms ± 600.973 ms ┊ GC (mean ± σ): 0.00% ± 0.00%
█
█▆▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆ ▁
691 ms Histogram: frequency by time 2.17 s <
Memory estimate: 3.06 KiB, allocs estimate: 98.
MATLAB
Running MATLAB on a GitHub Action is not easy. So we benchmarked three consecutive executions (on a screenshot) on a server with an AMD EPYC 7452 32-core processor. Note that Github and the server we used for MATLAB benchmarking are two different computers, which can give different timing results.

Animation
The main advantage of Julia is the speed with which the figures are updated.
timestamps = range(1, 50, step = 1)
framerate = 50
50
UnfoldMakie with .gif
@benchmark begin
f = Makie.Figure()
dat_obs = Observable(dat[:, 1, 1])
plot_topoplot!(f[1, 1], dat_obs, positions = positions)
record(f, "topoplot_animation_UM.gif", timestamps; framerate = framerate) do t
dat_obs[] = @view(dat[:, t, 1])
end
end
BenchmarkTools.Trial: 2 samples with 1 evaluation per sample.
Range (min … max): 3.946 s … 3.979 s ┊ GC (min … max): 1.31% … 0.50%
Time (median): 3.962 s ┊ GC (median): 0.90%
Time (mean ± σ): 3.962 s ± 23.207 ms ┊ GC (mean ± σ): 0.90% ± 0.57%
█ █
█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ ▁
3.95 s Histogram: frequency by time 3.98 s <
Memory estimate: 678.78 MiB, allocs estimate: 896111.
MNE with .gif
@benchmark begin
fig, anim = simulated_epochs.animate_topomap(
times = Py(timestamps),
frame_rate = framerate,
blit = false,
image_interp = "cubic", # same as CloughTocher
)
anim.save("topomap_animation_mne.gif", writer = "ffmpeg", fps = framerate)
end
BenchmarkTools.Trial: 1 sample with 1 evaluation per sample.
Single result which took 9.115 s (0.00% GC) to evaluate,
with a memory estimate of 4.04 KiB, over 148 allocations.
Note, that due to some bugs in (probably) PythonCall
topoplot is black and white.
This page was generated using Literate.jl.