Skip to content
1 change: 1 addition & 0 deletions doc/changes/dev/13722.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ``sensitiviy`` kwarg in :meth:`mne.Report.add_forward` to get the sensitiviy map of the forward models, by `Himanshu Mahor`_.
143 changes: 139 additions & 4 deletions mne/report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from .. import __version__ as MNE_VERSION
from .._fiff.meas_info import Info, read_info
from .._fiff.pick import _DATA_CH_TYPES_SPLIT
from .._fiff.pick import _DATA_CH_TYPES_SPLIT, _contains_ch_type
from .._freesurfer import _mri_orientation, _reorient_image
from ..cov import Covariance, read_cov
from ..defaults import _handle_default
Expand All @@ -38,7 +38,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
Expand Down Expand Up @@ -1544,6 +1544,7 @@ def add_forward(
subject=None,
subjects_dir=None,
plot=False,
sensitivity=False,
tags=("forward-solution",),
section=None,
replace=False,
Expand All @@ -1565,6 +1566,14 @@ def add_forward(
If True, plot the source space of the forward solution.

.. versionadded:: 1.10
sensitivity : bool | list of str
If True, compute and plot sensitivity maps for all available
channel types (MEG gradiometers, MEG magnetometers, and EEG).
If a list, compute sensitivity maps for only the specified
channel types (e.g., ``['grad', 'mag']``).
Valid channel types are ``'grad'``, ``'mag'``, and ``'eeg'``.

.. versionadded:: 1.12
%(tags_report)s
%(section_report)s

Expand All @@ -1587,6 +1596,7 @@ def add_forward(
tags=tags,
replace=replace,
plot=plot,
sensitivity=sensitivity,
)

@fill_doc
Expand Down Expand Up @@ -3614,6 +3624,7 @@ def _add_forward(
title,
image_format,
plot,
sensitivity,
section,
tags,
replace,
Expand All @@ -3624,10 +3635,134 @@ 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
subjects_dir = self.subjects_dir if subjects_dir is None else subjects_dir

# XXX Todo
# Render sensitivity maps
sensitivity_maps_html = ""
if sensitivity:
if subjects_dir is None:
raise ValueError(
"subjects_dir must be provided to compute sensitivity maps"
)

info = forward["info"]
has_grad = _contains_ch_type(info, "grad")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use the public info.get_channel_types(unique=True) instead? Then you can just do something simpler like

info_ch_types = info.get_channel_types(unique=True)
all_ch_types = [key for key in ("grad", "mag", "eeg") if key in info_ch_types]

has_mag = _contains_ch_type(info, "mag")
has_eeg = _contains_ch_type(info, "eeg")

all_ch_types = []
if has_grad:
all_ch_types.append("grad")
if has_mag:
all_ch_types.append("mag")
if has_eeg:
all_ch_types.append("eeg")

if not all_ch_types:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are currently guaranteed that all our forward solutions contain some of these. If you want to be really safe you could make it do all of grad, mag, eeg, seeg, ecog and that would be future compatible

raise ValueError(
"No MEG or EEG channels found in forward solution. "
"Cannot compute sensitivity maps."
)

if sensitivity is True:
ch_types = all_ch_types
else:
ch_types = list(sensitivity)
for ch_type in ch_types:
_check_option("ch_type", ch_type, ["grad", "mag", "eeg"])
if ch_type not in all_ch_types:
raise ValueError(
f"Channel type '{ch_type}' not found in forward solution. "
f"Available types are: {all_ch_types}"
)

html_parts = []
for ch_type in ch_types:
_check_option("ch_type", ch_type, ["grad", "mag", "eeg"])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not as DRY as it could be. Making some of the checks a little redundant (they are very fast) seems worth the tradeoff

if sensitivity is True:
    ch_types = all_ch_types
else:
    ch_types = list(sensitivity)
for ci, ch_type in enumerate(ch_types):
    which = f"sensitivity[{ci}]"
    _validate_type(ch_type, str, which)
    if ch_type not in all_ch_types:
        raise ValueError(f"{which}={ch_type} is not ...")

Also this should be above the opening if clause

_validate_type(sensitivity, (bool, list, tuple), "sensitivity")


html_parts = []
for ch_type in ch_types:
stc = sensitivity_map(forward, ch_type=ch_type, mode="fixed")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this so we don't forget to allow it (or you can add support here!)

Suggested change
stc = sensitivity_map(forward, ch_type=ch_type, mode="fixed")
# TODO: Eventually mode should be a kwarg, and be allowed to be a list
# (which would create a slider rather than one image)
stc = sensitivity_map(forward, ch_type=ch_type, mode="fixed")


stc_plot_kwargs = _handle_default("report_stc_plot_kwargs", dict())
stc_plot_kwargs.update(
subject=subject,
subjects_dir=subjects_dir,
clim=dict(kind="value", lims=[0, 50, 100]),
colorbar=True,
)

import matplotlib.pyplot as plt

if get_3d_backend() is not None:
brain = stc.plot(**stc_plot_kwargs)
brain._renderer.plotter.subplot(0, 0)
fig, ax = plt.subplots(figsize=(4.5, 4.5), layout="constrained")
ax.imshow(brain.screenshot(time_viewer=False, mode="rgb"))
ax.axis("off")
_constrain_fig_resolution(
fig,
max_width=stc_plot_kwargs.get("size", (800, 600))[0],
max_res=self.img_max_res,
)
plt.close(fig)
brain.close()
else:
fig_lh = plt.figure(layout="constrained")
fig_rh = plt.figure(layout="constrained")
brain_lh = stc.plot(
views="lat",
hemi="lh",
initial_time=stc.times[0],
backend="matplotlib",
subject=subject,
subjects_dir=subjects_dir,
figure=fig_lh,
**stc_plot_kwargs,
)
brain_rh = stc.plot(
views="lat",
hemi="rh",
Comment on lines +3696 to +3724
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't typically do this conditional stuff. If you want 3D plots in reports you should have a 3D backend working. So just assume it is like we do in something like the alignment code, and use Brain(..., hemi="split") or similar rather than having two separate plot calls for right and left hemi

initial_time=stc.times[0],
subject=subject,
subjects_dir=subjects_dir,
backend="matplotlib",
figure=fig_rh,
**stc_plot_kwargs,
)
_constrain_fig_resolution(
fig_lh,
max_width=stc_plot_kwargs.get("size", (800, 600))[0],
max_res=self.img_max_res,
)
_constrain_fig_resolution(
fig_rh,
max_width=stc_plot_kwargs.get("size", (800, 600))[0],
max_res=self.img_max_res,
)
fig = fig_lh
plt.close(fig_rh)
brain_lh.close()
brain_rh.close()

img = self._fig_to_img(fig=fig, image_format=image_format)
plt.close(fig)

img_id = f"forward-sensitivity-{ch_type}"
img_html = _html_image_element(
id_=img_id,
img=img,
image_format=image_format,
caption=f"Sensitivity map ({ch_type})",
show=True,
div_klass="forward-sensitivity-map",
img_klass="forward-sensitivity-map",
title=f"Sensitivity Map - {ch_type.upper()}",
tags=(),
)
html_parts.append(img_html)

sensitivity_maps_html = "\n".join(html_parts)

source_space_html = ""
if plot:
source_space_html = self._src_html(
Expand Down
40 changes: 40 additions & 0 deletions mne/report/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Epochs,
create_info,
pick_channels_cov,
pick_types_forward,
read_cov,
read_events,
read_evokeds,
Expand All @@ -27,6 +28,7 @@
from mne.datasets import testing
from mne.epochs import make_metadata
from mne.fixes import _reshape_view
from mne.forward import read_forward_solution, write_forward_solution
from mne.io import RawArray, read_info, read_raw_fif
from mne.preprocessing import ICA
from mne.report import Report, _ReportScraper, open_report, report
Expand Down Expand Up @@ -541,6 +543,44 @@ def test_add_forward(renderer_interactive_pyvistaqt):
assert report.html[0].count("<img") == 0


@pytest.mark.slowtest
@testing.requires_testing_data
def test_add_forward_sensitivity_meg_only(tmp_path, monkeypatch, invisible_fig):
"""Test sensitivity maps for a MEG-only forward solution."""
meg_only_fwd = read_forward_solution(fwd_fname)
meg_only_fwd = pick_types_forward(meg_only_fwd, meg=True, eeg=False)
meg_only_fwd_fname = tmp_path / "sample_meg-only-fwd.fif"
write_forward_solution(meg_only_fwd_fname, meg_only_fwd, overwrite=True)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why write at all? This code should work when passing the instance directly

called_ch_types = []
brain = Bunch(
_renderer=Bunch(plotter=Bunch(subplot=lambda *args: None)),
screenshot=lambda **kwargs: np.zeros((4, 4, 3), dtype=np.uint8),
close=lambda: None,
)

def _fake_sensitivity_map(forward, ch_type, mode):
assert mode == "fixed"
called_ch_types.append(ch_type)
return Bunch(plot=lambda **kwargs: brain)

monkeypatch.setattr(report_mod, "get_3d_backend", lambda: "fake")
monkeypatch.setattr(report_mod, "sensitivity_map", _fake_sensitivity_map)

report = Report(subjects_dir=subjects_dir, image_format="png")
report.add_forward(
forward=meg_only_fwd_fname,
subjects_dir=subjects_dir,
title="Forward solution sensitivity",
plot=False,
sensitivity=True,
)
assert len(report.html) == 1
assert called_ch_types == ["grad", "mag"]
assert "Sensitivity map (grad)" in report.html[0]
assert "Sensitivity map (mag)" in report.html[0]
assert "Sensitivity map (eeg)" not in report.html[0]


@testing.requires_testing_data
def test_render_mri_without_bem(tmp_path):
"""Test rendering MRI without BEM for mne report."""
Expand Down
12 changes: 12 additions & 0 deletions tutorials/intro/70_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,18 @@
)
report.save("report_forward_sol.html", overwrite=True)

# %%
# To ge the sensitivity maps of forward models you could use ``sensitivity=True``.

report.add_forward(
forward=fwd_path,
title="Forward solution",
plot=True,
subjects_dir=subjects_dir,
sensitivity=True, # right here
)
report.save("report_forward_sol.html", overwrite=True)

# %%
# Adding an `~mne.minimum_norm.InverseOperator`
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
Loading