Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7c80b70
Add src to Report.add_bem
vferat Sep 13, 2024
e464260
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 13, 2024
1710b11
Update report.py
vferat Oct 4, 2024
690945a
Revert "Add src to Report.add_bem"
vferat Oct 4, 2024
00c60fa
Add fig parameter to src.plot
vferat Oct 4, 2024
b2da283
test forward rendering
vferat Oct 4, 2024
924c029
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 4, 2024
26b6a42
wip
vferat Feb 3, 2025
f7fa3b8
Merge branch 'dev-report-src' of https://github.com/vferat/mne-python…
vferat Feb 3, 2025
712ba43
Merge branch 'main' into dev-report-src
vferat Feb 3, 2025
45a4fa1
wip
vferat Feb 10, 2025
031fa66
Merge branch 'dev-report-src' of https://github.com/vferat/mne-python…
vferat Feb 10, 2025
dd59dfe
Merge remote-tracking branch 'upstream/main' into dev-report-src
vferat Feb 10, 2025
175a622
fix section
vferat Feb 10, 2025
9226b32
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 10, 2025
b7529b5
Update test_misc.py
vferat Feb 10, 2025
3f80b8f
Create 12848.newfeature.rst
vferat Feb 10, 2025
d8a7a41
Merge branch 'dev-report-src' of https://github.com/vferat/mne-python…
vferat Feb 10, 2025
0136108
Merge remote-tracking branch 'upstream/main' into dev-report-src
vferat Feb 21, 2025
5f38c72
wip
vferat Feb 21, 2025
6103859
Update 12848.newfeature.rst
vferat Feb 21, 2025
8664b45
Update 12848.newfeature.rst
vferat Feb 22, 2025
ea20780
Merge branch 'dev-report-src' of https://github.com/vferat/mne-python…
vferat Mar 17, 2025
d24ebde
Merge remote-tracking branch 'upstream/main' into dev-report-src
vferat Mar 17, 2025
64c1e13
Change forward figures
vferat Mar 17, 2025
aac99fb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 17, 2025
5d9ef52
Edit views
vferat Apr 14, 2025
665610b
Merge branch 'dev-report-src' of https://github.com/vferat/mne-python…
vferat Apr 14, 2025
0f556f7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 14, 2025
0d547dd
Merge remote-tracking branch 'upstream/main' into dev-report-src
vferat May 26, 2025
0468218
Add test_add_forward
vferat May 26, 2025
7f70ad5
Update test_report.py
vferat May 26, 2025
42eb09f
Merge remote-tracking branch 'upstream/main' into dev-report-src
vferat Jun 27, 2025
5f9e0e3
Merge branch 'main' into dev-report-src
vferat Jun 27, 2025
b483bf4
Update views
vferat Jun 27, 2025
c2abd6a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2025
35be9a7
Update view.py
vferat Jun 27, 2025
b18ca44
Merge branch 'dev-report-src' of https://github.com/vferat/mne-python…
vferat Jun 27, 2025
137c25b
Update view.py
vferat Jun 27, 2025
439ced5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2025
45f5eb4
FIX: Fix test
larsoner Jul 3, 2025
eb94cef
FIX: A few cleanups
larsoner Jul 3, 2025
f9d97a6
FIX: One more
larsoner Jul 5, 2025
1890199
FIX: Better
larsoner Jul 7, 2025
8398347
Merge branch 'main' into dev-report-src
larsoner Jul 7, 2025
88a21f8
FIX: Msg
larsoner Jul 7, 2025
d294ffe
FIX: Fixes
larsoner Jul 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/devel/12848.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add source space(s) visualization(s) in :func:`mne.Report.add_forward`, by `Victor Ferat`_.
139 changes: 134 additions & 5 deletions mne/report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,17 @@ def _fig_to_img(


def _get_bem_contour_figs_as_arrays(
*, sl, n_jobs, mri_fname, surfaces, orientation, src, show, show_orientation, width
*,
sl,
n_jobs,
mri_fname,
surfaces,
orientation,
src,
trans,
show,
show_orientation,
width,
):
"""Render BEM surface contours on MRI slices.

Expand All @@ -494,6 +504,7 @@ def _get_bem_contour_figs_as_arrays(
surfaces=surfaces,
orientation=orientation,
src=src,
trans=trans,
show=show,
show_orientation=show_orientation,
width=width,
Expand All @@ -507,6 +518,21 @@ def _get_bem_contour_figs_as_arrays(
return out


def _iterate_alignment_views(function, alpha, **kwargs):
"""Auxiliary function to iterate over views in trans fig."""
from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING

# TODO: Eventually maybe we should expose the size option?
size = (80, 80) if MNE_3D_BACKEND_TESTING else (800, 800)
fig = create_3d_figure(size, bgcolor=(0.5, 0.5, 0.5))
from ..viz.backends.renderer import backend

try:
return _itv(function, fig, **kwargs)
finally:
backend._close_3d_figure(fig)


def _iterate_trans_views(function, alpha, **kwargs):
"""Auxiliary function to iterate over views in trans fig."""
from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING
Expand All @@ -531,7 +557,18 @@ def _itv(function, fig, *, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES, **kwarg

function(fig=fig, **kwargs)

views = ("frontal", "lateral", "medial", "axial", "rostral", "coronal")
views = (
"right_lateral",
"right_anterolateral",
"anterior",
"left_anterolateral",
"left_lateral",
"superior",
"right_posterolateral",
"posterior",
"left_posterolateral",
"inferior",
)

images = []
for view in views:
Expand All @@ -544,7 +581,7 @@ def _itv(function, fig, *, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES, **kwarg
images.append(im)

images = np.concatenate(
[np.concatenate(images[:3], axis=1), np.concatenate(images[3:], axis=1)], axis=0
[np.concatenate(images[:5], axis=1), np.concatenate(images[5:], axis=1)], axis=0
)

try:
Expand Down Expand Up @@ -2655,6 +2692,8 @@ def add_bem(
self._add_bem(
subject=subject,
subjects_dir=subjects_dir,
src=None,
trans=None,
decim=decim,
n_jobs=n_jobs,
width=width,
Expand Down Expand Up @@ -3245,6 +3284,8 @@ def _render_one_bem_axis(
surfaces,
image_format,
orientation,
src=None,
trans=None,
decim=2,
n_jobs=None,
width=512,
Expand All @@ -3265,7 +3306,8 @@ def _render_one_bem_axis(
mri_fname=mri_fname,
surfaces=surfaces,
orientation=orientation,
src=None,
src=src,
trans=trans,
show=False,
show_orientation="always",
width=width,
Expand Down Expand Up @@ -3574,6 +3616,89 @@ def _add_forward(
replace=replace,
)

if subject:
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.

Hmm on second thought I think this is causing failures in CircleCI

https://app.circleci.com/pipelines/github/mne-tools/mne-bids-pipeline/5101/workflows/aa9790e5-cea0-4db5-aaf8-a2df99cdefba/jobs/81886

Not sure we should make add_forward behavior dependent on whether or not subject is passed. Technically it could be pulled from fwd["src"][0]["subject_his_id"] in most cases. I'll think on it a bit and maybe make this part opt-in with plot=False (default) | True for report.add_forward, especially since report.add_bem already exists which gives some overlapping behavior.

src = forward["src"]
trans = forward["mri_head_t"]
# Alignment
kwargs = dict(
info=forward["info"],
trans=trans,
src=src,
subject=subject,
subjects_dir=subjects_dir,
meg=["helmet", "sensors"],
show_axes=True,
eeg=dict(original=0.2, projected=0.8),
coord_frame="mri",
)
img, _ = _iterate_trans_views(
function=plot_alignment,
alpha=0.5,
max_width=self.img_max_width,
max_res=self.img_max_res,
**kwargs,
)
self._add_image(
img=img,
title="Alignment",
section=section,
caption=None,
image_format="png",
tags=tags,
replace=replace,
)
# Source space
kwargs = dict(
trans=trans,
subjects_dir=subjects_dir,
)

self._add_bem(
subject=subject,
subjects_dir=subjects_dir,
src=src,
trans=trans,
decim=1,
n_jobs=1,
width=512,
image_format=image_format,
title="Source space(s) (BEM view)",
section=section,
tags=tags,
replace=replace,
)
Comment on lines +3656 to +3669
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.

Okay it's this step that takes over 10 minutes (!) to compute on CIs. Since we already have report.add_bem, I don't think we actually want this here.

Rather than make this depend on subject, too, I'm going to make it not depend on subject, but add a separate plot=True by default. That way if people want the old behavior, they can get it with plot=False. And if subject is not defined, it can be pulled from the source space.


if src.kind == "surface" or src.kind == "mixed":
surfaces = dict(head=0.1, white=0.5)
else:
surfaces = dict(head=0.1)

kwargs = dict(
trans=trans,
src=src,
subject=subject,
subjects_dir=subjects_dir,
show_axes=False,
coord_frame="mri",
surfaces=surfaces,
)
img, _ = _iterate_alignment_views(
function=plot_alignment,
alpha=0.5,
max_width=self.img_max_width,
max_res=self.img_max_res,
**kwargs,
)
self._add_image(
img=img,
title="Source space(s) (3D view)",
section=section,
caption=None,
image_format="png",
tags=tags,
replace=replace,
)

def _add_inverse_operator(
self,
*,
Expand Down Expand Up @@ -4440,13 +4565,15 @@ def _add_bem(
*,
subject,
subjects_dir,
src,
trans,
decim,
n_jobs,
width=512,
image_format,
title,
tags,
section,
tags,
replace,
):
"""Render mri+bem (only PNG)."""
Expand All @@ -4472,6 +4599,8 @@ def _add_bem(
mri_fname=mri_fname,
surfaces=surfaces,
orientation=orientation,
src=src,
trans=trans,
decim=decim,
n_jobs=n_jobs,
width=width,
Expand Down
26 changes: 25 additions & 1 deletion mne/report/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,29 @@ def test_add_bem_n_jobs(n_jobs, monkeypatch):
assert 0.778 < corr < 0.80


@pytest.mark.filterwarnings("ignore:Distances could not be calculated.*:RuntimeWarning")
@pytest.mark.slowtest
@testing.requires_testing_data
def test_add_forward(renderer_interactive_pyvistaqt):
"""Test add_forward."""
report = Report(subjects_dir=subjects_dir, image_format="png")
report.add_forward(
forward=fwd_fname,
subject="sample",
subjects_dir=subjects_dir,
title="Forward solution",
)
assert len(report.html) == 4

report = Report(subjects_dir=subjects_dir, image_format="png")
report.add_forward(
forward=fwd_fname,
subjects_dir=subjects_dir,
title="Forward solution",
)
assert len(report.html) == 1


@testing.requires_testing_data
def test_render_mri_without_bem(tmp_path):
"""Test rendering MRI without BEM for mne report."""
Expand Down Expand Up @@ -882,7 +905,8 @@ def test_survive_pickle(tmp_path):

@pytest.mark.slowtest # ~30 s on Azure Windows
@testing.requires_testing_data
def test_manual_report_2d(tmp_path, invisible_fig):
@pytest.mark.filterwarnings("ignore:Distances could not be calculated.*:RuntimeWarning")
def test_manual_report_2d(tmp_path, invisible_fig, renderer_pyvistaqt):
"""Simulate user manually creating report by adding one file at a time."""
pytest.importorskip("sklearn")
pytest.importorskip("pandas")
Expand Down
8 changes: 8 additions & 0 deletions mne/source_space/_source_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ def plot(
skull=None,
subjects_dir=None,
trans=None,
*,
fig=None,
verbose=None,
):
"""Plot the source space.
Expand Down Expand Up @@ -358,6 +360,11 @@ def plot(
produced during coregistration. If trans is None, an identity
matrix is assumed. This is only needed when the source space is in
head coordinates.
fig : Figure3D | None
PyVista scene in which to plot the alignment.
If ``None``, creates a new 600x600 pixel figure with black background.

.. versionadded:: 1.10
%(verbose)s

Returns
Expand Down Expand Up @@ -427,6 +434,7 @@ def plot(
ecog=False,
bem=bem,
src=self,
fig=fig,
)

def __getitem__(self, *args, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion mne/tests/test_epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ def my_reject_2(epoch_data):
for kwarg in ("reject", "flat"):
with pytest.raises(
TypeError,
match=r".* must be an instance of .* got <class '.*'> instead.",
match=r".* must be an instance of .* got .* instead.",
):
epochs = Epochs(
raw,
Expand Down
11 changes: 6 additions & 5 deletions mne/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def translation(x=0, y=0, z=0):
return m


def _ensure_trans(trans, fro="mri", to="head"):
def _ensure_trans(trans, fro="mri", to="head", *, extra=""):
"""Ensure we have the proper transform."""
if isinstance(fro, str):
from_str = fro
Expand All @@ -455,7 +455,8 @@ def _ensure_trans(trans, fro="mri", to="head"):
to_str = _frame_to_str[to]
to_const = to
del to
err_str = f"trans must be a Transform between {from_str}<->{to_str}, got"
extra = f" {extra}" if extra else ""
err_str = f"trans must be a Transform between {from_str}<->{to_str}{extra}, got"
if not isinstance(trans, list | tuple):
trans = [trans]
# Ensure that we have exactly one match
Expand All @@ -481,12 +482,12 @@ def _ensure_trans(trans, fro="mri", to="head"):
return trans


def _get_trans(trans, fro="mri", to="head", allow_none=True):
def _get_trans(trans, fro="mri", to="head", allow_none=True, *, extra=""):
"""Get mri_head_t (from=mri, to=head) from mri filename."""
types = (Transform, "path-like")
if allow_none:
types += (None,)
_validate_type(trans, types, "trans")
_validate_type(trans, types, "trans", extra=extra)
if _path_like(trans):
if trans == "fsaverage":
trans = Path(__file__).parent / "data" / "fsaverage" / "fsaverage-trans.fif"
Expand All @@ -510,7 +511,7 @@ def _get_trans(trans, fro="mri", to="head", allow_none=True):
fro_to_t = Transform(fro, to)
trans = "identity"
# it's usually a head->MRI transform, so we probably need to invert it
fro_to_t = _ensure_trans(fro_to_t, fro, to)
fro_to_t = _ensure_trans(fro_to_t, fro, to, extra=extra)
return fro_to_t, trans


Expand Down
3 changes: 2 additions & 1 deletion mne/utils/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,10 @@ def _validate_type(item, types=None, item_name=None, type_name=None, *, extra=""
type_name[-1] = "or " + type_name[-1]
type_name = ", ".join(type_name)
_item_name = "Item" if item_name is None else item_name
_item_type = type(item) if item is not None else item
raise TypeError(
f"{_item_name} must be an instance of {type_name}{extra}, "
f"got {type(item)} instead."
f"got {_item_type} instead."
)


Expand Down
36 changes: 35 additions & 1 deletion mne/viz/_brain/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,40 @@
rh_views_dict["flat"] = dict(
azimuth=0, elevation=0, focalpoint=ORIGIN, roll=0, distance=DIST
)

both_views_dict = lh_views_dict.copy()
both_views_dict["right_lateral"] = dict(
azimuth=180.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["right_anterolateral"] = dict(
azimuth=120.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["anterior"] = dict(
azimuth=90.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["left_anterolateral"] = dict(
azimuth=60.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["left_lateral"] = dict(
azimuth=180.0, elevation=-90.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["right_posterolateral"] = dict(
azimuth=-120.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["posterior"] = dict(
azimuth=90.0, elevation=-90.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["left_posterolateral"] = dict(
azimuth=-60.0, elevation=90.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["superior"] = dict(
azimuth=180.0, elevation=0.0, focalpoint=ORIGIN, distance=DIST
)
both_views_dict["inferior"] = dict(
azimuth=180.0, elevation=180.0, focalpoint=ORIGIN, distance=DIST
)


views_dicts = dict(
lh=lh_views_dict, vol=lh_views_dict, both=lh_views_dict, rh=rh_views_dict
lh=lh_views_dict, vol=lh_views_dict, both=both_views_dict, rh=rh_views_dict
)
Loading