-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Adding sensitivity maps for forward models in mne.report #13722
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6feddac
e5bac3f
50fa6b6
7a8308b
8451506
7a31947
80b4078
a0763e4
d6c49f8
ca976bc
4c2c13c
1066d11
577f3c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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`_. |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||
|
|
@@ -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 | ||||||||||
|
|
@@ -1544,6 +1544,7 @@ def add_forward( | |||||||||
| subject=None, | ||||||||||
| subjects_dir=None, | ||||||||||
| plot=False, | ||||||||||
| sensitivity=False, | ||||||||||
| tags=("forward-solution",), | ||||||||||
| section=None, | ||||||||||
| replace=False, | ||||||||||
|
|
@@ -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 | ||||||||||
|
|
||||||||||
|
|
@@ -1587,6 +1596,7 @@ def add_forward( | |||||||||
| tags=tags, | ||||||||||
| replace=replace, | ||||||||||
| plot=plot, | ||||||||||
| sensitivity=sensitivity, | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| @fill_doc | ||||||||||
|
|
@@ -3614,6 +3624,7 @@ def _add_forward( | |||||||||
| title, | ||||||||||
| image_format, | ||||||||||
| plot, | ||||||||||
| sensitivity, | ||||||||||
| section, | ||||||||||
| tags, | ||||||||||
| replace, | ||||||||||
|
|
@@ -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") | ||||||||||
| 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: | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||
| 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"]) | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Also this should be above the opening |
||||||||||
|
|
||||||||||
| html_parts = [] | ||||||||||
| for ch_type in ch_types: | ||||||||||
| stc = sensitivity_map(forward, ch_type=ch_type, mode="fixed") | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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_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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||
| 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( | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| Epochs, | ||
| create_info, | ||
| pick_channels_cov, | ||
| pick_types_forward, | ||
| read_cov, | ||
| read_events, | ||
| read_evokeds, | ||
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.""" | ||
|
|
||
There was a problem hiding this comment.
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