From ce9bb387beaaf5a57370df18fe710f73aac03be1 Mon Sep 17 00:00:00 2001
From: Mariam Husain <66348441+mariam-hedgie@users.noreply.github.com>
Date: Mon, 27 Jul 2026 21:22:30 +0530
Subject: [PATCH 01/11] test: add reproducer for report sensitivity maps
---
mne/report/tests/test_report.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
index 7f27a0b07d4..3aeef309e03 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
@@ -540,6 +541,22 @@ def test_add_forward(renderer_interactive_pyvistaqt):
assert len(report.html) == 1
assert report.html[0].count("
Date: Mon, 27 Jul 2026 21:56:57 +0530
Subject: [PATCH 02/11] enh: add forward sensitivity maps to reports
---
mne/report/report.py | 67 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 61 insertions(+), 6 deletions(-)
diff --git a/mne/report/report.py b/mne/report/report.py
index 2e5828eb12a..c76d32d75a3 100644
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -41,7 +41,7 @@
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 ..proj import read_proj, sensitivity_map
from ..source_estimate import SourceEstimate, read_source_estimate
from ..source_space._source_space import _ensure_src
from ..surface import dig_mri_distances
@@ -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,6 +4534,37 @@ def _add_stc(
replace,
):
"""Render STC."""
+ 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, SourceEstimate):
if subject is None:
subject = self.subject # supplied during Report init
@@ -4618,16 +4675,14 @@ def _add_stc(
brain_rh.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
)
for fig in figs:
From 394e75a7f1caaa4eda9d733c340d43ff4441e922 Mon Sep 17 00:00:00 2001
From: Mariam Husain <66348441+mariam-hedgie@users.noreply.github.com>
Date: Mon, 27 Jul 2026 22:06:43 +0530
Subject: [PATCH 03/11] test: cover report STC rendering edge cases
---
mne/report/tests/test_report.py | 84 +++++++++++++++++++++++++++++++++
1 file changed, 84 insertions(+)
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
index 3aeef309e03..d4109667b9d 100644
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -36,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
@@ -558,6 +559,89 @@ def test_add_forward_sensitivity_parameter():
assert "sensitivity" in inspect.signature(Report.add_forward).parameters
+class _FakeBrain:
+ """Minimal stand-in for a 3D source-estimate plot."""
+
+ def __init__(self):
+ self._renderer = Bunch(plotter=Bunch(subplot=lambda *_: None))
+
+ def close(self):
+ pass
+
+ def screenshot(self, **kwargs):
+ return np.zeros((2, 2, 3), np.uint8)
+
+ def set_time(self, time):
+ pass
+
+
+def _fake_stc_plot(*args, **kwargs):
+ """Return a lightweight source-estimate plot for report rendering tests."""
+ return _FakeBrain()
+
+
+def test_render_volume_stc(monkeypatch):
+ """Test rendering an in-memory volume source estimate."""
+ stc = VolSourceEstimate(
+ data=np.ones((1, 1)),
+ vertices=[np.array([0])],
+ tmin=0,
+ tstep=1,
+ subject="sample",
+ )
+ report = Report()
+ monkeypatch.setattr(report_mod, "get_3d_backend", lambda: "pyvista")
+ monkeypatch.setattr(VolSourceEstimate, "plot", _fake_stc_plot)
+ monkeypatch.setattr(
+ report_mod,
+ "read_source_estimate",
+ lambda **kwargs: pytest.fail("An in-memory STC must not be read from disk"),
+ )
+ html_partial = report._render_stc(
+ stc=stc,
+ title="Volume STC",
+ subject="sample",
+ subjects_dir=None,
+ n_time_points=1,
+ image_format="png",
+ tags=(),
+ stc_plot_kwargs=None,
+ )
+ assert callable(html_partial)
+
+
+def test_render_stc_matplotlib_captions(monkeypatch):
+ """Test Matplotlib source-estimate views have one caption per image."""
+ stc = SourceEstimate(
+ data=np.ones((2, 1)),
+ vertices=[np.array([0]), np.array([0])],
+ tmin=0,
+ tstep=1,
+ subject="sample",
+ )
+ report = Report()
+ slider_kwargs = {}
+
+ def _render_slider(**kwargs):
+ slider_kwargs.update(kwargs)
+ return lambda **kwargs: ""
+
+ monkeypatch.setattr(report_mod, "get_3d_backend", lambda: None)
+ monkeypatch.setattr(SourceEstimate, "plot", _fake_stc_plot)
+ monkeypatch.setattr(report, "_render_slider", _render_slider)
+ report._render_stc(
+ stc=stc,
+ title="Surface STC",
+ subject="sample",
+ subjects_dir=None,
+ n_time_points=1,
+ image_format="png",
+ tags=(),
+ stc_plot_kwargs=None,
+ )
+ assert len(slider_kwargs["figs"]) == len(slider_kwargs["captions"]) == 2
+
+
@testing.requires_testing_data
def test_render_mri_without_bem(tmp_path):
"""Test rendering MRI without BEM for mne report."""
From fb7c6b2406996fb7d40854586fcb9a26127921fd Mon Sep 17 00:00:00 2001
From: Mariam Husain <66348441+mariam-hedgie@users.noreply.github.com>
Date: Mon, 27 Jul 2026 22:08:05 +0530
Subject: [PATCH 04/11] fix: render report source estimates consistently
---
mne/report/report.py | 8 ++++++--
mne/report/tests/test_report.py | 9 +++++----
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/mne/report/report.py b/mne/report/report.py
index c76d32d75a3..b1ff69773c9 100644
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -42,7 +42,7 @@
from ..parallel import parallel_func
from ..preprocessing.ica import read_ica
from ..proj import read_proj, sensitivity_map
-from ..source_estimate import SourceEstimate, read_source_estimate
+from ..source_estimate import _BaseSourceEstimate, SourceEstimate, read_source_estimate
from ..source_space._source_space import _ensure_src
from ..surface import dig_mri_distances
from ..transforms import _find_trans
@@ -4565,7 +4565,7 @@ def _render_stc(
stc_plot_kwargs,
):
"""Render an STC as embeddable report HTML."""
- if isinstance(stc, SourceEstimate):
+ if isinstance(stc, _BaseSourceEstimate):
if subject is None:
subject = self.subject # supplied during Report init
if not subject:
@@ -4675,6 +4675,9 @@ def _render_stc(
brain_rh.close()
captions = [f"Time point: {round(t, 3):0.3f} s" for t in times]
+ if not backend_is_3d:
+ captions = [caption for caption in captions for _ in range(2)]
+ t_zero_idx *= 2
return self._render_slider(
figs=figs,
imgs=None,
@@ -4683,6 +4686,7 @@ def _render_stc(
image_format=image_format,
start_idx=t_zero_idx,
tags=tags,
+ klass="stc",
own_figure=False, # prevent rescaling
)
for fig in figs:
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
index d4109667b9d..3814567ed19 100644
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -613,9 +613,9 @@ def test_render_volume_stc(monkeypatch):
def test_render_stc_matplotlib_captions(monkeypatch):
"""Test Matplotlib source-estimate views have one caption per image."""
stc = SourceEstimate(
- data=np.ones((2, 1)),
+ data=np.ones((2, 3)),
vertices=[np.array([0]), np.array([0])],
- tmin=0,
+ tmin=-1,
tstep=1,
subject="sample",
)
@@ -634,12 +634,13 @@ def _render_slider(**kwargs):
title="Surface STC",
subject="sample",
subjects_dir=None,
- n_time_points=1,
+ n_time_points=3,
image_format="png",
tags=(),
stc_plot_kwargs=None,
)
- assert len(slider_kwargs["figs"]) == len(slider_kwargs["captions"]) == 2
+ assert len(slider_kwargs["figs"]) == len(slider_kwargs["captions"]) == 6
+ assert slider_kwargs["start_idx"] == 2
@testing.requires_testing_data
From 85b034ec13eed202f159f8a8641750edc8d45731 Mon Sep 17 00:00:00 2001
From: Mariam Husain <66348441+mariam-hedgie@users.noreply.github.com>
Date: Mon, 27 Jul 2026 22:16:07 +0530
Subject: [PATCH 05/11] refactor: remove unused report import
---
mne/report/report.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mne/report/report.py b/mne/report/report.py
index b1ff69773c9..6dea54aeee0 100644
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -42,7 +42,7 @@
from ..parallel import parallel_func
from ..preprocessing.ica import read_ica
from ..proj import read_proj, sensitivity_map
-from ..source_estimate import _BaseSourceEstimate, SourceEstimate, read_source_estimate
+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
From 8018785665079619342aeef86eccdd48a7d3a4cd Mon Sep 17 00:00:00 2001
From: Mariam Husain <66348441+mariam-hedgie@users.noreply.github.com>
Date: Mon, 27 Jul 2026 22:24:08 +0530
Subject: [PATCH 06/11] test: cover forward sensitivity report fallback
---
mne/report/report.py | 7 ++-----
mne/report/tests/test_report.py | 15 +++++++++++++++
2 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/mne/report/report.py b/mne/report/report.py
index 6dea54aeee0..6ac54c534a7 100644
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -4632,8 +4632,8 @@ def _render_stc(
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")
+ fig_lh = plt.figure()
+ fig_rh = plt.figure()
brain_lh = stc.plot(
views="lat",
@@ -4670,9 +4670,6 @@ def _render_stc(
if backend_is_3d:
brain.close()
- else:
- brain_lh.close()
- brain_rh.close()
captions = [f"Time point: {round(t, 3):0.3f} s" for t in times]
if not backend_is_3d:
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
index 3814567ed19..77b15525716 100644
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -559,6 +559,21 @@ def test_add_forward_sensitivity_parameter():
assert "sensitivity" in inspect.signature(Report.add_forward).parameters
+@testing.requires_testing_data
+def test_add_forward_sensitivity_matplotlib(monkeypatch):
+ """Test rendering forward sensitivity maps with the Matplotlib fallback."""
+ monkeypatch.setattr(report_mod, "get_3d_backend", lambda: None)
+ report = Report(subjects_dir=subjects_dir, image_format="png")
+ report.add_forward(
+ forward=fwd_fname,
+ subjects_dir=subjects_dir,
+ title="Forward solution",
+ sensitivity=True,
+ )
+ assert len(report.html) == 1
+ assert report.html[0].count("
Date: Mon, 27 Jul 2026 22:47:38 +0530
Subject: [PATCH 07/11] doc: add forward sensitivity changelog
---
doc/changes/dev/14107.newfeature.rst | 1 +
doc/changes/names.inc | 1 +
2 files changed, 2 insertions(+)
create mode 100644 doc/changes/dev/14107.newfeature.rst
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..f10d5a1455d 100644
--- a/doc/changes/names.inc
+++ b/doc/changes/names.inc
@@ -214,6 +214,7 @@
.. _Maksym Balatsko: https://github.com/mbalatsko
.. _Marcin Koculak: https://github.com/mkoculak
.. _Marian Dovgialo: https://github.com/mdovgialo
+.. _Mariam Husain: https://github.com/mariam-hedgie
.. _Marijn van Vliet: https://github.com/wmvanvliet
.. _Mark Alexander Henney: https://github.com/henneysq
.. _Mark Wronkiewicz: https://github.com/wronk
From bb3d2402a0c39cf807c8908a5d56b12f032353df Mon Sep 17 00:00:00 2001
From: Mariam Husain <66348441+mariam-hedgie@users.noreply.github.com>
Date: Tue, 28 Jul 2026 00:13:12 +0530
Subject: [PATCH 08/11] fix: remove unreachable report cleanup
---
mne/report/report.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/mne/report/report.py b/mne/report/report.py
index 6ac54c534a7..6e05eaf9dc5 100644
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -4686,9 +4686,6 @@ def _render_stc(
klass="stc",
own_figure=False, # prevent rescaling
)
- for fig in figs:
- if not isinstance(fig, np.ndarray):
- plt.close(fig)
@_use_agg
def _add_bem(
From 6bd7be417b6f87425d9182a6c6f524a01a1cea92 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Mon, 27 Jul 2026 19:14:21 +0000
Subject: [PATCH 09/11] [autofix.ci] apply automated fixes
---
doc/changes/names.inc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/changes/names.inc b/doc/changes/names.inc
index f10d5a1455d..2cd2c840e60 100644
--- a/doc/changes/names.inc
+++ b/doc/changes/names.inc
@@ -213,8 +213,8 @@
.. _Mainak Jas: https://jasmainak.github.io
.. _Maksym Balatsko: https://github.com/mbalatsko
.. _Marcin Koculak: https://github.com/mkoculak
-.. _Marian Dovgialo: https://github.com/mdovgialo
.. _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
.. _Mark Wronkiewicz: https://github.com/wronk
From 54908d14c1b76893b9054eb3f11c142e43a0258b Mon Sep 17 00:00:00 2001
From: Mariam Husain <66348441+mariam-hedgie@users.noreply.github.com>
Date: Tue, 28 Jul 2026 01:56:36 +0530
Subject: [PATCH 10/11] test: skip sensitivity report test without nibabel
---
mne/report/tests/test_report.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
index 77b15525716..d8c51d36f13 100644
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -562,6 +562,7 @@ def test_add_forward_sensitivity_parameter():
@testing.requires_testing_data
def test_add_forward_sensitivity_matplotlib(monkeypatch):
"""Test rendering forward sensitivity maps with the Matplotlib fallback."""
+ pytest.importorskip("nibabel")
monkeypatch.setattr(report_mod, "get_3d_backend", lambda: None)
report = Report(subjects_dir=subjects_dir, image_format="png")
report.add_forward(
From 2f90631e3ec7b5866d54d3433c1b02a285308f4c Mon Sep 17 00:00:00 2001
From: Mariam Husain <66348441+mariam-hedgie@users.noreply.github.com>
Date: Tue, 28 Jul 2026 02:21:35 +0530
Subject: [PATCH 11/11] fix: require 3d backend for report source estimates
---
mne/report/report.py | 74 +++++----------------------------
mne/report/tests/test_report.py | 52 +++++++----------------
2 files changed, 26 insertions(+), 100 deletions(-)
diff --git a/mne/report/report.py b/mne/report/report.py
index 6e05eaf9dc5..5b7f86399c9 100644
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -4600,81 +4600,29 @@ def _render_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()
- fig_rh = plt.figure()
-
- 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()
+ 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]
- if not backend_is_3d:
- captions = [caption for caption in captions for _ in range(2)]
- t_zero_idx *= 2
return self._render_slider(
figs=figs,
imgs=None,
@@ -4684,7 +4632,7 @@ def _render_stc(
start_idx=t_zero_idx,
tags=tags,
klass="stc",
- own_figure=False, # prevent rescaling
+ own_figure=False,
)
@_use_agg
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
index d8c51d36f13..85fb46e841b 100644
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -559,22 +559,6 @@ def test_add_forward_sensitivity_parameter():
assert "sensitivity" in inspect.signature(Report.add_forward).parameters
-@testing.requires_testing_data
-def test_add_forward_sensitivity_matplotlib(monkeypatch):
- """Test rendering forward sensitivity maps with the Matplotlib fallback."""
- pytest.importorskip("nibabel")
- monkeypatch.setattr(report_mod, "get_3d_backend", lambda: None)
- report = Report(subjects_dir=subjects_dir, image_format="png")
- report.add_forward(
- forward=fwd_fname,
- subjects_dir=subjects_dir,
- title="Forward solution",
- sensitivity=True,
- )
- assert len(report.html) == 1
- assert report.html[0].count("![Surface STC]()