From 44111b4462de5b46aceea2aa8cc6f69c2c706879 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Wed, 29 Jul 2026 00:05:24 +0200 Subject: [PATCH 01/12] ENH: Add extra source id parameter to vertex select --- mne/viz/_brain/_brain.py | 22 +++++++++++++++++----- mne/viz/ui_events.py | 5 +++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 1711333bf76..c3ad5716e9f 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1183,7 +1183,9 @@ def _configure_vertex_time_course(self): np.argmax(np.abs(use_data), axis=None), use_data.shape ) vertex_id = vertices[ind[0]] - publish(self, VertexSelect(hemi=hemi, vertex_id=vertex_id)) + publish( + self, VertexSelect(hemi=hemi, vertex_id=vertex_id, source_id=ind[0]) + ) def _configure_picking(self): # get data for each hemi @@ -1457,6 +1459,7 @@ def _on_pick(self, vtk_picker, event): # shift = np.array(grid.GetOrigin()) + spacing / 2. # ijk = np.round((pos - shift) / spacing).astype(int) # vertex_id = np.ravel_multi_index(ijk, shape, order='F') + source_id = idx else: vtk_cell = mesh.GetCell(cell_id) cell = [ @@ -1466,7 +1469,14 @@ def _on_pick(self, vtk_picker, event): vert_pos = mesh.points[cell] vertex_id = cell[np.argmin(np.linalg.norm(vert_pos - pos, axis=1))] - publish(self, VertexSelect(hemi=hemi, vertex_id=vertex_id)) + # retrieve the nearest source_id from the smooth_mat + smooth_mat = self.act_data_smooth[hemi][1] + # to deal CSR format and avoid materializing per click + # equivalent to: source_id = np.argmax(smooth_mat[vertex_id].toarray()) + lo, hi = smooth_mat.indptr[vertex_id], smooth_mat.indptr[vertex_id + 1] + source_id = smooth_mat.indices[lo + np.argmax(smooth_mat.data[lo:hi])] + + publish(self, VertexSelect(hemi=hemi, vertex_id=vertex_id, source_id=source_id)) def _on_time_change(self, event): """Respond to a time change UI event.""" @@ -4050,9 +4060,11 @@ def _update_current_time_idx(self, time_idx): mesh = self.layered_meshes[hemi] mesh.smooth_mat = hemi_data.get("smooth_mat") key_rng = [ - -key_data["fmax"] - if key_data["center"] is not None - else key_data["fmin"], + ( + -key_data["fmax"] + if key_data["center"] is not None + else key_data["fmin"] + ), key_data["fmax"], ] if data_key in mesh._overlays: diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 7667248a140..6fc7d6ba0aa 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -167,6 +167,8 @@ class VertexSelect(UIEvent): Can be ``"lh"``, ``"rh"``, or ``"vol"``. vertex_id : int The vertex number (in the high resolution mesh) that was selected. + source_id : int + The index number of the source estimate point that was selected. Attributes ---------- @@ -176,10 +178,13 @@ class VertexSelect(UIEvent): Can be ``"lh"``, ``"rh"``, or ``"vol"``. vertex_id : int The vertex number (in the high resolution mesh) that was selected. + source_id : int + The index number of the source estimate point that was selected. """ hemi: str vertex_id: int + source_id: int = None @dataclass From 1ee22165b1d2a7f06451b932efd4563021a38926 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 4 Aug 2026 09:15:34 +0200 Subject: [PATCH 02/12] ENH: Simplify argmax selection with non-zero guard --- mne/viz/_brain/_brain.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index c3ad5716e9f..e4f698accab 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1471,10 +1471,8 @@ def _on_pick(self, vtk_picker, event): # retrieve the nearest source_id from the smooth_mat smooth_mat = self.act_data_smooth[hemi][1] - # to deal CSR format and avoid materializing per click - # equivalent to: source_id = np.argmax(smooth_mat[vertex_id].toarray()) - lo, hi = smooth_mat.indptr[vertex_id], smooth_mat.indptr[vertex_id + 1] - source_id = smooth_mat.indices[lo + np.argmax(smooth_mat.data[lo:hi])] + row = smooth_mat[vertex_id] + source_id = smooth_mat[vertex_id].argmax() if row.nnz else None publish(self, VertexSelect(hemi=hemi, vertex_id=vertex_id, source_id=source_id)) From 8b84360c3370672ac164e50ea329f135fda69616 Mon Sep 17 00:00:00 2001 From: Lifeng <76589235+Gnefil@users.noreply.github.com> Date: Sat, 8 Aug 2026 01:50:06 +0800 Subject: [PATCH 03/12] Apply suggestions from code review Co-authored-by: Marijn van Vliet --- mne/viz/_brain/_brain.py | 2 +- mne/viz/ui_events.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index e4f698accab..c1586ca8566 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1184,7 +1184,7 @@ def _configure_vertex_time_course(self): ) vertex_id = vertices[ind[0]] publish( - self, VertexSelect(hemi=hemi, vertex_id=vertex_id, source_id=ind[0]) + self, VertexSelect(hemi=hemi, vertex_id=vertices[ind[0]], source_id=ind[0]) ) def _configure_picking(self): diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 6fc7d6ba0aa..c65fa86bbec 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -168,7 +168,8 @@ class VertexSelect(UIEvent): vertex_id : int The vertex number (in the high resolution mesh) that was selected. source_id : int - The index number of the source estimate point that was selected. + The index number of the closest source point to the vertex. + Only set if the publishing figure contains a source estimate. Attributes ---------- @@ -179,7 +180,8 @@ class VertexSelect(UIEvent): vertex_id : int The vertex number (in the high resolution mesh) that was selected. source_id : int - The index number of the source estimate point that was selected. + The index number of the closest source point to the vertex. + Only set if the publishing figure contains a source estimate. """ hemi: str From 9624128fc73ac1072c2cd773bb2e909de4ca676d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:52:38 +0000 Subject: [PATCH 04/12] [autofix.ci] apply automated fixes --- mne/viz/_brain/_brain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index c1586ca8566..9049c1f595e 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1184,7 +1184,8 @@ def _configure_vertex_time_course(self): ) vertex_id = vertices[ind[0]] publish( - self, VertexSelect(hemi=hemi, vertex_id=vertices[ind[0]], source_id=ind[0]) + self, + VertexSelect(hemi=hemi, vertex_id=vertices[ind[0]], source_id=ind[0]), ) def _configure_picking(self): From 6212a6470a566ea67a2978addcce69b4f0fda163 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Fri, 7 Aug 2026 19:53:43 +0200 Subject: [PATCH 05/12] ENH: Clean extra line --- mne/viz/_brain/_brain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 9049c1f595e..d904d301493 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1182,7 +1182,6 @@ def _configure_vertex_time_course(self): ind = np.unravel_index( np.argmax(np.abs(use_data), axis=None), use_data.shape ) - vertex_id = vertices[ind[0]] publish( self, VertexSelect(hemi=hemi, vertex_id=vertices[ind[0]], source_id=ind[0]), From 1b10afb50e8e9b0022f962548a51662bab0dfc7b Mon Sep 17 00:00:00 2001 From: Gnefil Date: Fri, 7 Aug 2026 20:07:51 +0200 Subject: [PATCH 06/12] DOC: Add changelog --- doc/changes/dev/14126.newfeature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/dev/14126.newfeature.rst diff --git a/doc/changes/dev/14126.newfeature.rst b/doc/changes/dev/14126.newfeature.rst new file mode 100644 index 00000000000..e4850922bd1 --- /dev/null +++ b/doc/changes/dev/14126.newfeature.rst @@ -0,0 +1 @@ +Add ``source_id`` as an optional parameter in :class:`mne.viz.ui_events.VertexSelect`, by `Lifeng Qiu Lin`_. \ No newline at end of file From 1563bc1a0a768b37e19d5fc9077dbdc2bc48578a Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sat, 8 Aug 2026 10:50:26 +0200 Subject: [PATCH 07/12] ENH: Add None option for source_id parameter --- mne/viz/ui_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index c65fa86bbec..bb54447a9e4 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -167,7 +167,7 @@ class VertexSelect(UIEvent): Can be ``"lh"``, ``"rh"``, or ``"vol"``. vertex_id : int The vertex number (in the high resolution mesh) that was selected. - source_id : int + source_id : int | None The index number of the closest source point to the vertex. Only set if the publishing figure contains a source estimate. @@ -179,7 +179,7 @@ class VertexSelect(UIEvent): Can be ``"lh"``, ``"rh"``, or ``"vol"``. vertex_id : int The vertex number (in the high resolution mesh) that was selected. - source_id : int + source_id : int | None The index number of the closest source point to the vertex. Only set if the publishing figure contains a source estimate. """ From 15f459a2127801c620b6f66c289fe45e5dbc6e8a Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sat, 8 Aug 2026 19:07:45 +0200 Subject: [PATCH 08/12] ENH: Add test for the new functionality --- mne/viz/_brain/tests/test_brain.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 3e94837886a..6f85b02cacb 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -1674,6 +1674,35 @@ def test_brain_ui_events(renderer_interactive_pyvistaqt, brain_gc): # Should remain unchanged. assert_array_equal(brain._data["ctable"][:3, 3], [0, 2, 4]) + # Test effect of vertex selection publishing. + events = list() + ui_events.subscribe(brain, "vertex_select", lambda event: events.append(event)) + + mesh = brain.layered_meshes["lh"]._polydata + faces = brain.geo["lh"].faces + vertices = brain._data["lh"]["vertices"] + smooth_mat = brain.act_data_smooth["lh"][1] + + # each for existing and missing source vertex cases + for is_source in (True, False): + mask = np.isin(faces[:, 0], vertices) + # select first vertex satisfying the condition + cell_id = np.where(mask if is_source else ~mask)[0][0] + vertex_id = faces[cell_id, 0] + # make the selection + n_events = len(events) + brain._on_pick(TstVTKPicker(mesh, cell_id, "lh", brain), None) + assert len(events) == n_events + 1 + event = events[-1] + assert event.vertex_id == vertex_id + row = smooth_mat[vertex_id, :] + if is_source: + assert event.source_id == row.argmax() + assert vertices[event.source_id] == event.vertex_id + else: + assert row.sum() == 0 + assert event.source_id is None + brain.close() From f918a32ce023e265d9d0e54aaf80adc11db35bf6 Mon Sep 17 00:00:00 2001 From: Lifeng <76589235+Gnefil@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:12:09 +0200 Subject: [PATCH 09/12] ENH: Remove variable renaming --- mne/viz/_brain/_brain.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 3f935ec682c..0706b43170d 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1476,15 +1476,14 @@ def _on_pick(self, vtk_picker, event): # dists = dists - dists.min() # dists = (1. - dists / dists.max()) * self._cmap_range[1] # grid.point_data['values'][vertices] = dists * mask - idx = idx[np.argmax(np.abs(scalars[idx]))] - vertex_id = vertices[idx] + source_id = idx[np.argmax(np.abs(scalars[idx]))] + vertex_id = vertices[source_id] # Naive way: convert pos directly to idx; i.e., apply mri_src_t # shape = self._data[hemi]['grid_shape'] # taking into account the cell vs point difference (spacing/2) # shift = np.array(grid.GetOrigin()) + spacing / 2. # ijk = np.round((pos - shift) / spacing).astype(int) # vertex_id = np.ravel_multi_index(ijk, shape, order='F') - source_id = idx else: vtk_cell = mesh.GetCell(cell_id) cell = [ From d30c03d6c044caeb86f91a2ea23b2f289e41a9fb Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 18 Aug 2026 13:36:49 +0200 Subject: [PATCH 10/12] DOC: Add an example tutorial for the vertex select function --- tutorials/visualization/20_ui_events.py | 56 ++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/tutorials/visualization/20_ui_events.py b/tutorials/visualization/20_ui_events.py index c75747a6e9b..71109263267 100644 --- a/tutorials/visualization/20_ui_events.py +++ b/tutorials/visualization/20_ui_events.py @@ -16,15 +16,17 @@ Since the figures on our website don't have any interaction capabilities, this example will only work properly when run in an interactive environment. """ + # Author: Marijn van Vliet # # License: BSD-3-Clause # Copyright the MNE-Python contributors. import matplotlib.pyplot as plt +import numpy as np import mne -from mne.viz.ui_events import TimeChange, link, publish, subscribe +from mne.viz.ui_events import TimeChange, VertexSelect, link, publish, subscribe # Turn on interactivity plt.ion() @@ -143,3 +145,55 @@ def on_time_change(event): # Method calls like this also emit the appropriate UI event. fig4.set_time(0.1) + + +######################################################################################## +# Reacting to a vertex selection +# ============================== +# Clicking a vertex on a :class:`~mne.viz.Brain` figure publishes a +# :class:`~mne.viz.ui_events.VertexSelect` event. Alongside ``vertex_id``, which is +# specific to the brain figure itself, a more useful payload is the ``source_id``: the +# index of the nearest point in the source space, what other plots need. +# +# Here we show an example to index the lead field (gain matrix) of a forward solution. +# The lead field shows the sensor pattern a unit dipole at that location would produce. +# Plotting the topomap after clicking the vertex answers the question: if this patch of +# cortex were active, which sensors would see it? + +fwd = mne.read_forward_solution( + data_path / "MEG" / "sample" / "sample_audvis-meg-eeg-oct-6-fwd.fif" +) +fwd = mne.convert_forward_solution(fwd, force_fixed=True, surf_ori=True, use_cps=True) +fwd = mne.pick_types_forward(fwd, meg="mag", eeg=False) +gain = fwd["sol"]["data"] # (n_channels, n_sources) + +hemi_offset = dict( + lh=0, rh=len(stc.vertices[0]) +) # right hemisphere after left (offset) + +fig6 = stc.plot("sample", subjects_dir=data_path / "subjects", hemi="both") +fig7, ax = plt.subplots(figsize=(4, 4), layout="constrained") + + +def on_vertex_select(event): + ax.clear() + if event.source_id is None: + # Parts of the mesh outside the source space + ax.set_title(f"vertex {event.vertex_id}:\nno nearby source point") + else: + column = hemi_offset[event.hemi] + event.source_id + mne.viz.plot_topomap(gain[:, column], fwd["info"], axes=ax, show=False) + ax.set_title(f"{event.hemi} source {event.source_id}") + fig7.canvas.draw() + + +subscribe(fig6, "vertex_select", on_vertex_select) + +# Publishing an example with the strongest source in the left hemisphere. +source_id = int(np.abs(stc.lh_data).max(axis=1).argmax()) +publish( + fig6, + VertexSelect( + hemi="lh", vertex_id=int(stc.vertices[0][source_id]), source_id=source_id + ), +) From ac75559fab2379629a19a50c4adaa0cbe3359c33 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 18 Aug 2026 13:59:33 +0200 Subject: [PATCH 11/12] DOC: Add changelog --- doc/changes/dev/14169.other.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/dev/14169.other.txt diff --git a/doc/changes/dev/14169.other.txt b/doc/changes/dev/14169.other.txt new file mode 100644 index 00000000000..e4586c308b9 --- /dev/null +++ b/doc/changes/dev/14169.other.txt @@ -0,0 +1 @@ +Add an example code script using ``VertexSelect`` leveraging ``source_id`` in ``20_ui_events.py``, by `Lifeng Qiu Lin`_. From 66f57a073b9a3b6eac03b132fb748e48b80562c6 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 20 Aug 2026 16:19:51 +0200 Subject: [PATCH 12/12] ENH: Add minor changes --- doc/changes/dev/{14169.other.txt => 14169.other.rst} | 0 tutorials/visualization/20_ui_events.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename doc/changes/dev/{14169.other.txt => 14169.other.rst} (100%) diff --git a/doc/changes/dev/14169.other.txt b/doc/changes/dev/14169.other.rst similarity index 100% rename from doc/changes/dev/14169.other.txt rename to doc/changes/dev/14169.other.rst diff --git a/tutorials/visualization/20_ui_events.py b/tutorials/visualization/20_ui_events.py index 71109263267..008ded5c834 100644 --- a/tutorials/visualization/20_ui_events.py +++ b/tutorials/visualization/20_ui_events.py @@ -153,7 +153,7 @@ def on_time_change(event): # Clicking a vertex on a :class:`~mne.viz.Brain` figure publishes a # :class:`~mne.viz.ui_events.VertexSelect` event. Alongside ``vertex_id``, which is # specific to the brain figure itself, a more useful payload is the ``source_id``: the -# index of the nearest point in the source space, what other plots need. +# index of the nearest point in the source space, which other plots need. # # Here we show an example to index the lead field (gain matrix) of a forward solution. # The lead field shows the sensor pattern a unit dipole at that location would produce.