diff --git a/.circleci/config.yml b/.circleci/config.yml index 97a4a5910cd..d47862ae880 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,6 +21,9 @@ _check_skip: &check_skip circleci-agent step halt; fi +_machine_image: &machine_image + image: ubuntu-2604:2026.05.1 + jobs: build_docs: parameters: @@ -28,7 +31,7 @@ jobs: type: string default: "false" machine: - image: ubuntu-2404:current + <<: *machine_image # large 4 vCPUs 15GB mem # https://discuss.circleci.com/t/changes-to-remote-docker-reporting-pricing/47759 resource_class: large @@ -110,7 +113,7 @@ jobs: # Load pip cache - restore_cache: keys: - - pip-cache-0 + - pip-cache-1 - restore_cache: keys: - user-install-bin-cache-310 @@ -122,7 +125,7 @@ jobs: ./tools/circleci_dependencies.sh - save_cache: - key: pip-cache-0 + key: pip-cache-1 paths: - ~/.cache/pip - save_cache: @@ -419,7 +422,7 @@ jobs: type: string default: "false" machine: - image: ubuntu-2404:current + <<: *machine_image resource_class: large steps: - restore_cache: @@ -439,7 +442,7 @@ jobs: command: ./tools/circleci_bash_env.sh - restore_cache: keys: - - pip-cache-0 + - pip-cache-1 - run: name: Get Python running command: | @@ -460,7 +463,7 @@ jobs: deploy: machine: - image: ubuntu-2404:current + <<: *machine_image steps: - attach_workspace: at: /tmp/build diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 4e44f7f8c5f..ccf40e66f25 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -16,8 +16,6 @@ jobs: with: persist-credentials: false - uses: actions/setup-python@v6 - with: - python-version: '3.12' - run: pip install --upgrade towncrier pygithub gitpython numpy - run: python ./.github/actions/rename_towncrier/rename_towncrier.py - run: python ./tools/dev/ensure_headers.py diff --git a/.github/workflows/credit.yml b/.github/workflows/credit.yml index 8d28e076799..d83eaf4eb7e 100644 --- a/.github/workflows/credit.yml +++ b/.github/workflows/credit.yml @@ -21,8 +21,6 @@ jobs: with: persist-credentials: true - uses: actions/setup-python@v6 - with: - python-version: '3.12' - run: pip install pygithub -e . - run: python tools/dev/update_credit_json.py - run: git add -f doc/sphinxext/prs/*.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d3d0bc4c86..9222601905b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,13 +19,11 @@ jobs: - uses: actions/checkout@v6.0.3 with: persist-credentials: false + fetch-depth: 0 - uses: actions/setup-python@v6 - with: - python-version: '3.10' - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install build twine + python -m pip install --upgrade pip packaging build twine - run: python -m build --sdist --wheel - run: twine check --strict dist/* - uses: actions/upload-artifact@v7 diff --git a/.github/workflows/spec_zero.yml b/.github/workflows/spec_zero.yml index e63b3d4d55d..7673474f30a 100644 --- a/.github/workflows/spec_zero.yml +++ b/.github/workflows/spec_zero.yml @@ -45,7 +45,7 @@ jobs: with: version: ">=0.9" activate-environment: true - python-version: 3.12 + python-version: "3.12" - name: Install dependencies run: uv pip install -e . packaging requests tomlkit - name: Update tracked dependencies diff --git a/doc/_static/style.css b/doc/_static/style.css index bb93e819233..59f1ddf3864 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -401,27 +401,27 @@ p.centered a img.instlogo { } /* ************************************************************ funders page */ -ul.current li { +ul.currentsponsor li { margin-left: calc(var(--mne-sponsor-logo-lg) + 0.5rem); text-indent: calc(-1 * var(--mne-sponsor-logo-lg) - 0.5rem); padding-bottom: 0.75rem; } -ul.former li { +ul.formersponsor li { margin-left: calc(var(--mne-sponsor-logo-sm) + 1rem); text-indent: calc(-1 * var(--mne-sponsor-logo-sm) - 1rem); padding-bottom: 0.5rem; } -ul.current li img, -ul.former li img { +ul.currentsponsor li img, +ul.formersponsor li img { background-color: unset !important; box-sizing: content-box; object-fit: contain; } -ul.current li img { +ul.currentsponsor li img { max-height: calc(0.4 * var(--mne-sponsor-logo-lg)); width: var(--mne-sponsor-logo-lg); } -ul.former li img { +ul.formersponsor li img { max-height: calc(0.5 * var(--mne-sponsor-logo-sm)); width: var(--mne-sponsor-logo-sm); } diff --git a/doc/_templates/homepage.html b/doc/_templates/homepage.html index b559f069eaf..d3eb451d557 100644 --- a/doc/_templates/homepage.html +++ b/doc/_templates/homepage.html @@ -23,7 +23,7 @@ {% for item in current_sponsors -%}
- + {{ item.title }}
@@ -31,7 +31,7 @@ {% for inst in current_institutions -%}
- + {{ inst.name }}
diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 667671b148e..6a08df3c88d 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -50,7 +50,7 @@ .. _btkcodedev: https://github.com/btkcodedev .. _buildqa: https://github.com/buildqa .. _Carina Forster: https://github.com/CarinaFo -.. _Carlos de la Torre-Ortiz: https://ctorre.me +.. _Carlos de la Torre-Ortiz: https://github.com/c-torre .. _Cathy Nangini: https://github.com/KatiRG .. _Chetan Gohil: https://github.com/cgohil8 .. _Chris Bailey: https://github.com/cjayb @@ -239,7 +239,7 @@ .. _Mohammad Daneshzand: https://github.com/mdaneshzand .. _Mojackhak: https://github.com/Mojackhak .. _Moritz Gerster: https://github.com/moritz-gerster -.. _Motofumi Fushimi: https://github.com/motofumi-fushimi/motofumi-fushimi.github.io +.. _Motofumi Fushimi: https://github.com/motofumi-fushimi .. _Nabil Alibou: https://github.com/nabilalibou .. _Natalie Klein: https://github.com/natalieklein .. _Nathalie Gayraud: https://github.com/ngayraud diff --git a/doc/conf.py b/doc/conf.py index f14026d21c6..6a48d9f1058 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -657,6 +657,7 @@ def fix_sklearn_inherited_docstrings(app, what, name, obj, options, lines): "https://doi.org/10.1126/", # www.science.org "https://doi.org/10.1137/", # epubs.siam.org "https://doi.org/10.1145/", # dl.acm.org + "https://doi.org/10.5281/", # zenodo.org "https://doi.org/10.1155/", # www.hindawi.com/journals/cin "https://doi.org/10.1161/", # www.ahajournals.org "https://doi.org/10.1162/", # direct.mit.edu/neco/article/ @@ -667,6 +668,8 @@ def fix_sklearn_inherited_docstrings(app, what, name, obj, options, lines): "https://doi.org/10.3390/", # mdpi.com "https://hms.harvard.edu/", # doc/funding.rst "https://stackoverflow.com/questions/21752259/python-why-pickle", # doc/help/faq + "https://mitpress.mit.edu/9780262525855", # works but linkcheck fails to resolve + "https://zenodo.org", # doc/help/faq "https://blender.org", "https://home.alexk101.dev", "https://www.mq.edu.au/", @@ -711,8 +714,12 @@ def fix_sklearn_inherited_docstrings(app, what, name, obj, options, lines): "https://psychophysiology.cpmc.columbia.edu", "https://erc.easme-web.eu", "https://www.crnl.fr", + # Spurious failure + "https://megcore.nih.gov/index.php/Staff", # Not rendered by linkcheck builder r"ides\.html", + # Sponsors not rendered properly by linkcheck builder + "{{inst.url}}", ] linkcheck_anchors = False # saves a bit of time linkcheck_timeout = 15 # some can be quite slow diff --git a/doc/credits/sponsors.rst b/doc/credits/sponsors.rst index 005eefb01bf..0ad5d88ddd8 100644 --- a/doc/credits/sponsors.rst +++ b/doc/credits/sponsors.rst @@ -7,7 +7,7 @@ Sponsors Maintenance and development of MNE-Python is currently supported by the following funding agencies and partners: -.. rst-class:: list-unstyled current +.. rst-class:: list-unstyled currentsponsor - |nih| **National Institutes of Health:** `R01-NS104585 `_ @@ -18,7 +18,7 @@ Maintenance and development of MNE-Python is currently supported by the followin Past sponsors ------------- -.. rst-class:: list-unstyled former +.. rst-class:: list-unstyled formersponsor - |nih| National Institutes of Health: `R01-EB009048 `_, @@ -56,7 +56,7 @@ Supporting institutions Some institutions support their employees’ contributions to MNE-Python as part of normal work duties. Current supporting institutions include: -.. rst-class:: list-unstyled current +.. rst-class:: list-unstyled currentsponsor {% for inst in current_institutions %} {% if not inst.name.endswith("_dk") %} diff --git a/doc/development/roadmap.rst b/doc/development/roadmap.rst index a608062a135..8d09cc03a8b 100644 --- a/doc/development/roadmap.rst +++ b/doc/development/roadmap.rst @@ -273,7 +273,7 @@ Time-frequency visualization ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We implemented a viewer for interactive visualization of volumetric source-time-frequency (5-D) maps on MRI slices (orthogonal 2D viewer). -`NutmegTrip `__ +`NutmegTrip `__ (written by Sarang Dalal) provides similar functionality in MATLAB in conjunction with FieldTrip. Example of NutmegTrip's source-time-frequency mode in action (click for link to YouTube): diff --git a/doc/links.inc b/doc/links.inc index 95fab1b1bf5..3f8caf28081 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -20,7 +20,7 @@ .. _`MNE-HCP`: http://mne.tools/mne-hcp .. _`MNE-Realtime`: https://mne.tools/mne-realtime .. _`MNE-LSL`: https://mne.tools/mne-lsl -.. _`MNE-RT`: https://payamsash.github.io/mne-rt/ +.. _`MNE-RT`: https://mne-rt-org.github.io/mne-rt/ .. _`MNE-gui-addons`: https://mne.tools/mne-gui-addons .. _`MNE-MATLAB`: https://github.com/mne-tools/mne-matlab .. _`MNE-Docker`: https://github.com/mne-tools/mne-docker diff --git a/doc/references.bib b/doc/references.bib index c7eefd9e808..825dbd96f6c 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -2375,7 +2375,7 @@ @book{Luck2014 edition = {2nd}, publisher = {The MIT Press}, address = {Cambridge, MA}, - url = {https://mitpress.mit.edu/books/introduction-event-related-potential-technique-second-edition}, + url = {https://mitpress.mit.edu/9780262525855/an-introduction-to-the-event-related-potential-technique}, isbn = {978-0-262-52585-5} } diff --git a/doc/sphinxext/mne_doc_utils.py b/doc/sphinxext/mne_doc_utils.py index 090a80cbe20..8e7415b3375 100644 --- a/doc/sphinxext/mne_doc_utils.py +++ b/doc/sphinxext/mne_doc_utils.py @@ -4,6 +4,7 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import functools import gc import os import time @@ -17,9 +18,7 @@ import mne from mne.utils import ( - _assert_no_instances as _assert_no_instances_mne, -) -from mne.utils import ( + _assert_no_instances, _get_extra_data_path, sizeof_fmt, ) @@ -123,14 +122,20 @@ def reset_warnings(gallery_conf, fname): t0 = time.time() -def _assert_no_instances(cls, when): +def _reraise_extension_error(func): """Wrap our internal one but make the traceback nicer when it fails.""" - try: - _assert_no_instances_mne(cls, when) - except Exception as exc: - raise ExtensionError(str(exc)) from None + + @functools.wraps(func) + def _wrapper(gallery_conf, fname, when): + try: + return func(gallery_conf, fname, when) + except Exception: + raise ExtensionError(f"{func.__name__} failed: {when=}, {fname=}") + + return _wrapper +@_reraise_extension_error def reset_modules(gallery_conf, fname, when): """Do the reset.""" import matplotlib.pyplot as plt diff --git a/examples/visualization/eyetracking_plot_heatmap.py b/examples/visualization/eyetracking_plot_heatmap.py index 07983685b5e..8d656708a12 100644 --- a/examples/visualization/eyetracking_plot_heatmap.py +++ b/examples/visualization/eyetracking_plot_heatmap.py @@ -82,8 +82,10 @@ # start at a value greater than the darkest value in our previous heatmap, which will # make the darkest colors of the heatmap transparent. -cmap.set_under("k", alpha=0) # make the lowest values transparent -ax = plt.subplot() +cmap = cmap.with_extremes( + under=(0.0, 0.0, 0.0, 0.0) +) # make the lowest values transparent +_, ax = plt.subplots(figsize=(6, 3.5), layout="constrained") ax.imshow(plt.imread(stim_fpath)) plot_gaze( epochs["natural"], diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 3363c398f35..64cf81d1c9d 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -18,6 +18,7 @@ import re import weakref from dataclasses import dataclass +from functools import partial from matplotlib.colors import Colormap @@ -230,6 +231,22 @@ class ChannelsSelect(UIEvent): ch_names: list[str] +def _delete_event_channel(event=None, *, weakfig): + """Delete the event channel (callback function).""" + fig = weakfig() + if fig is None: + return + publish(fig, event=FigureClosing()) # Notify subscribers of imminent close + logger.debug(f"unlink(({fig})") + unlink(fig) # Remove channel from the _event_channel_links dict + if fig in _event_channels: + logger.debug(f" del _event_channels[{fig}]") + del _event_channels[fig] + if fig in _disabled_event_channels: + logger.debug(f" _disabled_event_channels.remove({fig})") + _disabled_event_channels.remove(fig) + + def _get_event_channel(fig): """Get the event channel associated with a figure. @@ -263,30 +280,17 @@ def _get_event_channel(fig): # When the figure is closed, its associated event channel should be # deleted. This is a good time to set this up. - def delete_event_channel(event=None, *, weakfig=weakfig): - """Delete the event channel (callback function).""" - fig = weakfig() - if fig is None: - return - publish(fig, event=FigureClosing()) # Notify subscribers of imminent close - logger.debug(f"unlink(({fig})") - unlink(fig) # Remove channel from the _event_channel_links dict - if fig in _event_channels: - logger.debug(f" del _event_channels[{fig}]") - del _event_channels[fig] - if fig in _disabled_event_channels: - logger.debug(f" _disabled_event_channels.remove({fig})") - _disabled_event_channels.remove(fig) # Hook up the above callback function to the close event of the figure # window. How this is done exactly depends on the various figure types # MNE-Python has. _validate_type(fig, (matplotlib.figure.Figure, Brain, EvokedField), "fig") + this_delete_event_channel = partial(_delete_event_channel, weakfig=weakfig) if isinstance(fig, matplotlib.figure.Figure): - fig.canvas.mpl_connect("close_event", delete_event_channel) + fig.canvas.mpl_connect("close_event", this_delete_event_channel) else: assert hasattr(fig, "_renderer") # figures like Brain, EvokedField, etc. - fig._renderer._window_close_connect(delete_event_channel, after=False) + fig._renderer._window_close_connect(this_delete_event_channel, after=False) # Now the event channel exists for sure. return _event_channels[fig] @@ -491,6 +495,5 @@ def _cleanup_agg(): if isinstance(key, matplotlib.figure.Figure): for cb in key.canvas.callbacks.callbacks["close_event"].values(): cb = cb() # get the true ref - # Our __name__ is "_get_event_channel..delete_event_channel" - if cb is not None and cb.__name__.endswith("delete_event_channel"): + if isinstance(cb, partial) and cb.func is _delete_event_channel: cb() diff --git a/pyproject.toml b/pyproject.toml index 956074974f8..83a7b7f043a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ doc = [ "numpydoc >= 0.5", "openneuro-py >= 2020.1", "psutil", - "pydata_sphinx_theme >= 0.15.2", + "pydata-sphinx-theme >= 0.15.2", "pygments >= 2.13", "pymef", "pyvistaqt >= 0.11", # released 2023-06-30, no newer version available diff --git a/tools/circleci_bash_env.sh b/tools/circleci_bash_env.sh index 91df8a4f1b7..91056558e39 100755 --- a/tools/circleci_bash_env.sh +++ b/tools/circleci_bash_env.sh @@ -4,10 +4,10 @@ set -e set -o pipefail ./tools/setup_xvfb.sh -sudo apt install -qq graphviz optipng python3.12-venv python3-venv libxft2 ffmpeg r-base libtirpc-dev +sudo apt install -qq graphviz optipng python3-venv libxft2 ffmpeg r-base libtirpc-dev libgvplugin-neato-layout8 wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb sudo apt install ./google-chrome-stable_current_amd64.deb -python3.12 -m venv ~/python_env +python -m venv ~/python_env echo "set -e" >> $BASH_ENV echo "set -o pipefail" >> $BASH_ENV echo "export XDG_RUNTIME_DIR=/tmp/runtime-circleci" >> $BASH_ENV diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh index 0d9d523ec29..894f5e7114e 100755 --- a/tools/install_pre_requirements.sh +++ b/tools/install_pre_requirements.sh @@ -55,12 +55,11 @@ python -m pip install $STD_ARGS \ git+https://github.com/pyqtgraph/pyqtgraph \ "mne-qt-browser @ https://github.com/mne-tools/mne-qt-browser/archive/refs/heads/main.zip" \ "mne-bids @ https://github.com/mne-tools/mne-bids/archive/refs/heads/main.zip" \ - git+https://github.com/nipy/nibabel \ + "nibabel @ https://github.com/nipy/nibabel/archive/refs/heads/master.zip" \ 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 \ - git+https://github.com/python-quantities/python-quantities \ trame trame-vtk trame-vuetify trame-pyvista nest-asyncio2 jupyter ipyevents ipympl \ openmeeg imageio-ffmpeg xlrd mffpy traitlets pybv eeglabio defusedxml antio curryreader echo "::endgroup::" diff --git a/tools/setup_xvfb.sh b/tools/setup_xvfb.sh index d22f8e2b7ac..61fab33f1c3 100755 --- a/tools/setup_xvfb.sh +++ b/tools/setup_xvfb.sh @@ -11,5 +11,11 @@ done # This also includes the libraries necessary for PyQt5/PyQt6 sudo apt update -sudo apt install -yqq xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libopengl0 libegl1 libosmesa6 mesa-utils libxcb-shape0 libxcb-cursor0 libxml2 +# Need two different libxml2 variants for 24.04 and 26.04 +if [[ $(lsb_release -rs) == "26.04" ]]; then + XML_DEP=libxml2-16 +else + XML_DEP=libxml2 +fi +sudo apt install -yqq xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libopengl0 libegl1 libosmesa6 mesa-utils libxcb-shape0 libxcb-cursor0 $XML_DEP /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render -noreset diff --git a/tutorials/epochs/20_visualize_epochs.py b/tutorials/epochs/20_visualize_epochs.py index 04eca026b7e..596046f2710 100644 --- a/tutorials/epochs/20_visualize_epochs.py +++ b/tutorials/epochs/20_visualize_epochs.py @@ -25,12 +25,11 @@ raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=120) # %% -# To create the `~mne.Epochs` data structure, we'll extract the event -# IDs stored in the :term:`stim channel`, map those integer event IDs to more -# descriptive condition labels using an event dictionary, and pass those to the -# `~mne.Epochs` constructor, along with the `~mne.io.Raw` data and the -# desired temporal limits of our epochs, ``tmin`` and ``tmax`` (for a -# detailed explanation of these steps, see :ref:`tut-epochs-class`). +# To create the `~mne.Epochs` data structure, we'll extract the event IDs stored in the +# :term:`stim channel`, map those integer event IDs to more descriptive condition labels +# using an event dictionary, and pass those to the `~mne.Epochs` constructor, along with +# the `~mne.io.Raw` data and the desired temporal limits of our epochs, ``tmin`` and +# ``tmax`` (for a detailed explanation of these steps, see :ref:`tut-epochs-class`). events = mne.find_events(raw, stim_channel="STI 014") event_dict = { diff --git a/tutorials/epochs/60_make_fixed_length_epochs.py b/tutorials/epochs/60_make_fixed_length_epochs.py index 10b8c12ea19..f13d49a0657 100644 --- a/tutorials/epochs/60_make_fixed_length_epochs.py +++ b/tutorials/epochs/60_make_fixed_length_epochs.py @@ -16,15 +16,6 @@ epochs by segmenting the data around the onset of each stimulus, we will create 30 second epochs that allow us to perform non-event-related analyses of the signal. - -.. note:: - Starting in version 1.0, all functions in the ``mne.connectivity`` - sub-module are housed in a separate package called - :mod:`mne-connectivity `. Download it by running: - - .. code-block:: console - - $ pip install mne-connectivity """ # Authors: The MNE-Python contributors. diff --git a/tutorials/evoked/10_evoked_overview.py b/tutorials/evoked/10_evoked_overview.py index b251a1f8239..4e639dea7b8 100644 --- a/tutorials/evoked/10_evoked_overview.py +++ b/tutorials/evoked/10_evoked_overview.py @@ -58,9 +58,8 @@ # :class:`~mne.Epochs` object, but may also be initiated (or disabled) # manually. We will discuss this in more detail later. # -# The information about the baseline period of :class:`~mne.Epochs` is -# transferred to derived :class:`~mne.Evoked` objects to maintain provenance as -# you process your data: +# The information about the baseline period of :class:`~mne.Epochs` is transferred to +# derived :class:`~mne.Evoked` objects to maintain provenance as you process your data: print(f"Epochs baseline: {epochs.baseline}") print(f"Evoked baseline: {evoked.baseline}") diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index 3a91c38bb3d..eb172a4704b 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -5,8 +5,7 @@ Visualizing Evoked data ======================= -This tutorial shows the different visualization methods for -:class:`~mne.Evoked` objects. +This tutorial shows different visualization methods for :class:`~mne.Evoked` objects. As usual we'll start by importing the modules we need: """