Skip to content

Commit c6284fe

Browse files
authored
Merge branch 'main' into epochs-variable-duration
2 parents 524ec2e + 8fa0c10 commit c6284fe

26 files changed

Lines changed: 919 additions & 121 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ mne/viz/_brain/tests/.ipynb_checkpoints
5555

5656
dist/
5757
doc/_build/
58+
doc/pypi/
5859
doc/generated/
5960
doc/auto_examples/
6061
doc/auto_tutorials/

AGENTS.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ User-facing changes need a file `doc/changes/dev/<PR-number>.<type>.rst` (types:
114114
to `doc/changes/names.inc` (build fails otherwise) and are credited with `:newcontrib:` in their
115115
changelog entry instead of a plain name link.
116116

117+
The `<PR-number>` for a not-yet-opened PR is one more than the highest number currently in use;
118+
issues and PRs share a single number sequence, so query the most recently created of either
119+
(the `issues` API endpoint includes PRs):
120+
```bash
121+
gh api "repos/mne-tools/mne-python/issues?state=all&per_page=1&sort=created&direction=desc" \
122+
--jq '.[0].number'
123+
```
124+
117125
## Code conventions (beyond what ruff enforces)
118126

119127
- Classes: `CamelCase`. Functions/variables: `snake_case`, no abbreviated names like `nsamples`.
@@ -151,3 +159,15 @@ changelog entry instead of a plain name link.
151159
# (BSD-compatible).
152160
```
153161
If the license of a snippet cannot be determined, do not adapt it.
162+
163+
- Benchmarking performance changes: only interleaved A/B runs against a
164+
pristine snapshot built from the exact upstream base are trustworthy;
165+
whole-suite back-to-back runs drift ±20–100 %. Verify which installed mne a
166+
benchmark actually imports (`print(mne.__file__)`) before trusting numbers,
167+
and keep fixture data out of commits.
168+
- Changelog fragments (`doc/changes/dev/<PR#>.<type>.rst`): pick `<type>` by
169+
intent — performance improvements are `newfeature`, not `bugfix`. Keep the
170+
entry to one short sentence ending with the contributor name link, e.g.
171+
"Speed up X by optimizing Y, by `Jane Doe`_", and make sure the name anchors
172+
in `doc/changes/names.inc` (add it if missing). Read an existing fragment
173+
or two before writing yours.

doc/Makefile

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ MPROF = SG_STAMP_STARTS=true mprof run -E --python sphinx
99
# Internal variables.
1010
ALLSPHINXOPTS = -d _build/doctrees $(SPHINXOPTS) .
1111

12-
.PHONY: help clean html html-noplot html-pattern linkcheck linkcheck-grep doctest
12+
.PHONY: help clean html html-noplot html-pattern jupyterlite_wheel linkcheck linkcheck-grep doctest
1313

1414
# make with no arguments will build the first target by default, i.e., build standalone HTML files
1515
first_target: html-noplot
@@ -28,29 +28,32 @@ help:
2828
@echo " view to view the built HTML"
2929

3030
clean:
31-
-rm -rf _build sg_execution_times.rst auto_examples auto_tutorials generated *.stc *.fif *.nii.gz
31+
-rm -rf _build pypi sg_execution_times.rst auto_examples auto_tutorials generated *.stc *.fif *.nii.gz
3232

33-
html:
33+
jupyterlite_wheel:
34+
@python sphinxext/build_lite_wheel.py
35+
36+
html: jupyterlite_wheel
3437
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html
3538
@echo
3639
@echo "Build finished. The HTML pages are in _build/html."
3740

38-
html-memory:
41+
html-memory: jupyterlite_wheel
3942
$(MPROF) -b html $(ALLSPHINXOPTS) _build/html
4043
@echo
4144
@echo "Build finished. The HTML pages are in _build/html."
4245

43-
html-pattern:
46+
html-pattern: jupyterlite_wheel
4447
$(SPHINXBUILD) -D sphinx_gallery_conf.filename_pattern=$(PATTERN) -D sphinx_gallery_conf.run_stale_examples=True -b html $(ALLSPHINXOPTS) _build/html
4548
@echo
4649
@echo "Build finished. The HTML pages are in _build/html"
4750

48-
html-pattern-memory:
51+
html-pattern-memory: jupyterlite_wheel
4952
$(MPROF) -D sphinx_gallery_conf.filename_pattern=$(PATTERN) -D sphinx_gallery_conf.run_stale_examples=True -b html $(ALLSPHINXOPTS) _build/html
5053
@echo
5154
@echo "Build finished. The HTML pages are in _build/html"
5255

53-
html-noplot:
56+
html-noplot: jupyterlite_wheel
5457
$(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) _build/html
5558
@echo
5659
@echo "Build finished. The HTML pages are in _build/html."

doc/changes/dev/14135.other.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a build script and ``make`` target that produce the MNE wheel the JupyterLite browser kernel installs, by `Natneal B`_.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a trace-list sidebar to the :class:`mne.viz.Brain` GUI, replacing legend by `Payam Sadeghi-Shabestari`_.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3D plot windows (e.g., :meth:`mne.SourceEstimate.plot`) now follow light/dark mode switches of the operating system (macOS only for now) while they are open, by `Eric Larson`_.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a ``rank`` parameter to :class:`mne.preprocessing.Xdawn` for using Xdawn with rank-deficient data without regularization, by `Eric Larson`_.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up reading by optimizing small read paths, by `Bruno Aristimunha`_

doc/sphinxext/build_lite_wheel.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""Build the MNE wheel for the JupyterLite browser kernel.
2+
3+
Run this once before building the docs, either in CI or locally::
4+
5+
python doc/sphinxext/build_lite_wheel.py
6+
7+
The wheel is written to ``doc/pypi``, where the jupyterlite-pyodide-kernel
8+
PipliteAddon discovers, copies and indexes it (adding it to ``pipliteUrls`` in
9+
``jupyter-lite.json``), so the browser kernel installs the MNE the surrounding
10+
pages are built from rather than the last release on PyPI. That is the
11+
development version on ``main`` and that release's code on a ``maint/*``
12+
branch, since the docs build from whichever branch it is running on. See
13+
https://jupyterlite.readthedocs.io/en/latest/howto/pyodide/wheels.html
14+
15+
Both functions are importable, so a docs build can reuse a wheel that is already
16+
present rather than building one on every invocation::
17+
18+
from build_lite_wheel import build_wheel, find_wheels
19+
20+
wheels = find_wheels() or build_wheel()
21+
"""
22+
23+
# Authors: The MNE-Python contributors.
24+
# License: BSD-3-Clause
25+
# Copyright the MNE-Python contributors.
26+
27+
import json
28+
import shutil
29+
import subprocess
30+
import sys
31+
import urllib.request
32+
from pathlib import Path
33+
34+
REPO_ROOT = Path(__file__).resolve().parents[2]
35+
PYPI_WHEELS_DIR = REPO_ROOT / "doc" / "pypi"
36+
37+
38+
def find_wheels():
39+
"""Return the MNE wheels already present in ``doc/pypi``.
40+
41+
Returns
42+
-------
43+
wheels : list of Path
44+
Paths of the MNE wheels found, empty if there are none.
45+
"""
46+
return sorted(PYPI_WHEELS_DIR.glob("mne-*.whl"))
47+
48+
49+
def _latest_pypi_version():
50+
"""Return the newest MNE version on PyPI, or None if it cannot be reached.
51+
52+
Returns
53+
-------
54+
version : str | None
55+
The version string, or None if PyPI could not be queried.
56+
"""
57+
# Broad on purpose: this only ever runs while raising, so a network problem
58+
# here must not replace the real error with a less useful one.
59+
try:
60+
url = "https://pypi.org/pypi/mne/json"
61+
with urllib.request.urlopen(url, timeout=10) as response:
62+
return json.load(response)["info"]["version"]
63+
except Exception:
64+
return None
65+
66+
67+
def build_wheel():
68+
"""Build the MNE wheel for the browser kernel into ``doc/pypi``.
69+
70+
Returns
71+
-------
72+
wheels : list of Path
73+
Paths of the MNE wheels that were built.
74+
"""
75+
# This directory is the piplite index, so it should hold the wheel this
76+
# build produced and nothing else, including anything left behind by an
77+
# earlier build or a manual pip wheel. The version is left to hatch-vcs:
78+
# piplite serves this index exclusively rather than merging it with PyPI
79+
# (_query_package returns as soon as the package is found here), so a
80+
# development version has nothing to lose a resolution against.
81+
shutil.rmtree(PYPI_WHEELS_DIR, ignore_errors=True)
82+
PYPI_WHEELS_DIR.mkdir(parents=True, exist_ok=True)
83+
84+
# The wheel is built from pyproject.toml as it stands: Pyodide 314 ships
85+
# matplotlib 3.10.8, scipy 1.18.0 and numpy 2.4.3, all of which satisfy the
86+
# minimums MNE declares, so none of them needs relaxing for the browser.
87+
# NB: build isolation is left ON (the default). MNE uses the hatchling build
88+
# backend, so pip must create an isolated build env to install
89+
# hatchling/hatch-vcs; --no-build-isolation fails with "Cannot import
90+
# 'hatchling.build'" on CI, where those build deps are not in the base
91+
# environment.
92+
subprocess.run(
93+
[
94+
sys.executable,
95+
"-m",
96+
"pip",
97+
"wheel",
98+
REPO_ROOT,
99+
"--no-deps",
100+
"-w",
101+
PYPI_WHEELS_DIR,
102+
],
103+
check=True,
104+
)
105+
106+
# Fail loudly rather than silently letting the browser kernel fall back to
107+
# the released MNE from PyPI.
108+
wheels = find_wheels()
109+
if not wheels:
110+
latest = _latest_pypi_version()
111+
fallback = f"MNE {latest}" if latest else "the latest MNE release"
112+
raise RuntimeError(
113+
f"JupyterLite: no MNE wheel was built into {PYPI_WHEELS_DIR}; the "
114+
f"browser kernel would fall back to {fallback} from PyPI. Check the "
115+
"'pip wheel' output above."
116+
)
117+
return wheels
118+
119+
120+
if __name__ == "__main__":
121+
# Reuse a wheel that is already there, so repeat `make html` runs do not
122+
# rebuild it. Remove doc/pypi (or `make clean`) to force a fresh one.
123+
existing = find_wheels()
124+
wheels = ", ".join(str(wheel) for wheel in (existing or build_wheel()))
125+
verb = "Reusing" if existing else "Built"
126+
print(f"[JupyterLite] {verb} MNE wheel(s) for the browser kernel: {wheels}")

examples/preprocessing/xdawn_denoising.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@
4949

5050
raw.info["bads"] = ["MEG 2443"] # set bad channels
5151
picks = pick_types(raw.info, meg=True, eeg=False, stim=False, eog=False, exclude="bads")
52-
# Epoching
52+
# Epoching, applying the SSP projectors that come with this dataset
53+
raw.apply_proj()
5354
epochs = Epochs(
5455
raw,
5556
events,
5657
event_id,
5758
tmin,
5859
tmax,
59-
proj=False,
60+
proj=True,
6061
picks=picks,
6162
baseline=None,
6263
preload=True,
@@ -69,12 +70,19 @@
6970
# %%
7071
# Now, we estimate a set of xDAWN filters for the epochs (which contain only
7172
# the ``vis_r`` class).
73+
#
74+
# Applying the three SSP projectors above makes the data rank deficient (302
75+
# instead of 305), so the generalized eigenvalue decomposition that xDAWN
76+
# relies on is ill-conditioned (and can fail outright) unless we tell it about
77+
# the rank of the data. Passing ``rank="info"`` restricts the decomposition to
78+
# the 302-dimensional principal subspace of the signal covariance and projects
79+
# the resulting filters and patterns back out to the 305 sensors.
7280

7381
# Estimates signal covariance
7482
signal_cov = compute_raw_covariance(raw, picks=picks)
7583

7684
# Xdawn instance
77-
xd = Xdawn(n_components=2, signal_cov=signal_cov)
85+
xd = Xdawn(n_components=2, signal_cov=signal_cov, rank="info")
7886

7987
# Fit xdawn
8088
xd.fit(epochs)

0 commit comments

Comments
 (0)