Skip to content

Speed up evoked time plotting - #14249

Merged
larsoner merged 13 commits into
mne-tools:mainfrom
larsoner:topo-blit
Sep 3, 2026
Merged

Speed up evoked time plotting#14249
larsoner merged 13 commits into
mne-tools:mainfrom
larsoner:topo-blit

Conversation

@larsoner

@larsoner larsoner commented Aug 29, 2026

Copy link
Copy Markdown
Member

Closes #14242

@wmvanvliet I generalized our blit code. Looks like it's actually pretty clean, steps:

  1. Generalize our brain blit code to a more general version _BlitManager
  2. Use that version in 3D plotting
  3. Use that version in evoked.plot and evoked.plot_topo
  4. Use that version in our animate_topomap code
  5. Speed up animate_topomap by actually blitting properly (working around a matplotilb issue where the figure would silently invalidate and draw nothing if you tried to use the correct blit path)

Minor changes to evoked.plot and evoked.topo are that the time axvline will stay on top now, but I think that's reasonable (and expected).

(5) was a pain to discover -- turns out to be caused by the modification of the contours, which removes and re-adds the set to the axes, which invalidates the draw, causing a full redraw (with nothing left to draw, so you get a blank plot). The workaround I came up with is to add a dummy no-op ax class pass to the contour call, and then pull the contours from it. I'm going to work on upstreaming a warning about this and a less-hacky way to get the contours... but it might be a bit to land etc. In the meantime I think the no-op class is workable, and has a big performance boost.

(5) speeds up report.add_evoked from about 8s to 5s by dropping the timepoint code from over 3s to under 1s.

I then also sped up how images are embedded in reports to cut down another second off that 5s add_evokeds.

I used Claude Opus 5 to investigate (iterating / reviewing multiple versions).

@larsoner

larsoner commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

@wmvanvliet okay to merge or did you need to test / look further?

* upstream/main: (22 commits)
  Avoid copying all epochs data in GetEpochsMixin._getitem (mne-tools#14262)
  Add Report.save(only_if_changed=True) (mne-tools#14261)
  Add jamica to related software [ci skip] (mne-tools#14260)
  Remove debugging cruft (mne-tools#14259)
  ENH: add Raw annotation span conversion (mne-tools#14240)
  Interactive dipole fitting: add STC mesh controls (mne-tools#14256)
  Document code principles in AGENTS.md (mne-tools#14239)
  MAINT: Update dependency specifiers (mne-tools#14257)
  [dependabot]: Bump the actions group with 2 updates (mne-tools#14258)
  ENH: Add JAMICA as an ICA method (mne-tools#14247)
  Reuse the MEF session across reads (mne-tools#14254)
  Read KIT data in cache-sized blocks (mne-tools#14255)
  Read EGI simple-binary event channels in blocks (mne-tools#14250)
  Decode Persyst and Nihon Kohden data in cache-sized blocks (mne-tools#14251)
  Normalize byte order before calibrating strided integer buffers (mne-tools#14252)
  Speed up EDF and BDF reading (mne-tools#14237)
  ENH: Add Forward-based projection reconstruction (mne-tools#14235)
  Read Artemis123, Curry, EEGLAB, Eximia, FIL and NSx in cache-sized blocks (mne-tools#14246)
  Remove rotating dipoles capability from interactive dipole fitting GUI (mne-tools#14243)
  Read BrainVision data in cache-sized blocks [ci skip] (mne-tools#14241)
  ...
@wmvanvliet

Copy link
Copy Markdown
Contributor

Could you explain a bit some of the changes in this PR? It looks like MNE-Python need to work around blitting bugs in matplotlib of something. Why is the coding dealing with low-level image functions?

@larsoner

larsoner commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Sure -- this PR was targeting blitting and speed, so I decided to tack on a few other loosely related fixes that could in principle be separated into separate PRs if needed.

1. Evoked.plot and plot_topo blitting

This was the starting idea and set of changes (the #14242 part).

2. Blitting bug for plot_topomap

Back in #13905, I spent a lot of time trying to get blitting right, but it turned out I got it mostly wrong because I hit a matplotlib bug involving silent invalidation of the figure when adding a new artist (adding a new contour to the figure caused it not draw the blit artists!). The real fix is in matplotlib/matplotlib#32271 (and another PR that warns a user that "this new artist broke blitting" that I haven't opened yet), but in the meantime the same thing -- getting new contours at the current levels for new data -- can be achieved by creating a dummy figure and/or axes, adding a contour to that with the same levels, and then pulling the necessary info from that temporary object over to the contour object on the blitting axes. Rather than doing plt.figure().axes().contour(...), NoopAxes allows creation of a ContourSet without ever actually drawing it or needing a real matplotlib figure. It's a safe enough work around until my upstream ContourSet.set_data PR lands and we (2 years from now) get to drop the workaround. (I'll add notes about that once the proper fix lands upstream and we can actually use it here.)

3. Report image capture

One case where these blitting and image paths matter is in report.add_evokeds. While incorporating _BlitManager there for the animate_topomap code, I noticed we were using an inefficient path: if we wanted an ndarray, we would fig.savefig to PNG then read this back to ndarray... when we could have accessed the fig.canvas data directly with proper casting. With this change, the report.add_evokeds is now bottlenecked by Agg draws, not our blitting code (1 and 2 above) or how we capture images (this point).

EDIT: Now allows lossy image saving, too, for speed and storage savings.

4. CircleCI

The CircleCI changes are just tacked on since they speed things up (and reduce cost!).

@wmvanvliet

Copy link
Copy Markdown
Contributor

I appreciate the explanation, now I know why this is way more complicated then I thought it would be.

@larsoner

larsoner commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Want me to split it up? Easy enough for me to do

@wmvanvliet wmvanvliet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just trying to follow all the logic. But it looks good, no need to split this up into smaller PRs I think.

Comment thread mne/report/report.py Outdated
Comment on lines +517 to +518
# Raw RGBA: the pixels are all that is wanted below, so encoding them as PNG
# only to decode them again is pure overhead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that things have moved around, this comment seems out of place. At this particular point, the code is clear: the caller asked for image_format="rgba" so that is what we will use. I think this comment needs to be further down (see my other comment)

Comment thread mne/report/report.py
pil_kwargs.update(optimize=True, compress_level=9)
output.seek(0)
orig = Image.open(output)
if mpl_format == "rgba":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is were we distinguish between whether the image was converted to svg/png or not and hence needs to be decoded. So this might be the place for a comment like "grab the pixels straight from the canvas"

@larsoner
larsoner enabled auto-merge (squash) September 3, 2026 13:54
@larsoner

larsoner commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Fixed, thanks for the review @wmvanvliet ! Marking for merge-when-green

@larsoner
larsoner merged commit 166e959 into mne-tools:main Sep 3, 2026
26 checks passed
@larsoner
larsoner deleted the topo-blit branch September 3, 2026 14:23
larsoner added a commit to natinew77-creator/mne-python that referenced this pull request Sep 3, 2026
* upstream/main: (37 commits)
  Speed up evoked time plotting (mne-tools#14249)
  Persist the numba JIT cache, sysmon coverage, refleak freeze (mne-tools#14265)
  Fix interpolate_to spline target positions (mne-tools#14266)
  CI reduce package builds on pull requests (mne-tools#14264)
  Avoid copying all epochs data in GetEpochsMixin._getitem (mne-tools#14262)
  Add Report.save(only_if_changed=True) (mne-tools#14261)
  Add jamica to related software [ci skip] (mne-tools#14260)
  Remove debugging cruft (mne-tools#14259)
  ENH: add Raw annotation span conversion (mne-tools#14240)
  Interactive dipole fitting: add STC mesh controls (mne-tools#14256)
  Document code principles in AGENTS.md (mne-tools#14239)
  MAINT: Update dependency specifiers (mne-tools#14257)
  [dependabot]: Bump the actions group with 2 updates (mne-tools#14258)
  ENH: Add JAMICA as an ICA method (mne-tools#14247)
  Reuse the MEF session across reads (mne-tools#14254)
  Read KIT data in cache-sized blocks (mne-tools#14255)
  Read EGI simple-binary event channels in blocks (mne-tools#14250)
  Decode Persyst and Nihon Kohden data in cache-sized blocks (mne-tools#14251)
  Normalize byte order before calibrating strided integer buffers (mne-tools#14252)
  Speed up EDF and BDF reading (mne-tools#14237)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Blitting code for other matplotlib time figures

2 participants