diff --git a/doc/changes/dev/13307.newfeature.rst b/doc/changes/dev/13307.newfeature.rst new file mode 100644 index 00000000000..5917c49531f --- /dev/null +++ b/doc/changes/dev/13307.newfeature.rst @@ -0,0 +1 @@ +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`_. diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index d9447078d29..e1264bd2fe6 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -37,7 +37,15 @@ apply_trans, invert_transform, ) -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, _compute_forwards_meeg, @@ -437,6 +445,7 @@ def _prepare_for_forward( bem, mindist, n_jobs, + *, bem_extra="", trans="", info_extra="", @@ -444,6 +453,7 @@ def _prepare_for_forward( eeg=True, ignore_ref=False, allow_bem_none=False, + on_inside="raise", verbose=None, ): """Prepare for forward computation. @@ -567,11 +577,12 @@ def check_inside_head(x): meg_loc = apply_trans(invert_transform(mri_head_t), meg_loc) n_inside = check_inside_head(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) if len(src): rr = np.concatenate([s["rr"][s["vertno"]] for s in src]) @@ -610,6 +621,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. @@ -640,6 +652,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 @@ -710,12 +729,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) @@ -741,7 +761,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, @@ -767,6 +789,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 @@ -805,7 +834,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 268dd263af9..b584202465c 100644 --- a/mne/forward/tests/test_make_forward.py +++ b/mne/forward/tests/test_make_forward.py @@ -894,8 +894,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 94f186992e1..518e0ffe451 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, @@ -793,7 +794,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)