diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f173768b2e..fd6070e29b2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -130,7 +130,8 @@ jobs: sed -i "/numba/d" environment.yml # And on Windows and macOS PySide6.9.0 segfaults elif [[ "$RUNNER_OS" == "macOS" ]]; then - sed -i "" "s/ - PySide6 .*/ - PySide6 <6.8/g" environment.yml + sed -i "" "s/ - PySide6 .*/ - PySide6 =6.7.3/g" environment.yml + sed -i "" "s/ - vtk .*/ - vtk =9.3.1/g" environment.yml elif [[ "$RUNNER_OS" == "Windows" ]]; then sed -i "s/ - PySide6 .*/ - PySide6 <6.8/g" environment.yml fi @@ -139,8 +140,10 @@ jobs: with: environment-file: ${{ env.CONDA_ENV }} environment-name: mne + log-level: ${{ runner.debug == '1' && 'debug' || 'info' }} create-args: >- python=${{ env.PYTHON_VERSION }} + -v if: ${{ !startswith(matrix.kind, 'pip') }} timeout-minutes: 20 - run: bash ./tools/github_actions_dependencies.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 458d447411f..e24e2cbdb74 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 + rev: v0.13.0 hooks: - id: ruff-check name: ruff lint mne @@ -58,7 +58,7 @@ repos: args: ["--ignore-case"] - repo: https://github.com/pappasam/toml-sort - rev: v0.24.2 + rev: v0.24.3 hooks: - id: toml-sort-fix files: pyproject.toml @@ -82,7 +82,7 @@ repos: # zizmor - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.12.1 + rev: v1.13.0 hooks: - id: zizmor diff --git a/doc/changes/dev/13398.apichange.rst b/doc/changes/dev/13398.apichange.rst new file mode 100644 index 00000000000..8b8adc0362a --- /dev/null +++ b/doc/changes/dev/13398.apichange.rst @@ -0,0 +1 @@ +The default for :func:`mne.make_field_map` will change to ``"auto"`` in MNE-Python 1.12 (from ``(0., 0., 0.04)``), changes by :newcontrib:`Paul Anders`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 38c240d69d7..754299f8575 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -231,6 +231,7 @@ .. _Pablo Mainar: https://github.com/pablomainar .. _Pablo-Arias: https://github.com/Pablo-Arias .. _Padma Sundaram: https://www.nmr.mgh.harvard.edu/user/8071 +.. _Paul Anders: https://github.com/Mettphysik .. _Paul Pasler: https://github.com/ppasler .. _Paul Roujansky: https://github.com/paulroujansky .. _Pavel Navratil: https://github.com/navrpa13 diff --git a/examples/visualization/mne_helmet.py b/examples/visualization/mne_helmet.py index a12ccd4f32f..ceb149d77ba 100644 --- a/examples/visualization/mne_helmet.py +++ b/examples/visualization/mne_helmet.py @@ -36,6 +36,7 @@ subject="sample", subjects_dir=subjects_dir, upsampling=2, + origin="auto", ) time = 0.083 fig = mne.viz.create_3d_figure((512, 512), bgcolor="w", title="MNE helmet") diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 0990ecd8fd4..44dbf4c32bc 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -934,7 +934,7 @@ def interpolate_bads( origin = _check_origin(origin, self.info) for ch_type, interp in method.items(): if interp == "nan": - _interpolate_bads_nan(self, ch_type, exclude=exclude) + _interpolate_bads_nan(self, ch_type=ch_type, exclude=exclude) if method.get("eeg", "") == "spline": _interpolate_bads_eeg(self, origin=origin, exclude=exclude) meg_mne = method.get("meg", "") == "MNE" diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index 7d5d1a981b7..c0a70a7d6ed 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -172,25 +172,25 @@ def _interpolate_bads_eeg(inst, origin, exclude=None, ecog=False, verbose=None): @verbose -def _interpolate_bads_ecog(inst, origin, exclude=None, verbose=None): +def _interpolate_bads_ecog(inst, *, origin, exclude=None, verbose=None): _interpolate_bads_eeg(inst, origin, exclude=exclude, ecog=True, verbose=verbose) def _interpolate_bads_meg( - inst, mode="accurate", origin=(0.0, 0.0, 0.04), verbose=None, ref_meg=False + inst, mode="accurate", *, origin, verbose=None, ref_meg=False ): return _interpolate_bads_meeg( - inst, mode, origin, ref_meg=ref_meg, eeg=False, verbose=verbose + inst, mode, ref_meg=ref_meg, eeg=False, origin=origin, verbose=verbose ) @verbose def _interpolate_bads_nan( inst, + *, ch_type, ref_meg=False, exclude=(), - *, verbose=None, ): info = _simplify_info(inst.info) @@ -208,12 +208,12 @@ def _interpolate_bads_nan( def _interpolate_bads_meeg( inst, mode="accurate", - origin=(0.0, 0.0, 0.04), + *, meg=True, eeg=True, ref_meg=False, exclude=(), - *, + origin, method=None, verbose=None, ): diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 62c7d79e3eb..de09a97c306 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -264,7 +264,7 @@ def test_interpolation_meg(): def _this_interpol(inst, ref_meg=False): from mne.channels.interpolation import _interpolate_bads_meg - _interpolate_bads_meg(inst, ref_meg=ref_meg, mode="fast") + _interpolate_bads_meg(inst, ref_meg=ref_meg, mode="fast", origin=(0.0, 0.0, 0.04)) return inst diff --git a/mne/conftest.py b/mne/conftest.py index f83f6e8bf78..fd50f48299d 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -672,6 +672,7 @@ def _check_skip_backend(name): assert name == "notebook", name pytest.importorskip("jupyter") pytest.importorskip("ipympl") + pytest.importorskip("ipyevents") pytest.importorskip("trame") pytest.importorskip("trame_vtk") pytest.importorskip("trame_vuetify") diff --git a/mne/forward/_field_interpolation.py b/mne/forward/_field_interpolation.py index c8deb8e15ff..8695154e1cc 100644 --- a/mne/forward/_field_interpolation.py +++ b/mne/forward/_field_interpolation.py @@ -22,7 +22,7 @@ from ..fixes import _safe_svd from ..surface import get_head_surf, get_meg_helmet_surf from ..transforms import _find_trans, transform_surface_to -from ..utils import _check_fname, _check_option, _pl, _reg_pinv, logger, verbose +from ..utils import _check_fname, _check_option, _pl, _reg_pinv, logger, verbose, warn from ._lead_dots import ( _do_cross_dots, _do_self_dots, @@ -116,7 +116,7 @@ def _pinv_tikhonov(x, reg): return inv, n -def _map_meg_or_eeg_channels(info_from, info_to, mode, origin, miss=None): +def _map_meg_or_eeg_channels(info_from, info_to, mode, *, origin, miss=None): """Find mapping from one set of channels to another. Parameters @@ -132,13 +132,15 @@ def _map_meg_or_eeg_channels(info_from, info_to, mode, origin, miss=None): origin : array-like, shape (3,) | str Origin of the sphere in the head coordinate frame and in meters. Can be ``'auto'``, which means a head-digitization-based origin - fit. Default is ``(0., 0., 0.04)``. + fit. Returns ------- mapping : array, shape (n_to, n_from) A mapping matrix. """ + assert origin is not None # should be assured elsewhere + # no need to apply trans because both from and to coils are in device # coordinates info_kinds = set(ch["kind"] for ch in info_to["chs"]) @@ -314,7 +316,8 @@ def _make_surface_mapping( trans=None, mode="fast", n_jobs=None, - origin=(0.0, 0.0, 0.04), + *, + origin, verbose=None, ): """Re-map M/EEG data to a surface. @@ -337,8 +340,6 @@ def _make_surface_mapping( %(n_jobs)s origin : array-like, shape (3,) | str Origin of the sphere in the head coordinate frame and in meters. - The default is ``'auto'``, which means a head-digitization-based - origin fit. %(verbose)s Returns @@ -347,6 +348,8 @@ def _make_surface_mapping( A n_vertices x n_sensors array that remaps the MEG or EEG data, as `new_data = np.dot(mapping, data)`. """ + assert origin is not None # should be assured elsewhere + if not all(key in surf for key in ["rr", "nn"]): raise KeyError('surf must have both "rr" and "nn"') if "coord_frame" not in surf: @@ -443,7 +446,7 @@ def make_field_map( ch_type=None, mode="fast", meg_surf="helmet", - origin=(0.0, 0.0, 0.04), + origin=None, n_jobs=None, *, upsampling=1, @@ -483,6 +486,9 @@ def make_field_map( fit. Default is ``(0., 0., 0.04)``. .. versionadded:: 0.11 + .. versionchanged:: 1.12 + In 1.12 the default value is "auto". + In 1.11 and prior versions, it is ``(0., 0., 0.04)``. %(n_jobs)s %(helmet_upsampling)s @@ -498,6 +504,15 @@ def make_field_map( The surface maps to be used for field plots. The list contains separate ones for MEG and EEG (if both MEG and EEG are present). """ + if origin is None: + warn_message = ( + 'Default value for origin is "(0.0, 0.0, 0.04)" in version 1.11 ' + 'but will be changed to "auto" in 1.12. Set the origin parameter ' + "explicitly to avoid this warning." + ) + warn(warn_message, FutureWarning) + origin = (0.0, 0.0, 0.04) + info = evoked.info if ch_type is None: diff --git a/mne/forward/tests/test_field_interpolation.py b/mne/forward/tests/test_field_interpolation.py index 98f84dd0b87..57b204d97af 100644 --- a/mne/forward/tests/test_field_interpolation.py +++ b/mne/forward/tests/test_field_interpolation.py @@ -50,8 +50,14 @@ def test_field_map_ctf(): evoked = Epochs(raw, events).average() evoked.pick(evoked.ch_names[:50]) # crappy mapping but faster # smoke test - passing trans_fname as pathlib.Path as additional check + # set origin to "(0.0, 0.0, 0.04)", which was the default until v1.12 + # estimating origin from "auto" impossible due to missing digitization points make_field_map( - evoked, trans=Path(trans_fname), subject="sample", subjects_dir=subjects_dir + evoked, + trans=Path(trans_fname), + subject="sample", + subjects_dir=subjects_dir, + origin=(0.0, 0.0, 0.04), ) @@ -128,11 +134,13 @@ def test_make_field_map_eeg(): evoked.info["bads"] = ["MEG 2443", "EEG 053"] # add some bads surf = get_head_surf("sample", subjects_dir=subjects_dir) # we must have trans if surface is in MRI coords - pytest.raises(ValueError, _make_surface_mapping, evoked.info, surf, "eeg") + pytest.raises( + ValueError, _make_surface_mapping, evoked.info, surf, "eeg", origin="auto" + ) evoked.pick(picks="eeg") fmd = make_field_map( - evoked, trans_fname, subject="sample", subjects_dir=subjects_dir + evoked, trans_fname, subject="sample", subjects_dir=subjects_dir, origin="auto" ) # trans is necessary for EEG only @@ -143,10 +151,11 @@ def test_make_field_map_eeg(): None, subject="sample", subjects_dir=subjects_dir, + origin="auto", ) fmd = make_field_map( - evoked, trans_fname, subject="sample", subjects_dir=subjects_dir + evoked, trans_fname, subject="sample", subjects_dir=subjects_dir, origin="auto" ) assert len(fmd) == 1 assert_array_equal(fmd[0]["data"].shape, (642, 59)) # maps data onto surf @@ -163,31 +172,37 @@ def test_make_field_map_meg(): # let's reduce the number of channels by a bunch to speed it up info["bads"] = info["ch_names"][:200] # bad ch_type - pytest.raises(ValueError, _make_surface_mapping, info, surf, "foo") + pytest.raises(ValueError, _make_surface_mapping, info, surf, "foo", origin="auto") # bad mode - pytest.raises(ValueError, _make_surface_mapping, info, surf, "meg", mode="foo") + pytest.raises( + ValueError, _make_surface_mapping, info, surf, "meg", mode="foo", origin="auto" + ) # no picks evoked_eeg = evoked.copy().pick(picks="eeg") - pytest.raises(RuntimeError, _make_surface_mapping, evoked_eeg.info, surf, "meg") + pytest.raises( + RuntimeError, _make_surface_mapping, evoked_eeg.info, surf, "meg", origin="auto" + ) # bad surface def nn = surf["nn"] del surf["nn"] - pytest.raises(KeyError, _make_surface_mapping, info, surf, "meg") + pytest.raises(KeyError, _make_surface_mapping, info, surf, "meg", origin="auto") surf["nn"] = nn cf = surf["coord_frame"] del surf["coord_frame"] - pytest.raises(KeyError, _make_surface_mapping, info, surf, "meg") + pytest.raises(KeyError, _make_surface_mapping, info, surf, "meg", origin="auto") surf["coord_frame"] = cf # now do it with make_field_map evoked.pick(picks="meg") evoked.info.normalize_proj() # avoid projection warnings - fmd = make_field_map(evoked, None, subject="sample", subjects_dir=subjects_dir) + fmd = make_field_map( + evoked, None, subject="sample", subjects_dir=subjects_dir, origin="auto" + ) assert len(fmd) == 1 assert_array_equal(fmd[0]["data"].shape, (304, 106)) # maps data onto surf assert len(fmd[0]["ch_names"]) == 106 - pytest.raises(ValueError, make_field_map, evoked, ch_type="foobar") + pytest.raises(ValueError, make_field_map, evoked, ch_type="foobar", origin="auto") # now test the make_field_map on head surf for MEG evoked.pick(picks="meg") @@ -198,6 +213,7 @@ def test_make_field_map_meg(): meg_surf="head", subject="sample", subjects_dir=subjects_dir, + origin="auto", ) assert len(fmd) == 1 assert_array_equal(fmd[0]["data"].shape, (642, 106)) # maps data onto surf @@ -210,6 +226,7 @@ def test_make_field_map_meg(): meg_surf="foobar", subjects_dir=subjects_dir, trans=trans_fname, + origin="auto", ) @@ -221,12 +238,15 @@ def test_make_field_map_meeg(): picks = picks[::10] evoked.pick([evoked.ch_names[p] for p in picks]) evoked.info.normalize_proj() + # set origin to "(0.0, 0.0, 0.04)", which was the default until v1.12 + # estimated origin from "auto" fails the assertions below maps = make_field_map( evoked, trans_fname, subject="sample", subjects_dir=subjects_dir, verbose="debug", + origin=(0.0, 0.0, 0.04), ) assert_equal(maps[0]["data"].shape, (642, 6)) # EEG->Head assert_equal(maps[1]["data"].shape, (304, 31)) # MEG->Helmet diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 01d6d5a960d..ab24e6a70db 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -190,6 +190,7 @@ def test_plot_evoked_field(renderer): n_jobs=None, ch_type=t, upsampling=up, + origin="auto", verbose=True, ) log = log.getvalue() diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh index 3104e84ecb9..47e8322cd2d 100755 --- a/tools/install_pre_requirements.sh +++ b/tools/install_pre_requirements.sh @@ -55,9 +55,11 @@ python -m pip install $STD_ARGS \ git+https://github.com/joblib/joblib \ git+https://github.com/h5io/h5io \ git+https://github.com/BUNPC/pysnirf2 \ - git+https://github.com/the-siesta-group/edfio \ trame trame-vtk trame-vuetify jupyter ipyevents ipympl openmeeg \ - imageio-ffmpeg xlrd mffpy traitlets pybv eeglabio defusedxml antio + imageio-ffmpeg xlrd mffpy traitlets pybv eeglabio defusedxml antio \ + edfio +# https://github.com/the-siesta-group/edfio/issues/78 +# git+https://github.com/the-siesta-group/edfio echo "::endgroup::" echo "::group::Make sure we're on a NumPy 2.0 variant" diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index 531c16bfb70..3a91c38bb3d 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -293,7 +293,11 @@ def custom_func(x): # `: maps = mne.make_field_map( - evks["aud/left"], trans=str(trans_file), subject="sample", subjects_dir=subjects_dir + evks["aud/left"], + trans=str(trans_file), + subject="sample", + subjects_dir=subjects_dir, + origin="auto", ) evks["aud/left"].plot_field(maps, time=0.1) @@ -310,6 +314,7 @@ def custom_func(x): subject="sample", subjects_dir=subjects_dir, meg_surf="head", + origin="auto", ) fig = evk.plot_field(_map, time=0.1) mne.viz.set_3d_title(fig, ch_type, size=20) diff --git a/tutorials/visualization/20_ui_events.py b/tutorials/visualization/20_ui_events.py index 1c4038276f4..879f04c8b6a 100644 --- a/tutorials/visualization/20_ui_events.py +++ b/tutorials/visualization/20_ui_events.py @@ -68,6 +68,7 @@ trans=data_path / "MEG" / "sample" / "sample_audvis_raw-trans.fif", subject="sample", subjects_dir=data_path / "subjects", + origin="auto", ) fig_field = mne.viz.plot_evoked_field( avg_evokeds,