Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICA plot_components opens figure one by one (and might hang depending on the console) #11693

Closed
mscheltienne opened this issue May 12, 2023 · 4 comments · Fixed by #11696
Closed
Labels

Comments

@mscheltienne
Copy link
Member

mscheltienne commented May 12, 2023

Description of the problem

A video will be clearer. Same behavior in a regular python console and in an IPython console.

Screencast.from.05-12-2023.04.45.47.PM.webm

The figure are shown one by one after you close the old one. At least in a python or IPython console, it works. In spyder, it hangs the console after you close the first figure 😞

Tested both in a working environment and in a fresh environment with only pip install ipython mne installed (MNE 1.4).

Steps to reproduce

from mne.datasets import sample
from mne.io import read_raw_fif
from mne.preprocessing import ICA


directory = sample.data_path() / "MEG" / "sample"
raw = read_raw_fif(directory / "sample_audvis_raw.fif", preload=False)
raw.pick_types(eeg=True)
raw.crop(0, 10)
raw.load_data()
raw.filter(1., 40.)
ica = ICA(n_components=None)
ica.fit(raw)
ica.plot_components(inst=raw)

No idea what is causing it at the moment, will try to look into it. Might be related to #11654 and to #11510

@drammock
Copy link
Member

the plots are not created within a plt.ion() context so they're blocking execution. A solution would be to generate (but not show) the figures in the for-loop over subsets of components, then show all of them at the end after the for-loop completes. Getting this stuff to work right in all contexts (notebook, ipython, spyder, pycharm) is tricky; don't hesitate to ask for help if you get stuck.

@mscheltienne
Copy link
Member Author

mscheltienne commented May 15, 2023

I have to say, this blocking/event-loop/backend aspect of matplotlib always eluded me. Couple of additional points:

  • This issue did not appear because of a change we made. Testing the same code snippet in python/IPython/Spyder with MNE 1.2 results in the same behavior while it was correctly displaying all figures a couple of months ago. I tried rolling back some dependencies, including matplotlib, without luck. Not sure which one is responsible.
  • One solution is to provide the argument block=False here:
    plt_show(show)

    After this change, it does correctly open all figures BUT:
    • from a regular python interpreter the call becomes non-blocking, thus the code snippet above by itself would run and end immediately. Maybe that's how it should be?
    • from an IPython console or from a regular python interpreter (blocked with an input() at the end), opening the properties plot by clicking on the topographies works but with the lovely QCoreApplication::exec: The event loop is already running. This warning is not present in Spyder.

And for your proposition @drammock

show all of them at the end after the for-loop completes

I don't see an easy way to do it without adding an argument to flag if we are in a single execution (picks is not None) or in a recursive execution (initial call with picks=None) and I don't like this solution for a public function.
EDIT: I will rewrite this afternoon the function structure to remove the recursion, see if that helps.


Matplotlib backend used for testing: QtAgg with PyQt5.

@mscheltienne
Copy link
Member Author

mscheltienne commented May 15, 2023

More discoveries! In #11696 I removed the recursion in plot_ica_components and now gather all figures and show only once at the end. I don't see a downside in this approach, and it solves part of the issue. All the figures open at once.

But:

Speedy screencast to demonstrate (too bad it doesn't capture the mouse..)

Screencast.from.05-15-2023.02.46.05.PM.webm

And now the discovery part. It works on user-provided axes!

from matplotlib import pyplot as plt
from mne.datasets import sample
from mne.io import read_raw_fif
from mne.preprocessing import ICA


directory = sample.data_path() / "MEG" / "sample"
raw = read_raw_fif(directory / "sample_audvis_raw.fif", preload=False)
raw.pick_types(eeg=True)
raw.crop(0, 10)
raw.load_data()
raw.filter(1., 40.)
ica = ICA(n_components=5)
ica.fit(raw)

f, ax = plt.subplots(1, 2)
ica.plot_components(inst=raw, picks=[0, 2], axes=ax)

This works in all of those 4 tests and does not hang the interpreter in Spyder and Jupyter. Something is not behaving around here..

mne-python/mne/viz/utils.py

Lines 470 to 527 in 1e6aa7d

def _prepare_trellis(
n_cells,
ncols,
nrows="auto",
title=False,
colorbar=False,
size=1.3,
sharex=False,
sharey=False,
):
from matplotlib.gridspec import GridSpec
from ._mpl_figure import _figure
if n_cells == 1:
nrows = ncols = 1
elif isinstance(ncols, int) and n_cells <= ncols:
nrows, ncols = 1, n_cells
else:
if ncols == "auto" and nrows == "auto":
nrows = math.floor(math.sqrt(n_cells))
ncols = math.ceil(n_cells / nrows)
elif ncols == "auto":
ncols = math.ceil(n_cells / nrows)
elif nrows == "auto":
nrows = math.ceil(n_cells / ncols)
else:
naxes = ncols * nrows
if naxes < n_cells:
raise ValueError(
"Cannot plot {} axes in a {} by {} "
"figure.".format(n_cells, nrows, ncols)
)
if colorbar:
ncols += 1
width = size * ncols
height = (size + max(0, 0.1 * (4 - size))) * nrows + bool(title) * 0.5
height_ratios = None
fig = _figure(toolbar=False, figsize=(width * 1.5, 0.25 + height * 1.5))
gs = GridSpec(nrows, ncols, figure=fig, height_ratios=height_ratios)
axes = []
if colorbar:
# exclude last axis of each row except top row, which is for colorbar
exclude = set(range(2 * ncols - 1, nrows * ncols, ncols))
ax_idxs = sorted(set(range(nrows * ncols)) - exclude)[: n_cells + 1]
else:
ax_idxs = range(n_cells)
for ax_idx in ax_idxs:
subplot_kw = dict()
if ax_idx > 0:
if sharex:
subplot_kw.update(sharex=axes[0])
if sharey:
subplot_kw.update(sharey=axes[0])
axes.append(fig.add_subplot(gs[ax_idx], **subplot_kw))
return fig, axes, ncols, nrows
Probably the MNEFigure part since bypassing _figure also works.

@mscheltienne
Copy link
Member Author

plt.ion()
BACKEND = get_backend()
# This ↑↑↑↑↑↑↑↑↑↑↑↑↑ does weird things:
# https://github.com/matplotlib/matplotlib/issues/23298
# but wrapping it in ion() context makes it go away (can't actually use
# `with plt.ion()` as context manager, though, for compat reasons).
# Moving this bit to a separate function in ../../fixes.py doesn't work.
plt.ioff()

What is the purpose of those lines? If we remove them, everything works fine in python, ipython, spyder and jupyter (still needs to be tested in VSCode, PyCharm).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
2 participants