diff --git a/.circleci/config.yml b/.circleci/config.yml index 644fd8b31b7..4297dc5fedf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -252,7 +252,7 @@ jobs: name: Check sphinx log for warnings (which are treated as errors) when: always command: | - ! grep "^.* WARNING: .*$" sphinx_log.txt + ! grep "^.* (WARNING|ERROR): .*$" sphinx_log.txt - run: name: Show profiling output when: always diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8975e72784b..0fc130bba97 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -99,24 +99,22 @@ jobs: python-version: ${{ matrix.python }} if: startswith(matrix.kind, 'pip') # Python (if conda) - - name: Remove numba and dipy - run: | # TODO: Remove when numba 0.59 and dipy 1.8 land on conda-forge - sed -i '/numba/d' environment.yml - sed -i '/dipy/d' environment.yml - sed -i 's/- mne$/- mne-base/' environment.yml - if: matrix.os == 'ubuntu-latest' && startswith(matrix.kind, 'conda') && matrix.python == '3.12' + - name: Fixes for conda + run: | + # For some reason on Linux we get crashes + if [[ "$RUNNER_OS" == "Linux" ]]; then + sed -i "/numba/d" environment.yml + elif [[ "$RUNNER_OS" == "macOS" ]]; then + sed -i "" "s/ - PySide6 .*/ - PySide6 <6.8/g" environment.yml + fi + if: matrix.kind == 'conda' || matrix.kind == 'mamba' - uses: mamba-org/setup-micromamba@v2 with: environment-file: ${{ env.CONDA_ENV }} environment-name: mne create-args: >- python=${{ env.PYTHON_VERSION }} - mamba - nomkl if: ${{ !startswith(matrix.kind, 'pip') }} - # Make sure we have the right Python - - run: python -c "import platform; assert platform.machine() == 'arm64', platform.machine()" - if: matrix.os == 'macos-14' - run: ./tools/github_actions_dependencies.sh # Minimal commands on Linux (macOS stalls) - run: ./tools/get_minimal_commands.sh diff --git a/doc/conf.py b/doc/conf.py index 7dd6ec90d4f..96028fb9045 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -666,6 +666,10 @@ def fix_sklearn_inherited_docstrings(app, what, name, obj, options, lines): r"https://scholar.google.com/scholar\?cites=12188330066413208874&as_ylo=2014", r"https://scholar.google.com/scholar\?cites=1521584321377182930&as_ylo=2013", "https://www.research.chop.edu/imaging", + "http://prdownloads.sourceforge.net/optipng/optipng-0.7.8-win64.zip?download", + "https://sourceforge.net/projects/aespa/files/", + "https://sourceforge.net/projects/ezwinports/files/", + "https://www.mathworks.com/products/compiler/matlab-runtime.html", # 500 server error "https://openwetware.org/wiki/Beauchamp:FreeSurfer", # 503 Server error @@ -688,6 +692,7 @@ def fix_sklearn_inherited_docstrings(app, what, name, obj, options, lines): # SSL problems sometimes "http://ilabs.washington.edu", "https://psychophysiology.cpmc.columbia.edu", + "https://erc.easme-web.eu", ] linkcheck_anchors = False # saves a bit of time linkcheck_timeout = 15 # some can be quite slow diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 07c28f55d3d..011fd3c11f4 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -1114,6 +1114,6 @@ it can serve as a useful example of what to expect from the PR review process. .. optipng .. _optipng: http://optipng.sourceforge.net/ -.. _optipng for Windows: http://prdownloads.sourceforge.net/optipng/optipng-0.7.7-win32.zip?download +.. _optipng for Windows: http://prdownloads.sourceforge.net/optipng/optipng-0.7.8-win64.zip?download .. include:: ../links.inc diff --git a/doc/install/installers.rst b/doc/install/installers.rst index 5b7eeba5203..533c0207963 100644 --- a/doc/install/installers.rst +++ b/doc/install/installers.rst @@ -86,7 +86,7 @@ Platform-specific installers .. We have to use a button-link here because button-ref doesn't properly nested parse the inline code - .. button-link:: ./ides.html + .. button-link:: ides.html :ref-type: ref :color: success :shadow: diff --git a/environment.yml b/environment.yml index 4dc45788af1..b25d83958c2 100644 --- a/environment.yml +++ b/environment.yml @@ -23,11 +23,13 @@ dependencies: - joblib - jupyter - lazy_loader >=0.3 + - mamba - matplotlib >=3.7 - mffpy >=0.5.7 - mne-qt-browser - nibabel - nilearn + - nomkl - numba - numpy >=1.25,<3 - openmeeg >=2.5.5 diff --git a/mne/conftest.py b/mne/conftest.py index fc3bc3b7a53..6eea624467a 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -183,6 +183,10 @@ def pytest_configure(config): ignore:The (non_)?interactive_bk attribute was deprecated.*: # SWIG (via OpenMEEG) ignore:.*builtin type swigvarlink has no.*:DeprecationWarning + # eeglabio + ignore:numpy\.core\.records is deprecated.*:DeprecationWarning + # joblib + ignore:process .* is multi-threaded, use of fork/exec.*:DeprecationWarning """ # noqa: E501 for warning_line in warning_lines.split("\n"): warning_line = warning_line.strip() diff --git a/mne/datasets/sleep_physionet/_utils.py b/mne/datasets/sleep_physionet/_utils.py index b97d0611591..7fbcca3a2d7 100644 --- a/mne/datasets/sleep_physionet/_utils.py +++ b/mne/datasets/sleep_physionet/_utils.py @@ -2,6 +2,7 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import inspect import os import os.path as op @@ -114,12 +115,16 @@ def _update_sleep_temazepam_records(fname=TEMAZEPAM_SLEEP_RECORDS): data = data.set_index(("Subject - age - sex", "Nr")) data.index.name = "subject" data.columns.names = [None, None] + kwargs = dict() + # TODO VERSION can be removed once we require Pandas 2.1 + if "future_stack" in inspect.getfullargspec(pd.DataFrame.stack).args: + kwargs["future_stack"] = True data = ( data.set_index( [("Subject - age - sex", "Age"), ("Subject - age - sex", "M1/F2")], append=True, ) - .stack(level=0) + .stack(level=0, **kwargs) .reset_index() ) diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 85ed102b514..a291416bb17 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -85,7 +85,11 @@ def __sklearn_tags__(self): """Get sklearn tags.""" from sklearn.utils import get_tags # added in 1.6 - return get_tags(self.model) + # fit method below does not allow sparse data via check_data, we could + # eventually make it smarter if we had to + tags = get_tags(self.model) + tags.input_tags.sparse = False + return tags def __getattr__(self, attr): """Wrap to model for some attributes.""" diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 8eb2dcc5510..e475cd22161 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -238,7 +238,7 @@ def inverse_transform(self, epochs_data): return out -class Vectorizer(TransformerMixin): +class Vectorizer(TransformerMixin, BaseEstimator): """Transform n-dimensional array into 2D array of n_samples by n_features. This class reshapes an n-dimensional array into an n_samples * n_features @@ -343,7 +343,7 @@ def inverse_transform(self, X): @fill_doc -class PSDEstimator(TransformerMixin): +class PSDEstimator(TransformerMixin, BaseEstimator): """Compute power spectral density (PSD) using a multi-taper method. Parameters @@ -452,7 +452,7 @@ def transform(self, epochs_data): @fill_doc -class FilterEstimator(TransformerMixin): +class FilterEstimator(TransformerMixin, BaseEstimator): """Estimator to filter RtEpochs. Applies a zero-phase low-pass, high-pass, band-pass, or band-stop @@ -743,7 +743,7 @@ def _apply_method(self, X, method): @fill_doc -class TemporalFilter(TransformerMixin): +class TemporalFilter(TransformerMixin, BaseEstimator): """Estimator to filter data array along the last dimension. Applies a zero-phase low-pass, high-pass, band-pass, or band-stop diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 706a83476e4..ca0853837fc 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -145,7 +145,7 @@ def _create_raw_for_edf_tests(stim_channel_index=None): edfio_mark = pytest.mark.skipif( - not _check_edfio_installed(strict=False), reason="unsafe use of private module" + not _check_edfio_installed(strict=False), reason="requires edfio" ) @@ -476,7 +476,7 @@ def test_export_epochs_eeglab(tmp_path, preload): with ctx(): epochs.export(temp_fname) epochs.drop_channels([ch for ch in ["epoc", "STI 014"] if ch in epochs.ch_names]) - epochs_read = read_epochs_eeglab(temp_fname) + epochs_read = read_epochs_eeglab(temp_fname, verbose="error") # head radius assert epochs.ch_names == epochs_read.ch_names cart_coords = np.array([d["loc"][:3] for d in epochs.info["chs"]]) # just xyz cart_coords_read = np.array([d["loc"][:3] for d in epochs_read.info["chs"]]) diff --git a/mne/fixes.py b/mne/fixes.py index 2aed20492ec..070d4125d18 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -720,3 +720,16 @@ def minimum_phase(h, method="homomorphic", n_fft=None, *, half=True): n_out = (n_half + len(h) % 2) if half else len(h) return h_minimum[:n_out] + + +# SciPy 1.15 deprecates sph_harm for sph_harm_y and using it will trigger a +# DeprecationWarning. This is a backport of the new function for older SciPy versions. +def sph_harm_y(n, m, theta, phi, *, diff_n=0): + """Wrap scipy.special.sph_harm for sph_harm_y.""" + # Can be removed once we no longer support scipy < 1.15.0 + from scipy import special + + if "sph_harm_y" in special.__dict__: + return special.sph_harm_y(n, m, theta, phi, diff_n=diff_n) + else: + return special.sph_harm(m, n, phi, theta) diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index e1fb548caa5..789c8520f05 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -10,7 +10,7 @@ import numpy as np from scipy import linalg -from scipy.special import lpmv, sph_harm +from scipy.special import lpmv from .. import __version__ from .._fiff.compensator import make_compensator @@ -24,7 +24,7 @@ from ..annotations import _annotations_starts_stops from ..bem import _check_origin from ..channels.channels import _get_T1T2_mag_inds, fix_mag_coil_types -from ..fixes import _safe_svd, bincount +from ..fixes import _safe_svd, bincount, sph_harm_y from ..forward import _concatenate_coils, _create_meg_coils, _prep_meg_channels from ..io import BaseRaw, RawArray from ..surface import _normalize_vectors @@ -436,7 +436,7 @@ def _prep_maxwell_filter( # we purposefully stay away from shorthand notation in both and use # explicit terms (like 'azimuth' and 'polar') to avoid confusion. # See mathworld.wolfram.com/SphericalHarmonic.html for more discussion. - # Our code follows the same standard that ``scipy`` uses for ``sph_harm``. + # Our code follows the same standard that ``scipy`` uses for ``sph_harm_y``. # triage inputs ASAP to avoid late-thrown errors _validate_type(raw, BaseRaw, "raw") @@ -1487,7 +1487,7 @@ def _sss_basis_basic(exp, coils, mag_scale=100.0, method="standard"): S_in_out = list() grads_in_out = list() # Same spherical harmonic is used for both internal and external - sph = sph_harm(order, degree, az, pol) + sph = sph_harm_y(degree, order, pol, az) sph_norm = _sph_harm_norm(order, degree) # Compute complex gradient for all integration points # in spherical coordinates (Eq. 6). The gradient for rad, az, pol diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index 102900bb1fc..f5e816258f8 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -11,7 +11,6 @@ import pytest from numpy.testing import assert_allclose, assert_array_equal from scipy import sparse -from scipy.special import sph_harm import mne from mne import compute_raw_covariance, concatenate_raws, pick_info, pick_types @@ -19,6 +18,7 @@ from mne.annotations import _annotations_starts_stops from mne.chpi import filter_chpi, read_head_pos from mne.datasets import testing +from mne.fixes import sph_harm_y from mne.forward import _prep_meg_channels, use_coil_def from mne.io import ( BaseRaw, @@ -431,9 +431,9 @@ def test_spherical_conversions(): az, pol = np.meshgrid(np.linspace(0, 2 * np.pi, 30), np.linspace(0, np.pi, 20)) for degree in range(1, int_order): for order in range(0, degree + 1): - sph = sph_harm(order, degree, az, pol) + sph = sph_harm_y(degree, order, pol, az) # ensure that we satisfy the conjugation property - assert_allclose(_sh_negate(sph, order), sph_harm(-order, degree, az, pol)) + assert_allclose(_sh_negate(sph, order), sph_harm_y(degree, -order, pol, az)) # ensure our conversion functions work sph_real_pos = _sh_complex_to_real(sph, order) sph_real_neg = _sh_complex_to_real(sph, -order) diff --git a/mne/transforms.py b/mne/transforms.py index c85c31964b6..7072ea25124 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -12,14 +12,13 @@ import numpy as np from scipy import linalg from scipy.spatial.distance import cdist -from scipy.special import sph_harm from ._fiff.constants import FIFF from ._fiff.open import fiff_open from ._fiff.tag import read_tag from ._fiff.write import start_and_end_file, write_coord_trans from .defaults import _handle_default -from .fixes import _get_img_fdata, jit +from .fixes import _get_img_fdata, jit, sph_harm_y from .utils import ( _check_fname, _check_option, @@ -926,7 +925,7 @@ def _compute_sph_harm(order, az, pol): # _deg_ord_idx(0, 0) = -1 so we're actually okay to use it here for degree in range(order + 1): for order_ in range(degree + 1): - sph = sph_harm(order_, degree, az, pol) + sph = sph_harm_y(degree, order_, pol, az) out[:, _deg_ord_idx(degree, order_)] = _sh_complex_to_real(sph, order_) if order_ > 0: out[:, _deg_ord_idx(degree, -order_)] = _sh_complex_to_real( diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 149f5a194da..cebd2caefa7 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -23,7 +23,7 @@ if [ ! -z "$CONDA_ENV" ]; then elif [[ "${MNE_CI_KIND}" == "pip" ]]; then # Only used for 3.13 at the moment, just get test deps plus a few extras # that we know are available - INSTALL_ARGS="nibabel scikit-learn numpydoc PySide6 mne-qt-browser" + INSTALL_ARGS="nibabel scikit-learn numpydoc PySide6 mne-qt-browser pandas h5io mffpy defusedxml" INSTALL_KIND="test" else test "${MNE_CI_KIND}" == "pip-pre" diff --git a/tools/hooks/update_environment_file.py b/tools/hooks/update_environment_file.py index 8cac6193959..bc36567147a 100755 --- a/tools/hooks/update_environment_file.py +++ b/tools/hooks/update_environment_file.py @@ -4,14 +4,14 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -import difflib import re from pathlib import Path import tomllib repo_root = Path(__file__).resolve().parents[2] -pyproj = tomllib.loads((repo_root / "pyproject.toml").read_text("utf-8")) +with open(repo_root / "pyproject.toml", "rb") as fid: + pyproj = tomllib.load(fid) # Get our "full" dependences from `pyproject.toml`, but actually ignore the # "full" section as it's just "full-noqt" plus PyQt6, and for conda we need PySide @@ -22,7 +22,7 @@ deps |= set(section_deps) recursive_deps = set(d for d in deps if d.startswith("mne[")) deps -= recursive_deps -deps |= {"pip"} +deps |= {"pip", "mamba", "nomkl"} def remove_spaces(version_spec): @@ -48,11 +48,6 @@ def split_dep(dep): translations = dict(neo="python-neo") pip_deps = set() conda_deps = set() -check_old = ( - "numpy scipy matplotlib pandas scikit-learn nibabel tqdm pooch decorator " - "packaging jinja2 lazy_loader" -).split() -old_deps = [None] * len(check_old) for dep in deps: package_name, version_spec = split_dep(dep) # handle package name differences @@ -68,12 +63,6 @@ def split_dep(dep): pip_deps.add(f" {line}") else: conda_deps.add(line) - # old deps - if package_name in check_old: - # Pull out >= part, change to =, remove < (which should be after comma) - old_deps[check_old.index(package_name)] = line.replace(">=", "=").split(",")[0] -for di, dep in enumerate(old_deps): - assert dep is not None, f"Missing {check_old[di]}" # TODO: temporary workaround while we wait for a release containing the fix for # https://github.com/mamba-org/mamba/issues/3467 @@ -87,33 +76,14 @@ def split_dep(dep): """ pip_section = pip_section if len(pip_deps) else "" # prepare the env file -header = f"""\ +env = f"""\ # THIS FILE IS AUTO-GENERATED BY {'/'.join(Path(__file__).parts[-3:])} AND WILL BE OVERWRITTEN name: mne channels: - conda-forge -dependencies:""" # noqa: E501 -env = f"""{header} +dependencies: - python {req_python} {newline.join(sorted(conda_deps, key=str.casefold))} -{pip_section}""" - -env_file = repo_root / "environment.yml" -old_env = env_file.read_text("utf-8") -if old_env != env: - diff = "\n".join(difflib.unified_diff(old_env.splitlines(), env.splitlines())) - print(f"Updating {env_file} with diff:\n{diff}") - env_file.write_text(env, encoding="utf-8") - -# Now we also updated tools/environment_old.yml -env_file = repo_root / "tools" / "environment_old.yml" -old_env = env_file.read_text("utf-8") -use_python = req_python.replace(">=", "=") -env = f"""{header} - - python {use_python} -{newline.join(old_deps)} -""" -if old_env != env: - diff = "\n".join(difflib.unified_diff(old_env.splitlines(), env.splitlines())) - print(f"Updating {env_file} with diff:\n{diff}") - env_file.write_text(env, encoding="utf-8") +{pip_section}""" # noqa: E501 + +(repo_root / "environment.yml").write_text(env) diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh index 74868b0a435..113f122b399 100755 --- a/tools/install_pre_requirements.sh +++ b/tools/install_pre_requirements.sh @@ -18,10 +18,11 @@ python -m pip install $STD_ARGS pip setuptools packaging \ py-cpuinfo blosc2 hatchling echo "NumPy/SciPy/pandas etc." python -m pip uninstall -yq numpy +python -m pip install --upgrade matplotlib # TODO: Until https://github.com/matplotlib/matplotlib/pull/29427 lands python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 \ --index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" \ "numpy>=2.1.0.dev0" "scikit-learn>=1.6.dev0" "scipy>=1.15.0.dev0" \ - "pandas>=3.0.0.dev0" "matplotlib>=3.10.0.dev0" \ + "pandas>=3.0.0.dev0" \ "h5py>=3.12.1" "dipy>=1.10.0.dev0" "pyarrow>=19.0.0.dev0" "tables>=3.10.2.dev0" # statsmodels requires formulaic@main so we need to use --extra-index-url @@ -48,7 +49,7 @@ python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https:/ python -c "import vtk" echo "PyVista" -python -m pip install $STD_ARGS "git+https://github.com/pyvista/pyvista" +python -m pip install $STD_ARGS "git+https://github.com/pyvista/pyvista" trame trame-vtk trame-vuetify echo "picard" python -m pip install $STD_ARGS git+https://github.com/pierreablin/picard @@ -57,7 +58,7 @@ echo "pyvistaqt" pip install $STD_ARGS git+https://github.com/pyvista/pyvistaqt echo "imageio-ffmpeg, xlrd, mffpy" -pip install $STD_ARGS imageio-ffmpeg xlrd mffpy traitlets pybv eeglabio +pip install $STD_ARGS imageio-ffmpeg xlrd mffpy traitlets pybv eeglabio defusedxml echo "mne-qt-browser" pip install $STD_ARGS git+https://github.com/mne-tools/mne-qt-browser