From 052668017c7fa4cf90213a958f7a9835a04b6ac3 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 4 Jun 2026 23:10:57 +0800 Subject: [PATCH 1/6] BUG: add two failing tests to assess arbitrary order of channels and misalignment of evokeds in compare evoked topo mode --- mne/viz/tests/test_evoked.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index c1e88acc1ef..71f5abe21f8 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -442,6 +442,16 @@ def test_plot_compare_evokeds(evoked): # test defaults figs = plot_compare_evokeds(evoked) assert len(figs) == 3 + # test arbitrary ordering of channels is handled correctly, in topo mode + evoked_subset = evoked.copy().pick(["MEG 0113", "MEG 0112"]) + fig_ordered = plot_compare_evokeds(evoked_subset, axes="topo") + evoked_reordered = evoked_subset.copy() + evoked_reordered.reorder_channels(["MEG 0112", "MEG 0113"]) + figs_reordered = plot_compare_evokeds(evoked_reordered, axes="topo") + assert_allclose( + fig_ordered[0].axes[0].lines[0].get_ydata(), + figs_reordered[0].axes[0].lines[0].get_ydata(), + ) # test passing more than one evoked red, blue = evoked.copy(), evoked.copy() red.comment = red.comment + "*" * 100 @@ -463,6 +473,17 @@ def test_plot_compare_evokeds(evoked): yvals = line.get_ydata() assert (yvals < ylim[1]).all() assert (yvals > ylim[0]).all() + # test that the channels are aligned when many evoked + # are passed in different orders, in topo mode + evoked_subset = evoked.copy().pick(["MEG 0113", "MEG 0112"]) + evoked_reordered = evoked_subset.copy() + evoked_reordered.reorder_channels(["MEG 0112", "MEG 0113"]) + figs = plot_compare_evokeds( + dict(orig=evoked_subset, reordered=evoked_reordered), axes="topo" + ) + assert_allclose( + figs[0].axes[0].lines[0].get_ydata(), figs[0].axes[0].lines[1].get_ydata() + ) # test plotting eyetracking data plt.close("all") # close the previous figures as to avoid a too many figs warning info_tmp = mne.create_info(["pupil_left"], evoked.info["sfreq"], ["pupil"]) From 63af9e4ec5d245c580ab804109c2e6d466debc00 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 4 Jun 2026 23:18:31 +0800 Subject: [PATCH 2/6] BUG: fix channel ordering bug in plot_compare_evokeds when axes is topo --- mne/evoked.py | 7 ++++--- mne/viz/evoked.py | 9 ++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mne/evoked.py b/mne/evoked.py index 2f953ff24a0..309b14c3160 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -1556,7 +1556,7 @@ def _get_evoked_node(fname): return evoked_node -def _check_evokeds_ch_names_times(all_evoked): +def _check_evokeds_ch_names_times(all_evoked, inplace=False): evoked = all_evoked[0] ch_names = evoked.ch_names for ii, ev in enumerate(all_evoked[1:]): @@ -1564,8 +1564,9 @@ def _check_evokeds_ch_names_times(all_evoked): if set(ev.ch_names) != set(ch_names): raise ValueError(f"{evoked} and {ev} do not contain the same channels.") else: - warn("Order of channels differs, reordering channels ...") - ev = ev.copy() + print("Order of channels differs, reordering channels ...") + if not inplace: + ev = ev.copy() ev.reorder_channels(ch_names) all_evoked[ii + 1] = ev if not np.max(np.abs(ev.times - evoked.times)) < 1e-7: diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index a62d2379f03..3fe70236c92 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2858,9 +2858,7 @@ def plot_compare_evokeds( for evk in evokeds[cond]: _validate_type(evk, Evoked, "All evokeds entries ", "Evoked") # ensure same channels and times across all evokeds - all_evoked = sum(evokeds.values(), []) - _check_evokeds_ch_names_times(all_evoked) - del all_evoked + _check_evokeds_ch_names_times(sum(evokeds.values(), []), inplace=True) # get some representative info conditions = list(evokeds) @@ -3130,7 +3128,8 @@ def click_func( c_func = None if do_topo else combine_func all_data = list() all_cis = list() - for _picks, (ax, idx) in zip(picks, axes): + # We need to truncate axes because of a possible additional ax for the legend + for ax, idx in axes[: len(picks)]: data_dict = dict() ci_dict = dict() for cond in conditions: @@ -3145,7 +3144,7 @@ def click_func( combine, c_func, ch_type=ch_type, - picks=_picks, + picks=picks[idx], scaling=scalings, ci_fun=ci_fun, ) From 931a0b9f28702ce6850004122c3d58a4d1a5aafe Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 4 Jun 2026 23:59:42 +0800 Subject: [PATCH 3/6] BUG: add changelog --- doc/changes/dev/13943.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/dev/13943.bugfix.rst diff --git a/doc/changes/dev/13943.bugfix.rst b/doc/changes/dev/13943.bugfix.rst new file mode 100644 index 00000000000..15fcb578c3d --- /dev/null +++ b/doc/changes/dev/13943.bugfix.rst @@ -0,0 +1 @@ +Fix bugs on :func:`mne.viz.plot_compare_evokeds` when `axes="topo"`. Correct channel order when given in an arbitrary way, and align multiple evoked when channel orders are different, by `Lifeng Qiu Lin`_. \ No newline at end of file From 5ada9ec2b6f6eaf6c84515380c47c7196b3d94f0 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sat, 6 Jun 2026 11:53:06 +0800 Subject: [PATCH 4/6] BUG: return print to warn and expect warn in test --- mne/evoked.py | 2 +- mne/viz/tests/test_evoked.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mne/evoked.py b/mne/evoked.py index 309b14c3160..c724de87a62 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -1564,7 +1564,7 @@ def _check_evokeds_ch_names_times(all_evoked, inplace=False): if set(ev.ch_names) != set(ch_names): raise ValueError(f"{evoked} and {ev} do not contain the same channels.") else: - print("Order of channels differs, reordering channels ...") + warn("Order of channels differs, reordering channels ...") if not inplace: ev = ev.copy() ev.reorder_channels(ch_names) diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index 71f5abe21f8..5335f163bad 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -478,9 +478,11 @@ def test_plot_compare_evokeds(evoked): evoked_subset = evoked.copy().pick(["MEG 0113", "MEG 0112"]) evoked_reordered = evoked_subset.copy() evoked_reordered.reorder_channels(["MEG 0112", "MEG 0113"]) - figs = plot_compare_evokeds( - dict(orig=evoked_subset, reordered=evoked_reordered), axes="topo" - ) + # catch warnings when testing misalignment on purpose + with pytest.warns(RuntimeWarning, match="Order of channels differs"): + figs = plot_compare_evokeds( + dict(orig=evoked_subset, reordered=evoked_reordered), axes="topo" + ) assert_allclose( figs[0].axes[0].lines[0].get_ydata(), figs[0].axes[0].lines[1].get_ydata() ) From 158953262e8b0785f8520d08d54cc229624df74a Mon Sep 17 00:00:00 2001 From: Lifeng <76589235+Gnefil@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:43:54 +0800 Subject: [PATCH 5/6] Update doc/changes/dev/13943.bugfix.rst Co-authored-by: Marijn van Vliet --- doc/changes/dev/13943.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/13943.bugfix.rst b/doc/changes/dev/13943.bugfix.rst index 15fcb578c3d..9ac1b6c21d3 100644 --- a/doc/changes/dev/13943.bugfix.rst +++ b/doc/changes/dev/13943.bugfix.rst @@ -1 +1 @@ -Fix bugs on :func:`mne.viz.plot_compare_evokeds` when `axes="topo"`. Correct channel order when given in an arbitrary way, and align multiple evoked when channel orders are different, by `Lifeng Qiu Lin`_. \ No newline at end of file +Fix channel ordering bug in :func:`mne.viz.plot_compare_evokeds` when ``axes="topo"``, by `Lifeng Qiu Lin`_. \ No newline at end of file From 0ba11ceb0572a7b4995dd37b46ce00ffccbdc281 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Wed, 10 Jun 2026 00:51:39 +0800 Subject: [PATCH 6/6] BUG: assert only one axes content to ensure consistency in behaviour --- mne/viz/evoked.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 3fe70236c92..63aca378b59 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -3017,6 +3017,7 @@ def plot_compare_evokeds( if not do_topo: # add vacuous "index" (needed for topo) so same code works for both axes = [(ax, 0) for ax in axes] + assert len(axes) == 1 if np.array(picks).ndim < 2: picks = [picks] # enables zipping w/ axes else: