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 Resolving changes
             + libstdcxx
             + libstdcxx-ng
             + matplotlib
             + mne (pip)
             + openssl
             + python
             + uv
    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.

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: 164 samples with 1 evaluation per sample.
 Range (minmax):  27.125 ms177.052 ms   GC (min … max): 0.00% … 64.50%
 Time  (median):     27.641 ms                GC (median):    0.00%
 Time  (mean ± σ):   30.426 ms ±  18.793 ms   GC (mean ± σ):  6.40% ±  8.54%

                                                              
  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄ ▄
  27.1 ms       Histogram: log(frequency) by time       161 ms <

 Memory estimate: 7.69 MiB, allocs estimate: 114690.

UnfoldMakie.jl with DelaunayMesh

@benchmark plot_topoplot(
    dat[:, 320, 1];
    positions = positions,
    topo_interpolation = (; interpolation = DelaunayMesh()),
)
BenchmarkTools.Trial: 163 samples with 1 evaluation per sample.
 Range (minmax):  26.858 ms194.453 ms   GC (min … max): 0.00% … 63.73%
 Time  (median):     27.590 ms                GC (median):    0.00%
 Time  (mean ± σ):   30.790 ms ±  20.846 ms   GC (mean ± σ):  6.86% ±  8.51%

                                                              
  ▁▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄ ▄
  26.9 ms       Histogram: log(frequency) by time       160 ms <

 Memory estimate: 7.69 MiB, allocs estimate: 114697.

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: 362 samples with 1 evaluation per sample.
 Range (minmax):  12.009 ms224.809 ms   GC (min … max): 0.00% … 0.00%
 Time  (median):     12.268 ms                GC (median):    0.00%
 Time  (mean ± σ):   13.805 ms ±  16.256 ms   GC (mean ± σ):  0.00% ± 0.00%

  ▆▆█▅▂                                                        
  █████▄▆▁▁▁▄▁▁▁▁▁▄▁▁▁▁▁▁▁▁▁▄▁▄▁▁▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▁▁▁▁▁▄ ▆
  12 ms         Histogram: log(frequency) by time      17.8 ms <

 Memory estimate: 3.30 KiB, allocs estimate: 98.

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: 4 samples with 1 evaluation per sample.
 Range (minmax):  1.380 s  1.523 s   GC (min … max): 0.00% … 7.88%
 Time  (median):     1.474 s               GC (median):    3.86%
 Time  (mean ± σ):   1.463 s ± 67.758 ms   GC (mean ± σ):  3.99% ± 4.45%

  █                                                   █  █  
  █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█▁▁█ ▁
  1.38 s         Histogram: frequency by time        1.52 s <

 Memory estimate: 310.04 MiB, allocs estimate: 3799886.

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 (minmax):  684.282 ms   1.162 s   GC (min … max): 0.00% … 0.00%
 Time  (median):     686.583 ms                GC (median):    0.00%
 Time  (mean ± σ):   837.670 ms ± 235.394 ms   GC (mean ± σ):  0.00% ± 0.00%

                                                                
  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▁▁▁▁▆ ▁
  684 ms           Histogram: frequency by time          1.16 s <

 Memory estimate: 2.39 KiB, allocs estimate: 69.

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 (minmax):  3.857 s  3.922 s   GC (min … max): 0.95% … 0.44%
 Time  (median):     3.890 s               GC (median):    0.69%
 Time  (mean ± σ):   3.890 s ± 46.357 ms   GC (mean ± σ):  0.69% ± 0.36%

                              ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ ▁
  3.86 s         Histogram: frequency by time        3.92 s <

 Memory estimate: 682.09 MiB, allocs estimate: 874999.

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.727 s (0.00% GC) to evaluate,
 with a memory estimate of 3.03 KiB, over 96 allocations.

Note, that due to some bugs in (probably) PythonCall topoplot is black and white.


This page was generated using Literate.jl.