Skip to content

Commit

Permalink
Merge branch 'main' into correct_annot_export
Browse files Browse the repository at this point in the history
  • Loading branch information
qian-chu authored Dec 16, 2024
2 parents ab728d3 + 521d667 commit e07de28
Show file tree
Hide file tree
Showing 26 changed files with 242 additions and 60 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Bot auto-merge
on: pull_request # yamllint disable-line rule:truthy

jobs:
autobot:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
# Names can be found with gh api /repos/mne-tools/mne-python/pulls/12998 -q .user.login for example
if: (github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'pre-commit-ci[bot]' || github.event.pull_request.user.login == 'github-actions[bot]') && github.repository == 'mne-tools/mne-python'
steps:
- name: Enable auto-merge for bot PRs
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python: '3.13'
kind: pip
- os: ubuntu-latest
python: '3.12'
kind: pip-pre
Expand Down
3 changes: 2 additions & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ stages:
python -m pip install --progress-bar off --upgrade pip
python -m pip install --progress-bar off --upgrade --pre --only-binary=\"numpy,scipy,matplotlib,vtk\" numpy scipy matplotlib vtk
python -c "import vtk"
python -m pip install --progress-bar off --upgrade -ve .[full,test_extra]
# Bug on 2.5.13 https://github.com/openmeeg/openmeeg/issues/700
python -m pip install --progress-bar off --upgrade -ve .[full,test_extra] "openmeeg==2.5.12"
displayName: 'Install dependencies with pip'
- bash: |
set -e
Expand Down
1 change: 1 addition & 0 deletions doc/changes/devel/13012.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix errant detection of software-rendered vs hardware-rendered MESA GL contexts in 3D rendering on Linux, by `Eric Larson`_.
1 change: 1 addition & 0 deletions doc/changes/devel/13018.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new :meth:`Raw.rescale <mne.io.Raw.rescale>` method to rescale the data in place, by `Clemens Brunner`_.
1 change: 1 addition & 0 deletions doc/changes/devel/13021.dependency.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Compatibility improved for Python 3.13, by `Eric Larson`_.
14 changes: 14 additions & 0 deletions doc/install/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,20 @@ of VTK and/or QT are incompatible. This series of commands should fix it:
If you installed VTK using ``pip`` rather than ``conda``, substitute the first
line for ``pip uninstall -y vtk``.

3D plotting trouble on Linux
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you are having trouble with 3D plotting on Linux, one possibility is that you
are using Wayland for graphics. To check, you can do:

.. code-block:: console
$ echo $XDG_SESSION_TYPE
wayland
If so, you will need to tell Qt to use X11 instead of Wayland. You can do this
by setting ``export QT_QPA_PLATFORM=xcb`` in your terminal session. To make it
permanent for your logins, you can set it for example in ``~/.profile``.

.. LINKS
Expand Down
22 changes: 22 additions & 0 deletions doc/sphinxext/mne_doc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import time
import warnings
from pathlib import Path

import numpy as np
import pyvista
Expand All @@ -16,6 +17,7 @@
import mne
from mne.utils import (
_assert_no_instances,
_get_extra_data_path,
sizeof_fmt,
)
from mne.viz import Brain
Expand Down Expand Up @@ -225,6 +227,7 @@ def reset_modules(gallery_conf, fname, when):
mne.viz.ui_events._event_channels
)

orig_when = when
when = f"mne/conf.py:Resetter.__call__:{when}:{fname}"
# Support stuff like
# MNE_SKIP_INSTANCE_ASSERTIONS="Brain,Plotter,BackgroundPlotter,vtkPolyData,_Renderer" make html-memory # noqa: E501
Expand Down Expand Up @@ -262,6 +265,25 @@ def reset_modules(gallery_conf, fname, when):
mem = sizeof_fmt(process.memory_info().rss)
print(f"{prefix}{time.time() - t0:6.1f} s : {mem}".ljust(22))

if fname == "50_configure_mne.py":
# This messes with the config, so let's do so in a temp dir
if orig_when == "before":
fake_home = Path(_get_extra_data_path()) / "temp"
fake_home.mkdir(exist_ok=True, parents=True)
os.environ["_MNE_FAKE_HOME_DIR"] = str(fake_home)
else:
assert orig_when == "after"
to_del = Path(os.environ["_MNE_FAKE_HOME_DIR"])
try:
(to_del / "mne-python.json").unlink()
except Exception:
pass
try:
to_del.rmdir()
except Exception:
pass
del os.environ["_MNE_FAKE_HOME_DIR"]


report_scraper = mne.report._ReportScraper()
mne_qt_browser_scraper = mne.viz._scraper._MNEQtBrowserScraper()
Expand Down
4 changes: 4 additions & 0 deletions doc/sphinxext/related_software.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
"Summary": "A graphical user interface for MNE",
},
# TODO: these do not set a valid homepage or documentation page on PyPI
"eeg_positions": {
"Home-page": "https://eeg-positions.readthedocs.io",
"Summary": "Compute and plot standard EEG electrode positions.",
},
"mne-features": {
"Home-page": "https://mne.tools/mne-features",
"Summary": "MNE-Features software for extracting features from multivariate time series", # noqa: E501
Expand Down
2 changes: 1 addition & 1 deletion examples/preprocessing/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def compute_bias(raw):
idx = epochs.time_as_index(0.036)[0]
data = epochs.get_data(copy=False)[:, :, idx].T
evoked = mne.EvokedArray(data, epochs.info, tmin=0.0)
dip = fit_dipole(evoked, cov, sphere, n_jobs=None, verbose=False)[0]
dip = fit_dipole(evoked, cov, sphere, verbose=False)[0]
actual_pos = mne.dipole.get_phantom_dipoles()[0][dipole_number - 1]
misses = 1000 * np.linalg.norm(dip.pos - actual_pos, axis=-1)
return misses
Expand Down
2 changes: 2 additions & 0 deletions mne/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ def pytest_configure(config):
ignore:__array__ implementation doesn't accept a copy.*:DeprecationWarning
# quantities via neo
ignore:The 'copy' argument in Quantity is deprecated.*:
# debugpy uses deprecated matplotlib API
ignore:The (non_)?interactive_bk attribute was deprecated.*:
""" # noqa: E501
for warning_line in warning_lines.split("\n"):
warning_line = warning_line.strip()
Expand Down
6 changes: 5 additions & 1 deletion mne/datasets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,13 @@ def _download_all_example_data(verbose=True):

# If the user has SUBJECTS_DIR, respect it, if not, set it to the EEG one
# (probably on CircleCI, or otherwise advanced user)
fetch_fsaverage(None)
fetch_fsaverage(subjects_dir=None)
logger.info("[done fsaverage]")

# Now also update the sample dataset path, if not already SUBJECTS_DIR
# (some tutorials make use of these files)
fetch_fsaverage(subjects_dir=paths["sample"] / "subjects")

fetch_infant_template("6mo")
logger.info("[done infant_template]")

Expand Down
3 changes: 2 additions & 1 deletion mne/evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,8 @@ def plot_topo(
exclude="bads",
show=True,
):
"""
""".
Notes
-----
.. versionadded:: 0.10.0
Expand Down
65 changes: 65 additions & 0 deletions mne/io/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,71 @@ def resample(
)
return self, events

@verbose
def rescale(self, scalings, *, verbose=None):
"""Rescale channels.
.. warning::
MNE-Python assumes data are stored in SI base units. This function should
typically only be used to fix an incorrect scaling factor in the data to get
it to be in SI base units, otherwise unintended problems (e.g., incorrect
source imaging results) and analysis errors can occur.
Parameters
----------
scalings : int | float | dict
The scaling factor(s) by which to multiply the data. If a float, the same
scaling factor is applied to all channels (this works only if all channels
are of the same type). If a dict, the keys must be valid channel types and
the values the scaling factors to apply to the corresponding channels.
%(verbose)s
Returns
-------
raw : Raw
The raw object with rescaled data (modified in-place).
Examples
--------
A common use case for EEG data is to convert from µV to V, since many EEG
systems store data in µV, but MNE-Python expects the data to be in V. Therefore,
the data needs to be rescaled by a factor of 1e-6. To rescale all channels from
µV to V, you can do::
>>> raw.rescale(1e-6) # doctest: +SKIP
Note that the previous example only works if all channels are of the same type.
If there are multiple channel types, you can pass a dict with the individual
scaling factors. For example, to rescale only EEG channels, you can do::
>>> raw.rescale({"eeg": 1e-6}) # doctest: +SKIP
"""
_validate_type(scalings, (int, float, dict), "scalings")
_check_preload(self, "raw.rescale")

channel_types = self.get_channel_types(unique=True)

if isinstance(scalings, int | float):
if len(channel_types) == 1:
self.apply_function(lambda x: x * scalings, channel_wise=False)
else:
raise ValueError(
"If scalings is a scalar, all channels must be of the same type. "
"Consider passing a dict instead."
)
else:
for ch_type in scalings.keys():
if ch_type not in channel_types:
raise ValueError(
f'Channel type "{ch_type}" is not present in the Raw file.'
)
for ch_type, ch_scale in scalings.items():
self.apply_function(
lambda x: x * ch_scale, picks=ch_type, channel_wise=False
)

return self

@verbose
def crop(self, tmin=0.0, tmax=None, include_tmax=True, *, verbose=None):
"""Crop raw data file.
Expand Down
17 changes: 17 additions & 0 deletions mne/io/tests/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1063,3 +1063,20 @@ def test_last_samp():
raw = read_raw_fif(raw_fname).crop(0, 0.1).load_data()
last_data = raw._data[:, [-1]]
assert_array_equal(raw[:, -1][0], last_data)


def test_rescale():
"""Test rescaling channels."""
raw = read_raw_fif(raw_fname, preload=True) # multiple channel types

with pytest.raises(ValueError, match="If scalings is a scalar, all channels"):
raw.rescale(2) # need to use dict

orig = raw.get_data(picks="eeg")
raw.rescale({"eeg": 2}) # need to use dict
assert_allclose(raw.get_data(picks="eeg"), orig * 2)

raw.pick("mag") # only a single channel type "mag"
orig = raw.get_data()
raw.rescale(4) # a scalar works
assert_allclose(raw.get_data(), orig * 4)
1 change: 1 addition & 0 deletions mne/tests/test_docstring_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def _func_name(func, cls=None):
error_ignores = {
# These we do not live by:
"GL01", # Docstring should start in the line immediately after the quotes
"GL02", # Closing quotes on own line (doesn't work on Python 3.13 anyway)
"EX01",
"EX02", # examples failed (we test them separately)
"ES01", # no extended summary
Expand Down
55 changes: 33 additions & 22 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5109,6 +5109,8 @@ def copy_doc(source):
This is useful when inheriting from a class and overloading a method. This
decorator can be used to copy the docstring of the original method.
Docstrings are processed by :func:`python:inspect.cleandoc` before being used.
Parameters
----------
source : function
Expand All @@ -5131,15 +5133,16 @@ def copy_doc(source):
... ''' this gets appended'''
... pass
>>> print(B.m1.__doc__)
Docstring for m1 this gets appended
Docstring for m1
this gets appended
"""

def wrapper(func):
if source.__doc__ is None or len(source.__doc__) == 0:
raise ValueError("Cannot copy docstring: docstring was empty.")
doc = source.__doc__
if func.__doc__ is not None:
doc += func.__doc__
doc += f"\n{inspect.cleandoc(func.__doc__)}"
func.__doc__ = doc
return func

Expand All @@ -5158,6 +5161,10 @@ def copy_function_doc_to_method_doc(source):
function. This pattern is prevalent in for example the plotting functions
of MNE.
Docstrings are parsed by :func:`python:inspect.cleandoc` before being used.
If indentation and newlines are important, make the first line ``.``, and the dot
will be removed and all following lines dedented jointly.
Parameters
----------
source : function
Expand Down Expand Up @@ -5193,7 +5200,8 @@ def copy_function_doc_to_method_doc(source):
>>> class A:
... @copy_function_doc_to_method_doc(plot_function)
... def plot(self, a, b):
... '''
... '''.
...
... Notes
... -----
... .. versionadded:: 0.13.0
Expand All @@ -5202,26 +5210,31 @@ def copy_function_doc_to_method_doc(source):
>>> print(A.plot.__doc__)
Docstring for plotting function.
<BLANKLINE>
Parameters
----------
a : int
Some parameter
b : int
Some parameter
<BLANKLINE>
Notes
-----
.. versionadded:: 0.13.0
Parameters
----------
a : int
Some parameter
b : int
Some parameter
<BLANKLINE>
Notes
-----
.. versionadded:: 0.13.0
""" # noqa: D410, D411, D214, D215

def wrapper(func):
doc = source.__doc__.split("\n")
# Work with cleandoc'ed sources (py3.13-compat)
doc = inspect.cleandoc(source.__doc__).split("\n")
if func.__doc__ is not None:
func_doc = inspect.cleandoc(func.__doc__)
if func_doc[:2] == ".\n":
func_doc = func_doc[2:]
func_doc = f"\n{func_doc}"
else:
func_doc = ""

if len(doc) == 1:
doc = doc[0]
if func.__doc__ is not None:
doc += func.__doc__
func.__doc__ = doc
func.__doc__ = f"{doc[0]}{func_doc}"
return func

# Find parameter block
Expand Down Expand Up @@ -5269,7 +5282,7 @@ def wrapper(func):
break
else:
# End of docstring reached
first_parameter_end = line
first_parameter_end = line + 1
first_parameter = parameter_block

# Copy the docstring, but remove the first parameter
Expand All @@ -5278,9 +5291,7 @@ def wrapper(func):
+ "\n"
+ "\n".join(doc[first_parameter_end:])
)
if func.__doc__ is not None:
doc += func.__doc__
func.__doc__ = doc
func.__doc__ = f"{doc}{func_doc}"
return func

return wrapper
Expand Down
2 changes: 1 addition & 1 deletion mne/utils/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def test_sys_info_complete():
pyproject = tomllib.loads(pyproject.read_text("utf-8"))
deps = pyproject["project"]["optional-dependencies"]["test_extra"]
for dep in deps:
dep = dep.split("[")[0].split(">")[0]
dep = dep.split("[")[0].split(">")[0].strip()
assert f" {dep}" in out, f"Missing in dev config: {dep}"


Expand Down
Loading

0 comments on commit e07de28

Please sign in to comment.