From 44de87724fd76790ad0a4f75f4cdfd65fe63d98e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 3 Jul 2025 09:25:47 -0400 Subject: [PATCH 1/4] ENH: Add on_inside kwarg to make_forward_solution --- doc/changes/devel/newfeature.rst | 1 + mne/forward/_make_forward.py | 53 ++++++++++++++++++++------ mne/forward/tests/test_make_forward.py | 7 +++- mne/simulation/raw.py | 10 ++++- 4 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 doc/changes/devel/newfeature.rst diff --git a/doc/changes/devel/newfeature.rst b/doc/changes/devel/newfeature.rst new file mode 100644 index 00000000000..47c773d1cd0 --- /dev/null +++ b/doc/changes/devel/newfeature.rst @@ -0,0 +1 @@ +Added ``on_inside="raise"`` parameter to :func:`mne.forward.make_forward_solution` and :func:`mne.forward.make_forward_dipole` to control behavior when MEG sensors are inside the outer skin surface. This is useful for forward solutions that are computed with sensors just inside the outer skin surface (e.g., with some OPM coregistrations), by `Eric Larson`_. diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index 6c77f47e312..34a214220a2 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -37,7 +37,15 @@ invert_transform, transform_surface_to, ) -from ..utils import _check_fname, _pl, _validate_type, logger, verbose, warn +from ..utils import ( + _check_fname, + _on_missing, + _pl, + _validate_type, + logger, + verbose, + warn, +) from ._compute_forward import _compute_forwards from .forward import _FWD_ORDER, Forward, _merge_fwds, convert_forward_solution @@ -433,6 +441,7 @@ def _prepare_for_forward( bem, mindist, n_jobs, + *, bem_extra="", trans="", info_extra="", @@ -440,6 +449,7 @@ def _prepare_for_forward( eeg=True, ignore_ref=False, allow_bem_none=False, + on_inside="raise", verbose=None, ): """Prepare for forward computation. @@ -563,11 +573,12 @@ def check_inside(x): ) n_inside = check_inside(meg_loc).sum() if n_inside: - raise RuntimeError( + msg = ( f"Found {n_inside} MEG sensor{_pl(n_inside)} inside the " f"{check_surface}, perhaps coordinate frames and/or " - "coregistration must be incorrect" + "coregistration are incorrect" ) + _on_missing(on_inside, msg, name="on_inside", error_klass=RuntimeError) rr = np.concatenate([s["rr"][s["vertno"]] for s in src]) if len(rr) < 1: @@ -603,6 +614,7 @@ def make_forward_solution( mindist=0.0, ignore_ref=False, n_jobs=None, + on_inside="raise", verbose=None, ): """Calculate a forward solution for a subject. @@ -633,6 +645,13 @@ def make_forward_solution( option should be True for KIT files, since forward computation with reference channels is not currently supported. %(n_jobs)s + on_inside : 'raise' | 'warn' | 'ignore' + What to do if MEG sensors are inside the outer skin surface. If 'raise' + (default), an error is raised. If 'warn' or 'ignore', the forward + solution is computed anyway and a warning is or isn't emitted, + respectively. + + .. versionadded:: 1.10 %(verbose)s Returns @@ -703,12 +722,13 @@ def make_forward_solution( bem, mindist, n_jobs, - bem_extra, - trans, - info_extra, - meg, - eeg, - ignore_ref, + bem_extra=bem_extra, + trans=trans, + info_extra=info_extra, + meg=meg, + eeg=eeg, + ignore_ref=ignore_ref, + on_inside=on_inside, ) del (src, mri_head_t, trans, info_extra, bem_extra, mindist, meg, eeg, ignore_ref) @@ -734,7 +754,9 @@ def make_forward_solution( @verbose -def make_forward_dipole(dipole, bem, info, trans=None, n_jobs=None, *, verbose=None): +def make_forward_dipole( + dipole, bem, info, trans=None, n_jobs=None, *, on_inside="raise", verbose=None +): """Convert dipole object to source estimate and calculate forward operator. The instance of Dipole is converted to a discrete source space, @@ -760,6 +782,13 @@ def make_forward_dipole(dipole, bem, info, trans=None, n_jobs=None, *, verbose=N The head<->MRI transform filename. Must be provided unless BEM is a sphere model. %(n_jobs)s + on_inside : 'raise' | 'warn' | 'ignore' + What to do if MEG sensors are inside the outer skin surface. If 'raise' + (default), an error is raised. If 'warn' or 'ignore', the forward + solution is computed anyway and a warning is or isn't emitted, + respectively. + + .. versionadded:: 1.10 %(verbose)s Returns @@ -798,7 +827,9 @@ def make_forward_dipole(dipole, bem, info, trans=None, n_jobs=None, *, verbose=N # Forward operator created for channels in info (use pick_info to restrict) # Use defaults for most params, including min_dist - fwd = make_forward_solution(info, trans, src, bem, n_jobs=n_jobs, verbose=verbose) + fwd = make_forward_solution( + info, trans, src, bem, n_jobs=n_jobs, on_inside=on_inside, verbose=verbose + ) # Convert from free orientations to fixed (in-place) convert_forward_solution( fwd, surf_ori=False, force_fixed=True, copy=False, use_cps=False, verbose=None diff --git a/mne/forward/tests/test_make_forward.py b/mne/forward/tests/test_make_forward.py index 27f0e26dfdd..aed157ec565 100644 --- a/mne/forward/tests/test_make_forward.py +++ b/mne/forward/tests/test_make_forward.py @@ -839,8 +839,11 @@ def test_sensors_inside_bem(): trans["trans"][2, 3] = 0.03 sphere_noshell = make_sphere_model((0.0, 0.0, 0.0), None) sphere = make_sphere_model((0.0, 0.0, 0.0), 1.01) - with pytest.raises(RuntimeError, match=".* 15 MEG.*inside the scalp.*"): - make_forward_solution(info, trans, fname_src, fname_bem) + with pytest.warns(RuntimeWarning, match=".* 15 MEG.*inside the scalp.*"): + fwd = make_forward_solution(info, trans, fname_src, fname_bem, on_inside="warn") + assert fwd["nsource"] == 516 + assert fwd["nchan"] == 42 + assert np.isfinite(fwd["sol"]["data"]).all() make_forward_solution(info, trans, fname_src, fname_bem_meg) # okay make_forward_solution(info, trans, fname_src, sphere_noshell) # okay with pytest.raises(RuntimeError, match=".* 42 MEG.*outermost sphere sh.*"): diff --git a/mne/simulation/raw.py b/mne/simulation/raw.py index 298cd9bf185..44940417d28 100644 --- a/mne/simulation/raw.py +++ b/mne/simulation/raw.py @@ -46,6 +46,7 @@ _check_preload, _pl, _validate_type, + _verbose_safe_false, check_random_state, logger, verbose, @@ -792,7 +793,14 @@ def _iter_forward_solutions( info.update(projs=[], bads=[]) # Ensure no 'projs' or 'bads' mri_head_t, trans = _get_trans(trans) sensors, rr, info, update_kwargs, bem = _prepare_for_forward( - src, mri_head_t, info, bem, mindist, n_jobs, allow_bem_none=True, verbose=False + src, + mri_head_t, + info, + bem, + mindist, + n_jobs, + allow_bem_none=True, + verbose=_verbose_safe_false(), ) del (src, mindist) From df4bc9ecee5113ee6e4c7c19ff9b4ac225c90588 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:39:23 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- doc/changes/devel/{newfeature.rst => 13307.newfeature.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changes/devel/{newfeature.rst => 13307.newfeature.rst} (100%) diff --git a/doc/changes/devel/newfeature.rst b/doc/changes/devel/13307.newfeature.rst similarity index 100% rename from doc/changes/devel/newfeature.rst rename to doc/changes/devel/13307.newfeature.rst From 6e28aeb4dd9d57487b8591bf22b7cb8443620311 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 3 Jul 2025 18:03:05 -0400 Subject: [PATCH 3/4] FIX: What --- doc/changes/devel/13307.newfeature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/devel/13307.newfeature.rst b/doc/changes/devel/13307.newfeature.rst index 47c773d1cd0..5917c49531f 100644 --- a/doc/changes/devel/13307.newfeature.rst +++ b/doc/changes/devel/13307.newfeature.rst @@ -1 +1 @@ -Added ``on_inside="raise"`` parameter to :func:`mne.forward.make_forward_solution` and :func:`mne.forward.make_forward_dipole` to control behavior when MEG sensors are inside the outer skin surface. This is useful for forward solutions that are computed with sensors just inside the outer skin surface (e.g., with some OPM coregistrations), by `Eric Larson`_. +Added ``on_inside="raise"`` parameter to :func:`mne.make_forward_solution` and :func:`mne.make_forward_dipole` to control behavior when MEG sensors are inside the outer skin surface. This is useful for forward solutions that are computed with sensors just inside the outer skin surface (e.g., with some OPM coregistrations), by `Eric Larson`_. From 2327049ee184b5fcb95c75fd6d8161a780276059 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 3 Jul 2025 18:39:01 -0400 Subject: [PATCH 4/4] TST: Ping