diff --git a/doc/changes/dev/14107.newfeature.rst b/doc/changes/dev/14107.newfeature.rst new file mode 100644 index 00000000000..a6506b53f03 --- /dev/null +++ b/doc/changes/dev/14107.newfeature.rst @@ -0,0 +1 @@ +Added the ``sensitivity`` parameter to :meth:`mne.Report.add_forward` to include forward sensitivity maps in reports, by :newcontrib:`Mariam Husain`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index ed7305e9eae..2cd2c840e60 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -213,6 +213,7 @@ .. _Mainak Jas: https://jasmainak.github.io .. _Maksym Balatsko: https://github.com/mbalatsko .. _Marcin Koculak: https://github.com/mkoculak +.. _Mariam Husain: https://github.com/mariam-hedgie .. _Marian Dovgialo: https://github.com/mdovgialo .. _Marijn van Vliet: https://github.com/wmvanvliet .. _Mark Alexander Henney: https://github.com/henneysq diff --git a/mne/report/report.py b/mne/report/report.py index 2e5828eb12a..5b7f86399c9 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -41,8 +41,8 @@ from ..minimum_norm import InverseOperator, read_inverse_operator from ..parallel import parallel_func from ..preprocessing.ica import read_ica -from ..proj import read_proj -from ..source_estimate import SourceEstimate, read_source_estimate +from ..proj import read_proj, sensitivity_map +from ..source_estimate import _BaseSourceEstimate, read_source_estimate from ..source_space._source_space import _ensure_src from ..surface import dig_mri_distances from ..transforms import _find_trans @@ -1583,6 +1583,7 @@ def add_forward( subject=None, subjects_dir=None, plot=False, + sensitivity=False, tags=("forward-solution",), section=None, replace=False, @@ -1604,6 +1605,10 @@ def add_forward( If True, plot the source space of the forward solution. .. versionadded:: 1.10 + sensitivity : bool + If True, render sensitivity maps for all available sensor types. + + .. versionadded:: 1.13 %(tags_report)s %(section_report)s @@ -1626,6 +1631,7 @@ def add_forward( tags=tags, replace=replace, plot=plot, + sensitivity=sensitivity, ) @fill_doc @@ -3709,6 +3715,7 @@ def _add_forward( title, image_format, plot, + sensitivity, section, tags, replace, @@ -3720,9 +3727,28 @@ def _add_forward( subject = self.subject if subject is None else subject subject = forward["src"][0]["subject_his_id"] if subject is None else subject - # XXX Todo - # Render sensitivity maps sensitivity_maps_html = "" + if sensitivity: + ch_types = forward["info"].get_channel_types(unique=True) + for ch_type in ("grad", "mag", "eeg"): + if ch_type not in ch_types: + continue + stc = sensitivity_map(forward, ch_type=ch_type) + html_partial = self._render_stc( + stc=stc, + title=f"{ch_type.upper()} sensitivity", + subject=subject, + subjects_dir=subjects_dir, + n_time_points=1, + image_format=image_format, + tags=tags, + stc_plot_kwargs=None, + ) + sensitivity_maps_html += html_partial( + id_=self._get_dom_id( + section=section, title=f"{title}-{ch_type}-sensitivity" + ) + ) source_space_html = "" if plot: source_space_html = self._src_html( @@ -4508,7 +4534,38 @@ def _add_stc( replace, ): """Render STC.""" - if isinstance(stc, SourceEstimate): + html_partial = self._render_stc( + stc=stc, + title=title, + subject=subject, + subjects_dir=subjects_dir, + n_time_points=n_time_points, + image_format=image_format, + tags=tags, + stc_plot_kwargs=stc_plot_kwargs, + ) + self._add_or_replace( + title=title, + section=section, + tags=tags, + html_partial=html_partial, + replace=replace, + ) + + def _render_stc( + self, + *, + stc, + title, + subject, + subjects_dir, + n_time_points, + image_format, + tags, + stc_plot_kwargs, + ): + """Render an STC as embeddable report HTML.""" + if isinstance(stc, _BaseSourceEstimate): if subject is None: subject = self.subject # supplied during Report init if not subject: @@ -4543,96 +4600,40 @@ def _add_stc( ) t_zero_idx = np.abs(times).argmin() # index of time closest to zero - # Plot using 3d backend if available, and use Matplotlib - # otherwise. - import matplotlib.pyplot as plt + if get_3d_backend() is None: + raise RuntimeError( + "A 3D backend is required to render source estimates in a report." + ) stc_plot_kwargs = _handle_default("report_stc_plot_kwargs", stc_plot_kwargs) stc_plot_kwargs.update(subject=subject, subjects_dir=subjects_dir) - # we need to set the size based on the min (img_max_width can be None) if self.img_max_width is not None: stc_plot_kwargs["size"] = ( stc_plot_kwargs["size"][0], min(stc_plot_kwargs["size"][1], self.img_max_width), ) - if get_3d_backend() is not None: - brain = stc.plot(**stc_plot_kwargs) - brain._renderer.plotter.subplot(0, 0) - backend_is_3d = True - else: - backend_is_3d = False + + brain = stc.plot(**stc_plot_kwargs) + brain._renderer.plotter.subplot(0, 0) figs = [] for t in times: - with warnings.catch_warnings(): - warnings.filterwarnings( - action="ignore", - message="More than 20 figures have been opened", - category=RuntimeWarning, - ) - - if backend_is_3d: - brain.set_time(t) - figs.append(brain.screenshot(time_viewer=True, mode="rgb")) - else: - fig_lh = plt.figure(layout="constrained") - fig_rh = plt.figure(layout="constrained") - - brain_lh = stc.plot( - views="lat", - hemi="lh", - initial_time=t, - backend="matplotlib", - subject=subject, - subjects_dir=subjects_dir, - figure=fig_lh, - ) - brain_rh = stc.plot( - views="lat", - hemi="rh", - initial_time=t, - subject=subject, - subjects_dir=subjects_dir, - backend="matplotlib", - figure=fig_rh, - ) - _constrain_fig_resolution( - fig_lh, - max_width=stc_plot_kwargs["size"][0], - max_res=self.img_max_res, - ) - _constrain_fig_resolution( - fig_rh, - max_width=stc_plot_kwargs["size"][0], - max_res=self.img_max_res, - ) - figs.append(brain_lh) - figs.append(brain_rh) - plt.close(fig_lh) - plt.close(fig_rh) - - if backend_is_3d: - brain.close() - else: - brain_lh.close() - brain_rh.close() + brain.set_time(t) + figs.append(brain.screenshot(time_viewer=True, mode="rgb")) + brain.close() captions = [f"Time point: {round(t, 3):0.3f} s" for t in times] - self._add_slider( + return self._render_slider( figs=figs, imgs=None, captions=captions, title=title, image_format=image_format, start_idx=t_zero_idx, - section=section, tags=tags, - replace=replace, - own_figure=False, # prevent rescaling + klass="stc", + own_figure=False, ) - for fig in figs: - if not isinstance(fig, np.ndarray): - plt.close(fig) @_use_agg def _add_bem( diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 7f27a0b07d4..85fb46e841b 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -4,6 +4,7 @@ import base64 import glob +import inspect import os import pickle import re @@ -35,6 +36,7 @@ _ALLOWED_IMAGE_FORMATS, CONTENT_ORDER, ) +from mne.source_estimate import SourceEstimate, VolSourceEstimate from mne.utils import Bunch, _record_warnings from mne.utils._testing import assert_object_equal from mne.viz import plot_alignment @@ -540,6 +542,100 @@ def test_add_forward(renderer_interactive_pyvistaqt): assert len(report.html) == 1 assert report.html[0].count("