diff --git a/.mailmap b/.mailmap index 133eb2be306..b7d87833dcc 100644 --- a/.mailmap +++ b/.mailmap @@ -86,6 +86,7 @@ Eduard Ort examplename Ellen Lau ellenlau Emily Stephen Emily P. Stephen Emily Stephen emilyps14 +Emma Bailey <93327939+emma-bailey@users.noreply.github.com> emma-bailey <93327939+emma-bailey@users.noreply.github.com> Enrico Varano enricovara <69973551+enricovara@users.noreply.github.com> Enzo Altamiranda enzo Eric Larson Eric Larson @@ -315,6 +316,7 @@ Sara Sommariva sarasommariva Sebastien Treguer DataFox Sena Er <2799280+sena-neuro@users.noreply.github.com> Sena <2799280+sena-neuro@users.noreply.github.com> Senwen Deng <36327760+snwnde@users.noreply.github.com> Senwen DENG <36327760+snwnde@users.noreply.github.com> +Shristi Baral Shristi Baral Silvia Cotroneo <78911192+sfc-neuro@users.noreply.github.com> sfc-neuro <78911192+sfc-neuro@users.noreply.github.com> Simon Kern Simon Kern <14980558+skjerns@users.noreply.github.com> Simon Kern skjerns <14980558+skjerns@users.noreply.github.com> diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34b3ce9b130..41210aac357 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.9.2 + rev: v0.9.6 hooks: - id: ruff name: ruff lint mne @@ -23,7 +23,7 @@ repos: # Codespell - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell additional_dependencies: @@ -82,7 +82,7 @@ repos: # zizmor - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.2.2 + rev: v1.3.1 hooks: - id: zizmor diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7149edac50b..9c42b3286c1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -89,6 +89,7 @@ stages: DISPLAY: ':99' OPENBLAS_NUM_THREADS: '1' MNE_TEST_ALLOW_SKIP: '^.*(PySide6 causes segfaults).*$' + MNE_BROWSER_PRECOMPUTE: 'false' steps: - bash: | set -e diff --git a/doc/changes/devel/13044.newfeature.rst b/doc/changes/devel/13044.newfeature.rst new file mode 100644 index 00000000000..9633aba66b9 --- /dev/null +++ b/doc/changes/devel/13044.newfeature.rst @@ -0,0 +1 @@ +Add :meth:`mne.Evoked.interpolate_to` to allow interpolating EEG data to other montages, by :newcontrib:`Antoine Collas`. diff --git a/doc/changes/devel/13082.bugfix.rst b/doc/changes/devel/13082.bugfix.rst new file mode 100644 index 00000000000..0f5cad3d0af --- /dev/null +++ b/doc/changes/devel/13082.bugfix.rst @@ -0,0 +1 @@ +Fix bug with automated Mesa 3D detection for proper 3D option setting on systems with software rendering, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/13097.bugfix.rst b/doc/changes/devel/13097.bugfix.rst new file mode 100644 index 00000000000..02500b91027 --- /dev/null +++ b/doc/changes/devel/13097.bugfix.rst @@ -0,0 +1 @@ +Fix bug when loading certain EEGLAB files that do not contain a ``nodatchans`` field, by `Clemens Brunner`_. \ No newline at end of file diff --git a/doc/changes/devel/13100.bugfix.rst b/doc/changes/devel/13100.bugfix.rst new file mode 100644 index 00000000000..edd4c75264d --- /dev/null +++ b/doc/changes/devel/13100.bugfix.rst @@ -0,0 +1 @@ +Do not convert the first "New Segment" marker in a BrainVision file to an annotation, as it only contains the recording date (which is already available in ``info["meas_date"]``), by `Clemens Brunner`_. \ No newline at end of file diff --git a/doc/changes/devel/13101.bugfix.rst b/doc/changes/devel/13101.bugfix.rst new file mode 100644 index 00000000000..d24e55b5056 --- /dev/null +++ b/doc/changes/devel/13101.bugfix.rst @@ -0,0 +1 @@ +Take units (m or mm) into account when drawing :func:`~mne.viz.plot_evoked_field` on top of :class:`~mne.viz.Brain`, by `Marijn van Vliet`_. diff --git a/doc/changes/devel/13107.newfeature.rst b/doc/changes/devel/13107.newfeature.rst new file mode 100644 index 00000000000..a19381fbdb2 --- /dev/null +++ b/doc/changes/devel/13107.newfeature.rst @@ -0,0 +1 @@ +The :meth:`mne.Info.save` method now has an ``overwrite`` and a ``verbose`` parameter, by `Stefan Appelhoff`_. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 5a58ac0fa34..282fa8341a0 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -24,6 +24,7 @@ .. _Anna Padee: https://github.com/apadee/ .. _Annalisa Pascarella: https://www.iac.cnr.it/personale/annalisa-pascarella .. _Anne-Sophie Dubarry: https://github.com/annesodub +.. _Antoine Collas: https://www.antoinecollas.fr .. _Antoine Gauthier: https://github.com/Okamille .. _Antti Rantala: https://github.com/Odingod .. _Apoorva Karekal: https://github.com/apoorva6262 @@ -256,7 +257,7 @@ .. _Romain Derollepot: https://github.com/rderollepot .. _Romain Trachel: https://fr.linkedin.com/in/trachelr .. _Roman Goj: https://romanmne.blogspot.co.uk -.. _Ross Maddox: https://www.urmc.rochester.edu/labs/maddox-lab.aspx +.. _Ross Maddox: https://medicine.umich.edu/dept/khri/ross-maddox-phd .. _Rotem Falach: https://github.com/Falach .. _Roy Eric Wieske: https://github.com/Randomidous .. _Sammi Chekroud: https://github.com/schekroud diff --git a/doc/conf.py b/doc/conf.py index f1b771571d6..a6c9579c133 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -644,6 +644,7 @@ def fix_sklearn_inherited_docstrings(app, what, name, obj, options, lines): linkcheck_ignore = [ # will be compiled to regex # 403 Client Error: Forbidden "https://doi.org/10.1002/", # onlinelibrary.wiley.com/doi/10.1002/hbm + "https://doi.org/10.1016/", # neuroimage "https://doi.org/10.1021/", # pubs.acs.org/doi/abs "https://doi.org/10.1073/", # pnas.org "https://doi.org/10.1093/", # academic.oup.com/sleep/ @@ -667,10 +668,11 @@ 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", + "http://prdownloads.sourceforge.net/optipng", "https://sourceforge.net/projects/aespa/files/", "https://sourceforge.net/projects/ezwinports/files/", "https://www.mathworks.com/products/compiler/matlab-runtime.html", + "https://medicine.umich.edu/dept/khri/ross-maddox-phd", # 500 server error "https://openwetware.org/wiki/Beauchamp:FreeSurfer", # 503 Server error @@ -694,6 +696,8 @@ def fix_sklearn_inherited_docstrings(app, what, name, obj, options, lines): "http://ilabs.washington.edu", "https://psychophysiology.cpmc.columbia.edu", "https://erc.easme-web.eu", + # Not rendered by linkcheck builder + r"ides\.html", ] linkcheck_anchors = False # saves a bit of time linkcheck_timeout = 15 # some can be quite slow diff --git a/doc/references.bib b/doc/references.bib index e2578ed18f2..f0addb5f3b2 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -2514,3 +2514,11 @@ @article{OyamaEtAl2015 year = {2015}, pages = {24--36}, } + +@inproceedings{MellotEtAl2024, + title = {Physics-informed and Unsupervised Riemannian Domain Adaptation for Machine Learning on Heterogeneous EEG Datasets}, + author = {Mellot, Apolline and Collas, Antoine and Chevallier, Sylvain and Engemann, Denis and Gramfort, Alexandre}, + booktitle = {Proceedings of the 32nd European Signal Processing Conference (EUSIPCO)}, + year = {2024}, + address = {Lyon, France} +} diff --git a/doc/sphinxext/credit_tools.py b/doc/sphinxext/credit_tools.py index e22bd0b5530..d3886b11200 100644 --- a/doc/sphinxext/credit_tools.py +++ b/doc/sphinxext/credit_tools.py @@ -39,6 +39,16 @@ # Allowed singletons single_names = "btkcodedev buildqa sviter Akshay".split() +# Surnames where we have more than one distinct contributor: +name_counts = dict( + Bailey=2, + Das=2, + Drew=2, + Li=2, + Peterson=2, + Wong=2, + Zhang=2, +) # Exceptions, e.g., abbrevitaions in first/last name or all-caps exceptions = [ "T. Wang", @@ -217,16 +227,6 @@ def generate_credit_rst(app=None, *, verbose=False): ) # Check for duplicate names based on last name, and also singleton names. - # Below are surnames where we have more than one distinct contributor: - name_counts = dict( - Das=2, - Drew=2, - Li=2, - Peterson=2, - Wong=2, - Zhang=2, - ) - # Below are allowed singleton names last_map = defaultdict(lambda: set()) bad_names = set() for these_stats in stats.values(): @@ -245,10 +245,11 @@ def generate_credit_rst(app=None, *, verbose=False): if len(names) > name_counts.get(last, 1): bad_names.append(f"Duplicates: {sorted(names)}") if bad_names: - raise RuntimeError( - "Unexpected possible duplicates or bad names found:\n" - + "\n".join(bad_names) + what = ( + "Unexpected possible duplicates or bad names found, " + f"consider modifying {'/'.join(Path(__file__).parts[-3:])}:\n" ) + raise RuntimeError(what + "\n".join(bad_names)) unknown_emails = set( email @@ -258,9 +259,8 @@ def generate_credit_rst(app=None, *, verbose=False): and "dependabot[bot]" not in email and "github-actions[bot]" not in email ) - assert len(unknown_emails) == 0, "Unknown emails\n" + "\n".join( - sorted(unknown_emails) - ) + what = "Unknown emails, consider adding to .mailmap:\n" + assert len(unknown_emails) == 0, what + "\n".join(sorted(unknown_emails)) logger.info("Biggest included commits/PRs:") commits = dict( diff --git a/doc/sphinxext/prs/12071.json b/doc/sphinxext/prs/12071.json new file mode 100644 index 00000000000..09e7ab310dd --- /dev/null +++ b/doc/sphinxext/prs/12071.json @@ -0,0 +1,67 @@ +{ + "merge_commit_sha": "aca49655b10fc17679142e07c5d46659be1099da", + "authors": [ + { + "n": "Marijn van Vliet", + "e": "w.m.vanvliet@gmail.com" + }, + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + } + ], + "changes": { + "doc/changes/devel/12071.newfeature.rst": { + "a": 1, + "d": 0 + }, + "mne/epochs.py": { + "a": 2, + "d": 0 + }, + "mne/evoked.py": { + "a": 2, + "d": 0 + }, + "mne/viz/_figure.py": { + "a": 3, + "d": 3 + }, + "mne/viz/_mpl_figure.py": { + "a": 1, + "d": 1 + }, + "mne/viz/evoked.py": { + "a": 12, + "d": 1 + }, + "mne/viz/tests/test_raw.py": { + "a": 14, + "d": 25 + }, + "mne/viz/tests/test_topo.py": { + "a": 35, + "d": 1 + }, + "mne/viz/tests/test_utils.py": { + "a": 69, + "d": 0 + }, + "mne/viz/topo.py": { + "a": 81, + "d": 15 + }, + "mne/viz/ui_events.py": { + "a": 20, + "d": 0 + }, + "mne/viz/utils.py": { + "a": 108, + "d": 63 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/12656.json b/doc/sphinxext/prs/12656.json new file mode 100644 index 00000000000..6755a485d1c --- /dev/null +++ b/doc/sphinxext/prs/12656.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "3c6a054093d305a98757a97398e5e34988a3aced", + "authors": [ + { + "n": "Qian Chu", + "e": null + } + ], + "changes": { + "doc/changes/devel/12656.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/export/_brainvision.py": { + "a": 7, + "d": 0 + }, + "mne/export/_edf.py": { + "a": 4, + "d": 1 + }, + "mne/export/_eeglab.py": { + "a": 11, + "d": 5 + }, + "mne/export/_export.py": { + "a": 7, + "d": 1 + }, + "mne/export/tests/test_export.py": { + "a": 82, + "d": 7 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/12828.json b/doc/sphinxext/prs/12828.json new file mode 100644 index 00000000000..6030dffc960 --- /dev/null +++ b/doc/sphinxext/prs/12828.json @@ -0,0 +1,43 @@ +{ + "merge_commit_sha": "176f64ff061136cf5628d76535a8d7e2e164d399", + "authors": [ + { + "n": "Shristi Baral", + "e": null + }, + { + "n": "shristi", + "e": "shristi.baral@aalto.fi" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + } + ], + "changes": { + "doc/changes/devel/12828.bugfix.rst": { + "a": 1, + "d": 0 + }, + "doc/changes/names.inc": { + "a": 1, + "d": 0 + }, + "mne/source_estimate.py": { + "a": 7, + "d": 0 + }, + "mne/utils/docs.py": { + "a": 6, + "d": 0 + }, + "mne/viz/_3d.py": { + "a": 13, + "d": 3 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/12910.json b/doc/sphinxext/prs/12910.json new file mode 100644 index 00000000000..6240f4368c0 --- /dev/null +++ b/doc/sphinxext/prs/12910.json @@ -0,0 +1,51 @@ +{ + "merge_commit_sha": "1d2635f84a55785c3531cfe4027eda3820a7fb31", + "authors": [ + { + "n": "Thomas S. Binns", + "e": "t.s.binns@outlook.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/changes/devel/12910.newfeature.rst": { + "a": 1, + "d": 0 + }, + "mne/time_frequency/multitaper.py": { + "a": 10, + "d": 0 + }, + "mne/time_frequency/tests/test_tfr.py": { + "a": 195, + "d": 26 + }, + "mne/time_frequency/tfr.py": { + "a": 249, + "d": 113 + }, + "mne/utils/docs.py": { + "a": 12, + "d": 0 + }, + "mne/utils/numerics.py": { + "a": 3, + "d": 0 + }, + "mne/viz/tests/test_topomap.py": { + "a": 24, + "d": 1 + }, + "mne/viz/topomap.py": { + "a": 13, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13037.json b/doc/sphinxext/prs/13037.json new file mode 100644 index 00000000000..7de2d953e82 --- /dev/null +++ b/doc/sphinxext/prs/13037.json @@ -0,0 +1,107 @@ +{ + "merge_commit_sha": "4f53a3732917dd1dbc91d4725ae79fc1c7ad4661", + "authors": [ + { + "n": "Steinn Hauser Magnússon", + "e": null + }, + { + "n": "Emma Bailey", + "e": "bailey@cbs.mpg.de" + }, + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Steinn Magnusson", + "e": "s.magnusson@senec.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + }, + { + "n": "emma-bailey", + "e": "93327939+emma-bailey@users.noreply.github.com" + }, + { + "n": "autofix-ci[bot]", + "e": "114827586+autofix-ci[bot]@users.noreply.github.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + } + ], + "changes": { + ".circleci/config.yml": { + "a": 8, + "d": 1 + }, + "doc/api/datasets.rst": { + "a": 1, + "d": 0 + }, + "doc/api/preprocessing.rst": { + "a": 1, + "d": 0 + }, + "doc/changes/devel/13037.newfeature.rst": { + "a": 1, + "d": 0 + }, + "doc/changes/names.inc": { + "a": 2, + "d": 0 + }, + "doc/conf.py": { + "a": 1, + "d": 0 + }, + "doc/references.bib": { + "a": 10, + "d": 0 + }, + "examples/preprocessing/esg_rm_heart_artefact_pcaobs.py": { + "a": 196, + "d": 0 + }, + "mne/datasets/__init__.pyi": { + "a": 2, + "d": 0 + }, + "mne/datasets/utils.py": { + "a": 29, + "d": 1 + }, + "mne/preprocessing/__init__.pyi": { + "a": 2, + "d": 0 + }, + "mne/preprocessing/_pca_obs.py": { + "a": 333, + "d": 0 + }, + "mne/preprocessing/tests/test_pca_obs.py": { + "a": 107, + "d": 0 + }, + "mne/utils/numerics.py": { + "a": 3, + "d": 0 + }, + "pyproject.toml": { + "a": 1, + "d": 0 + }, + "tools/circleci_dependencies.sh": { + "a": 1, + "d": 1 + }, + "tutorials/preprocessing/50_artifact_correction_ssp.py": { + "a": 8, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13045.json b/doc/sphinxext/prs/13045.json new file mode 100644 index 00000000000..cb3db89fefd --- /dev/null +++ b/doc/sphinxext/prs/13045.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "fd8c1eed5209393e6a2527342b61354efcb813ee", + "authors": [ + { + "n": "github-actions[bot]", + "e": "41898282+github-actions[bot]@users.noreply.github.com" + } + ], + "changes": { + "doc/sphinxext/prs/13028.json": { + "a": 23, + "d": 0 + }, + "doc/sphinxext/prs/13038.json": { + "a": 63, + "d": 0 + }, + "doc/sphinxext/prs/13040.json": { + "a": 171, + "d": 0 + }, + "doc/sphinxext/prs/13041.json": { + "a": 43, + "d": 0 + }, + "doc/sphinxext/prs/13042.json": { + "a": 27, + "d": 0 + }, + "doc/sphinxext/prs/13043.json": { + "a": 15, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13046.json b/doc/sphinxext/prs/13046.json new file mode 100644 index 00000000000..f946cb5a90a --- /dev/null +++ b/doc/sphinxext/prs/13046.json @@ -0,0 +1,23 @@ +{ + "merge_commit_sha": "d81b7a66b692cfc3065ba260f4702f842d45e414", + "authors": [ + { + "n": "Stefan Appelhoff", + "e": "stefan.appelhoff@mailbox.org" + } + ], + "changes": { + "doc/changes/v1.7.rst": { + "a": 4, + "d": 4 + }, + "doc/changes/v1.9.rst": { + "a": 4, + "d": 4 + }, + "mne/channels/montage.py": { + "a": 1, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13047.json b/doc/sphinxext/prs/13047.json new file mode 100644 index 00000000000..9267f7fada0 --- /dev/null +++ b/doc/sphinxext/prs/13047.json @@ -0,0 +1,19 @@ +{ + "merge_commit_sha": "47ea36043076e4e38896eda6241c6cb0d4c25938", + "authors": [ + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + ".pre-commit-config.yaml": { + "a": 2, + "d": 2 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13048.json b/doc/sphinxext/prs/13048.json new file mode 100644 index 00000000000..a111abed437 --- /dev/null +++ b/doc/sphinxext/prs/13048.json @@ -0,0 +1,23 @@ +{ + "merge_commit_sha": "6bc7dfb3535ef954b7a6fcbad93887abe2aa7c92", + "authors": [ + { + "n": "Marijn van Vliet", + "e": "w.m.vanvliet@gmail.com" + }, + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + } + ], + "changes": { + "doc/changes/devel/13048.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/viz/evoked_field.py": { + "a": 30, + "d": 39 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13049.json b/doc/sphinxext/prs/13049.json new file mode 100644 index 00000000000..f66fcc1b618 --- /dev/null +++ b/doc/sphinxext/prs/13049.json @@ -0,0 +1,87 @@ +{ + "merge_commit_sha": "df350211b9177a26a2c5fe1e76ebc8e9d6cb4d99", + "authors": [ + { + "n": "Marijn van Vliet", + "e": "w.m.vanvliet@gmail.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + ".circleci/config.yml": { + "a": 1, + "d": 1 + }, + ".github/workflows/tests.yml": { + "a": 9, + "d": 11 + }, + "doc/conf.py": { + "a": 5, + "d": 0 + }, + "doc/development/contributing.rst": { + "a": 1, + "d": 1 + }, + "doc/install/installers.rst": { + "a": 1, + "d": 1 + }, + "environment.yml": { + "a": 2, + "d": 0 + }, + "mne/conftest.py": { + "a": 4, + "d": 0 + }, + "mne/datasets/sleep_physionet/_utils.py": { + "a": 6, + "d": 1 + }, + "mne/decoding/base.py": { + "a": 5, + "d": 1 + }, + "mne/decoding/transformer.py": { + "a": 4, + "d": 4 + }, + "mne/export/tests/test_export.py": { + "a": 2, + "d": 2 + }, + "mne/fixes.py": { + "a": 13, + "d": 0 + }, + "mne/preprocessing/maxwell.py": { + "a": 4, + "d": 4 + }, + "mne/preprocessing/tests/test_maxwell.py": { + "a": 3, + "d": 3 + }, + "mne/transforms.py": { + "a": 2, + "d": 3 + }, + "tools/github_actions_dependencies.sh": { + "a": 1, + "d": 1 + }, + "tools/hooks/update_environment_file.py": { + "a": 8, + "d": 38 + }, + "tools/install_pre_requirements.sh": { + "a": 4, + "d": 3 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13051.json b/doc/sphinxext/prs/13051.json new file mode 100644 index 00000000000..8a9acb2b286 --- /dev/null +++ b/doc/sphinxext/prs/13051.json @@ -0,0 +1,15 @@ +{ + "merge_commit_sha": "5fec4e024a963c3f628693ab172d5b77cbafe6db", + "authors": [ + { + "n": "Simon Kern", + "e": null + } + ], + "changes": { + "mne/channels/channels.py": { + "a": 15, + "d": 5 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13053.json b/doc/sphinxext/prs/13053.json new file mode 100644 index 00000000000..fc85ba79c3d --- /dev/null +++ b/doc/sphinxext/prs/13053.json @@ -0,0 +1,87 @@ +{ + "merge_commit_sha": "dedb3921b0ebadcc5a630234530604706c6faddd", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + } + ], + "changes": { + ".github/workflows/tests.yml": { + "a": 7, + "d": 6 + }, + ".pre-commit-config.yaml": { + "a": 2, + "d": 2 + }, + "azure-pipelines.yml": { + "a": 10, + "d": 13 + }, + "environment.yml": { + "a": 1, + "d": 1 + }, + "mne/conftest.py": { + "a": 54, + "d": 3 + }, + "mne/decoding/tests/test_ssd.py": { + "a": 1, + "d": 1 + }, + "mne/tests/test_parallel.py": { + "a": 1, + "d": 1 + }, + "mne/viz/_brain/tests/test_brain.py": { + "a": 0, + "d": 8 + }, + "mne/viz/tests/test_evoked.py": { + "a": 4, + "d": 2 + }, + "mne/viz/tests/test_raw.py": { + "a": 2, + "d": 4 + }, + "pyproject.toml": { + "a": 0, + "d": 1 + }, + "tools/azure_dependencies.sh": { + "a": 1, + "d": 0 + }, + "tools/get_minimal_commands.sh": { + "a": 5, + "d": 5 + }, + "tools/get_testing_version.sh": { + "a": 1, + "d": 1 + }, + "tools/github_actions_env_vars.sh": { + "a": 16, + "d": 12 + }, + "tools/github_actions_test.sh": { + "a": 15, + "d": 9 + }, + "tools/hooks/update_environment_file.py": { + "a": 3, + "d": 0 + }, + "tools/install_pre_requirements.sh": { + "a": 6, + "d": 8 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13054.json b/doc/sphinxext/prs/13054.json new file mode 100644 index 00000000000..3d8a7b9f99f --- /dev/null +++ b/doc/sphinxext/prs/13054.json @@ -0,0 +1,27 @@ +{ + "merge_commit_sha": "2ae61edccb2af5b5f9f3a89a3131499b5c229c27", + "authors": [ + { + "n": "Thomas S. Binns", + "e": "t.s.binns@outlook.com" + } + ], + "changes": { + "doc/api/time_frequency.rst": { + "a": 1, + "d": 0 + }, + "doc/changes/devel/13054.newfeature.rst": { + "a": 1, + "d": 0 + }, + "mne/time_frequency/__init__.pyi": { + "a": 2, + "d": 0 + }, + "mne/time_frequency/tfr.py": { + "a": 4, + "d": 4 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13056.json b/doc/sphinxext/prs/13056.json new file mode 100644 index 00000000000..94aa0bd39b6 --- /dev/null +++ b/doc/sphinxext/prs/13056.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "2abb7b220ed2580e141158499919300cfa1f6a3b", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + }, + { + "n": "autofix-ci[bot]", + "e": "114827586+autofix-ci[bot]@users.noreply.github.com" + } + ], + "changes": { + "doc/changes/devel/13056.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/_fiff/meas_info.py": { + "a": 11, + "d": 4 + }, + "mne/_fiff/tests/test_meas_info.py": { + "a": 30, + "d": 18 + }, + "mne/_fiff/write.py": { + "a": 5, + "d": 4 + }, + "mne/utils/_testing.py": { + "a": 2, + "d": 2 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13058.json b/doc/sphinxext/prs/13058.json new file mode 100644 index 00000000000..ef7ec02786e --- /dev/null +++ b/doc/sphinxext/prs/13058.json @@ -0,0 +1,43 @@ +{ + "merge_commit_sha": "f82d3993617d2a34744eb955385448c67672d6ec", + "authors": [ + { + "n": "Thomas S. Binns", + "e": "t.s.binns@outlook.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + } + ], + "changes": { + "doc/api/time_frequency.rst": { + "a": 1, + "d": 0 + }, + "doc/changes/devel/13058.newfeature.rst": { + "a": 1, + "d": 0 + }, + "mne/time_frequency/__init__.pyi": { + "a": 2, + "d": 0 + }, + "mne/time_frequency/spectrum.py": { + "a": 68, + "d": 0 + }, + "mne/time_frequency/tests/test_spectrum.py": { + "a": 54, + "d": 1 + }, + "mne/time_frequency/tfr.py": { + "a": 6, + "d": 6 + }, + "mne/utils/numerics.py": { + "a": 33, + "d": 24 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13060.json b/doc/sphinxext/prs/13060.json new file mode 100644 index 00000000000..67c9ee55115 --- /dev/null +++ b/doc/sphinxext/prs/13060.json @@ -0,0 +1,423 @@ +{ + "merge_commit_sha": "d472c268cb39fb6e4bf0dad24c802b17efdd4a33", + "authors": [ + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + ".pre-commit-config.yaml": { + "a": 2, + "d": 2 + }, + "azure-pipelines.yml": { + "a": 1, + "d": 1 + }, + "doc/conf.py": { + "a": 1, + "d": 1 + }, + "doc/sphinxext/credit_tools.py": { + "a": 4, + "d": 4 + }, + "doc/sphinxext/related_software.py": { + "a": 3, + "d": 3 + }, + "doc/sphinxext/unit_role.py": { + "a": 1, + "d": 2 + }, + "examples/inverse/vector_mne_solution.py": { + "a": 1, + "d": 1 + }, + "examples/visualization/evoked_whitening.py": { + "a": 1, + "d": 1 + }, + "mne/_fiff/_digitization.py": { + "a": 1, + "d": 2 + }, + "mne/_fiff/meas_info.py": { + "a": 3, + "d": 4 + }, + "mne/_fiff/proj.py": { + "a": 7, + "d": 9 + }, + "mne/_fiff/reference.py": { + "a": 3, + "d": 3 + }, + "mne/_fiff/tag.py": { + "a": 1, + "d": 2 + }, + "mne/_fiff/tests/test_meas_info.py": { + "a": 1, + "d": 1 + }, + "mne/_fiff/tests/test_pick.py": { + "a": 1, + "d": 1 + }, + "mne/beamformer/_compute_beamformer.py": { + "a": 5, + "d": 5 + }, + "mne/beamformer/tests/test_lcmv.py": { + "a": 1, + "d": 1 + }, + "mne/bem.py": { + "a": 6, + "d": 8 + }, + "mne/channels/channels.py": { + "a": 1, + "d": 1 + }, + "mne/channels/montage.py": { + "a": 1, + "d": 1 + }, + "mne/channels/tests/test_channels.py": { + "a": 1, + "d": 2 + }, + "mne/channels/tests/test_montage.py": { + "a": 1, + "d": 6 + }, + "mne/commands/mne_make_scalp_surfaces.py": { + "a": 1, + "d": 2 + }, + "mne/commands/mne_setup_source_space.py": { + "a": 1, + "d": 2 + }, + "mne/coreg.py": { + "a": 3, + "d": 6 + }, + "mne/cov.py": { + "a": 5, + "d": 5 + }, + "mne/datasets/_fetch.py": { + "a": 1, + "d": 2 + }, + "mne/datasets/config.py": { + "a": 4, + "d": 5 + }, + "mne/datasets/sleep_physionet/age.py": { + "a": 1, + "d": 4 + }, + "mne/epochs.py": { + "a": 6, + "d": 10 + }, + "mne/event.py": { + "a": 1, + "d": 1 + }, + "mne/evoked.py": { + "a": 5, + "d": 7 + }, + "mne/export/_egimff.py": { + "a": 1, + "d": 1 + }, + "mne/export/_export.py": { + "a": 1, + "d": 2 + }, + "mne/export/tests/test_export.py": { + "a": 2, + "d": 2 + }, + "mne/filter.py": { + "a": 2, + "d": 3 + }, + "mne/forward/_field_interpolation.py": { + "a": 2, + "d": 3 + }, + "mne/forward/_make_forward.py": { + "a": 5, + "d": 6 + }, + "mne/forward/forward.py": { + "a": 4, + "d": 7 + }, + "mne/gui/_coreg.py": { + "a": 2, + "d": 4 + }, + "mne/html_templates/_templates.py": { + "a": 1, + "d": 1 + }, + "mne/io/array/__init__.py": { + "a": 1, + "d": 1 + }, + "mne/io/array/_array.py": { + "a": 0, + "d": 0 + }, + "mne/io/artemis123/tests/test_artemis123.py": { + "a": 3, + "d": 3 + }, + "mne/io/base.py": { + "a": 2, + "d": 3 + }, + "mne/io/ctf/ctf.py": { + "a": 1, + "d": 1 + }, + "mne/io/ctf/info.py": { + "a": 1, + "d": 2 + }, + "mne/io/ctf/tests/test_ctf.py": { + "a": 3, + "d": 3 + }, + "mne/io/edf/edf.py": { + "a": 3, + "d": 3 + }, + "mne/io/egi/egimff.py": { + "a": 1, + "d": 1 + }, + "mne/io/fieldtrip/fieldtrip.py": { + "a": 1, + "d": 1 + }, + "mne/io/fil/tests/test_fil.py": { + "a": 9, + "d": 9 + }, + "mne/io/neuralynx/tests/test_neuralynx.py": { + "a": 6, + "d": 6 + }, + "mne/io/nirx/nirx.py": { + "a": 1, + "d": 1 + }, + "mne/io/tests/test_raw.py": { + "a": 1, + "d": 1 + }, + "mne/label.py": { + "a": 5, + "d": 9 + }, + "mne/minimum_norm/inverse.py": { + "a": 2, + "d": 2 + }, + "mne/minimum_norm/tests/test_inverse.py": { + "a": 1, + "d": 2 + }, + "mne/morph.py": { + "a": 3, + "d": 6 + }, + "mne/preprocessing/_fine_cal.py": { + "a": 1, + "d": 1 + }, + "mne/preprocessing/artifact_detection.py": { + "a": 4, + "d": 4 + }, + "mne/preprocessing/eog.py": { + "a": 2, + "d": 2 + }, + "mne/preprocessing/hfc.py": { + "a": 1, + "d": 2 + }, + "mne/preprocessing/ica.py": { + "a": 8, + "d": 10 + }, + "mne/preprocessing/ieeg/_volume.py": { + "a": 1, + "d": 1 + }, + "mne/preprocessing/infomax_.py": { + "a": 1, + "d": 2 + }, + "mne/preprocessing/maxwell.py": { + "a": 4, + "d": 4 + }, + "mne/preprocessing/nirs/_beer_lambert_law.py": { + "a": 1, + "d": 1 + }, + "mne/preprocessing/tests/test_maxwell.py": { + "a": 3, + "d": 3 + }, + "mne/preprocessing/xdawn.py": { + "a": 1, + "d": 2 + }, + "mne/report/report.py": { + "a": 7, + "d": 10 + }, + "mne/source_estimate.py": { + "a": 4, + "d": 5 + }, + "mne/source_space/_source_space.py": { + "a": 4, + "d": 6 + }, + "mne/surface.py": { + "a": 8, + "d": 9 + }, + "mne/tests/test_annotations.py": { + "a": 1, + "d": 2 + }, + "mne/tests/test_dipole.py": { + "a": 3, + "d": 3 + }, + "mne/tests/test_docstring_parameters.py": { + "a": 1, + "d": 2 + }, + "mne/tests/test_epochs.py": { + "a": 6, + "d": 6 + }, + "mne/tests/test_filter.py": { + "a": 3, + "d": 3 + }, + "mne/time_frequency/_stft.py": { + "a": 1, + "d": 2 + }, + "mne/time_frequency/csd.py": { + "a": 2, + "d": 3 + }, + "mne/time_frequency/spectrum.py": { + "a": 9, + "d": 9 + }, + "mne/time_frequency/tfr.py": { + "a": 11, + "d": 12 + }, + "mne/utils/_logging.py": { + "a": 1, + "d": 1 + }, + "mne/utils/check.py": { + "a": 5, + "d": 8 + }, + "mne/utils/config.py": { + "a": 6, + "d": 8 + }, + "mne/utils/misc.py": { + "a": 1, + "d": 1 + }, + "mne/viz/_brain/_brain.py": { + "a": 6, + "d": 6 + }, + "mne/viz/_brain/tests/test_brain.py": { + "a": 6, + "d": 6 + }, + "mne/viz/_proj.py": { + "a": 1, + "d": 2 + }, + "mne/viz/backends/_utils.py": { + "a": 1, + "d": 2 + }, + "mne/viz/misc.py": { + "a": 2, + "d": 3 + }, + "mne/viz/tests/test_3d.py": { + "a": 1, + "d": 1 + }, + "mne/viz/topomap.py": { + "a": 5, + "d": 9 + }, + "mne/viz/utils.py": { + "a": 1, + "d": 1 + }, + "tools/dev/ensure_headers.py": { + "a": 6, + "d": 6 + }, + "tools/hooks/update_environment_file.py": { + "a": 1, + "d": 1 + }, + "tutorials/forward/20_source_alignment.py": { + "a": 2, + "d": 2 + }, + "tutorials/forward/30_forward.py": { + "a": 1, + "d": 1 + }, + "tutorials/intro/15_inplace.py": { + "a": 2, + "d": 2 + }, + "tutorials/preprocessing/40_artifact_correction_ica.py": { + "a": 1, + "d": 2 + }, + "tutorials/preprocessing/50_artifact_correction_ssp.py": { + "a": 1, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13061.json b/doc/sphinxext/prs/13061.json new file mode 100644 index 00000000000..62ddd1e87e9 --- /dev/null +++ b/doc/sphinxext/prs/13061.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "9ef5c23927e377d0a66169185f394297ea29d7b4", + "authors": [ + { + "n": "Lumberbot (aka Jack)", + "e": null + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/changes/devel/13056.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/_fiff/meas_info.py": { + "a": 11, + "d": 4 + }, + "mne/_fiff/tests/test_meas_info.py": { + "a": 30, + "d": 18 + }, + "mne/_fiff/write.py": { + "a": 5, + "d": 4 + }, + "mne/utils/_testing.py": { + "a": 2, + "d": 2 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13062.json b/doc/sphinxext/prs/13062.json new file mode 100644 index 00000000000..4160927b960 --- /dev/null +++ b/doc/sphinxext/prs/13062.json @@ -0,0 +1,31 @@ +{ + "merge_commit_sha": "c0da91db9098b92ec3c20d8c0e237d0e02683865", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + }, + { + "n": "autofix-ci[bot]", + "e": "114827586+autofix-ci[bot]@users.noreply.github.com" + } + ], + "changes": { + "azure-pipelines.yml": { + "a": 1, + "d": 1 + }, + "doc/changes/devel/13062.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/preprocessing/_fine_cal.py": { + "a": 9, + "d": 6 + }, + "mne/preprocessing/tests/test_fine_cal.py": { + "a": 13, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13063.json b/doc/sphinxext/prs/13063.json new file mode 100644 index 00000000000..77684e67814 --- /dev/null +++ b/doc/sphinxext/prs/13063.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "087779c3bd5ba84dbcef7f3689a7d70f0b045da7", + "authors": [ + { + "n": "Santeri Ruuskanen", + "e": null + }, + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/changes/devel/13063.bugfix.rst": { + "a": 1, + "d": 0 + }, + "examples/visualization/evoked_topomap.py": { + "a": 2, + "d": 2 + }, + "mne/viz/evoked.py": { + "a": 11, + "d": 2 + }, + "mne/viz/topomap.py": { + "a": 24, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13064.json b/doc/sphinxext/prs/13064.json new file mode 100644 index 00000000000..c18116b1cd1 --- /dev/null +++ b/doc/sphinxext/prs/13064.json @@ -0,0 +1,39 @@ +{ + "merge_commit_sha": "672bdf4357f815c63dfab91d9c8e257266bceb21", + "authors": [ + { + "n": "Lumberbot (aka Jack)", + "e": null + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + ".pre-commit-config.yaml": { + "a": 1, + "d": 1 + }, + "README.rst": { + "a": 1, + "d": 1 + }, + "azure-pipelines.yml": { + "a": 1, + "d": 1 + }, + "doc/changes/devel/13062.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/preprocessing/_fine_cal.py": { + "a": 9, + "d": 6 + }, + "mne/preprocessing/tests/test_fine_cal.py": { + "a": 13, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13065.json b/doc/sphinxext/prs/13065.json new file mode 100644 index 00000000000..071c98cd26f --- /dev/null +++ b/doc/sphinxext/prs/13065.json @@ -0,0 +1,103 @@ +{ + "merge_commit_sha": "5f2b7f1a33d42c5a110f67e098f9efcf92be7fff", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + }, + { + "n": "autofix-ci[bot]", + "e": "114827586+autofix-ci[bot]@users.noreply.github.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + } + ], + "changes": { + "doc/changes/devel/13065.bugfix.rst": { + "a": 7, + "d": 0 + }, + "examples/decoding/linear_model_patterns.py": { + "a": 1, + "d": 1 + }, + "mne/cov.py": { + "a": 2, + "d": 2 + }, + "mne/decoding/base.py": { + "a": 8, + "d": 3 + }, + "mne/decoding/csp.py": { + "a": 43, + "d": 56 + }, + "mne/decoding/ems.py": { + "a": 19, + "d": 6 + }, + "mne/decoding/search_light.py": { + "a": 56, + "d": 25 + }, + "mne/decoding/ssd.py": { + "a": 75, + "d": 63 + }, + "mne/decoding/tests/test_base.py": { + "a": 10, + "d": 4 + }, + "mne/decoding/tests/test_csp.py": { + "a": 31, + "d": 15 + }, + "mne/decoding/tests/test_ems.py": { + "a": 7, + "d": 0 + }, + "mne/decoding/tests/test_search_light.py": { + "a": 15, + "d": 15 + }, + "mne/decoding/tests/test_ssd.py": { + "a": 44, + "d": 10 + }, + "mne/decoding/tests/test_time_frequency.py": { + "a": 17, + "d": 6 + }, + "mne/decoding/tests/test_transformer.py": { + "a": 71, + "d": 17 + }, + "mne/decoding/time_frequency.py": { + "a": 23, + "d": 9 + }, + "mne/decoding/transformer.py": { + "a": 127, + "d": 113 + }, + "mne/time_frequency/multitaper.py": { + "a": 8, + "d": 1 + }, + "mne/time_frequency/tfr.py": { + "a": 2, + "d": 1 + }, + "mne/utils/numerics.py": { + "a": 3, + "d": 2 + }, + "tools/vulture_allowlist.py": { + "a": 2, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13067.json b/doc/sphinxext/prs/13067.json new file mode 100644 index 00000000000..e9a5c0338f2 --- /dev/null +++ b/doc/sphinxext/prs/13067.json @@ -0,0 +1,27 @@ +{ + "merge_commit_sha": "8b9fc973e0bdaca9a5ba0c9333637722ed323633", + "authors": [ + { + "n": "Thomas S. Binns", + "e": "t.s.binns@outlook.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/changes/devel/13067.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/time_frequency/tests/test_tfr.py": { + "a": 12, + "d": 7 + }, + "mne/time_frequency/tfr.py": { + "a": 26, + "d": 25 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13069.json b/doc/sphinxext/prs/13069.json new file mode 100644 index 00000000000..05319605699 --- /dev/null +++ b/doc/sphinxext/prs/13069.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "bd8c318537ffcabf4c5fadd4347ec5068bb91b67", + "authors": [ + { + "n": "Simon Kern", + "e": null + }, + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/changes/devel/13069.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/conftest.py": { + "a": 1, + "d": 0 + }, + "mne/io/edf/edf.py": { + "a": 6, + "d": 3 + }, + "mne/io/edf/tests/test_edf.py": { + "a": 18, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13070.json b/doc/sphinxext/prs/13070.json new file mode 100644 index 00000000000..635bf15d76c --- /dev/null +++ b/doc/sphinxext/prs/13070.json @@ -0,0 +1,39 @@ +{ + "merge_commit_sha": "7daeceef4a2b80f4d849ec55a72a6450020c8c0c", + "authors": [ + { + "n": "Roy Eric", + "e": null + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + }, + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + } + ], + "changes": { + "doc/changes/devel/13070.bugfix.rst": { + "a": 1, + "d": 0 + }, + "doc/changes/names.inc": { + "a": 1, + "d": 0 + }, + "mne/io/base.py": { + "a": 4, + "d": 1 + }, + "mne/io/fiff/tests/test_raw_fiff.py": { + "a": 10, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13071.json b/doc/sphinxext/prs/13071.json new file mode 100644 index 00000000000..7efed2633d3 --- /dev/null +++ b/doc/sphinxext/prs/13071.json @@ -0,0 +1,15 @@ +{ + "merge_commit_sha": "27386d7bc8240500efcfc618e2fa57f0bcea1ace", + "authors": [ + { + "n": "dependabot[bot]", + "e": "49699333+dependabot[bot]@users.noreply.github.com" + } + ], + "changes": { + ".github/workflows/autofix.yml": { + "a": 1, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13072.json b/doc/sphinxext/prs/13072.json new file mode 100644 index 00000000000..2eda6ba7134 --- /dev/null +++ b/doc/sphinxext/prs/13072.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "96d22f87acb631c7cf04f5fcf0462f0956ba6f88", + "authors": [ + { + "n": "Thomas S. Binns", + "e": "t.s.binns@outlook.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/changes/devel/13067.bugfix.rst": { + "a": 1, + "d": 0 + }, + "mne/export/_export.py": { + "a": 9, + "d": 2 + }, + "mne/time_frequency/tests/test_tfr.py": { + "a": 0, + "d": 16 + }, + "mne/time_frequency/tfr.py": { + "a": 33, + "d": 19 + }, + "mne/utils/docs.py": { + "a": 8, + "d": 5 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13073.json b/doc/sphinxext/prs/13073.json new file mode 100644 index 00000000000..adf94fea4c7 --- /dev/null +++ b/doc/sphinxext/prs/13073.json @@ -0,0 +1,27 @@ +{ + "merge_commit_sha": "99e985845759005c2d809c705241918589aa2a0e", + "authors": [ + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + ".github/workflows/check_changelog.yml": { + "a": 3, + "d": 0 + }, + ".github/workflows/circle_artifacts.yml": { + "a": 3, + "d": 0 + }, + ".pre-commit-config.yaml": { + "a": 2, + "d": 2 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13075.json b/doc/sphinxext/prs/13075.json new file mode 100644 index 00000000000..94c1ec32100 --- /dev/null +++ b/doc/sphinxext/prs/13075.json @@ -0,0 +1,39 @@ +{ + "merge_commit_sha": "f97a916bc79942df1cc5578ed98cddbcf1aef907", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/changes/devel/12656.bugfix.rst": { + "a": 1, + "d": 1 + }, + "mne/export/_export.py": { + "a": 5, + "d": 3 + }, + "mne/forward/tests/test_make_forward.py": { + "a": 1, + "d": 1 + }, + "mne/preprocessing/tests/test_fine_cal.py": { + "a": 1, + "d": 1 + }, + "mne/utils/docs.py": { + "a": 8, + "d": 5 + }, + "tools/github_actions_dependencies.sh": { + "a": 1, + "d": 1 + }, + "tools/github_actions_env_vars.sh": { + "a": 1, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13076.json b/doc/sphinxext/prs/13076.json new file mode 100644 index 00000000000..bf05a88b721 --- /dev/null +++ b/doc/sphinxext/prs/13076.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "6028982a3e34bf843d4694f60565a0fbb821ed2e", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/sphinxext/mne_doc_utils.py": { + "a": 2, + "d": 0 + }, + "doc/sphinxext/related_software.py": { + "a": 16, + "d": 8 + }, + "mne/viz/backends/_pyvista.py": { + "a": 0, + "d": 1 + }, + "tools/circleci_dependencies.sh": { + "a": 1, + "d": 1 + }, + "tutorials/intro/70_report.py": { + "a": 5, + "d": 5 + }, + "tutorials/inverse/20_dipole_fit.py": { + "a": 1, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13077.json b/doc/sphinxext/prs/13077.json new file mode 100644 index 00000000000..5ba2d52db90 --- /dev/null +++ b/doc/sphinxext/prs/13077.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "4dc9fe44df5c0367ef9d250b18214832c90196fb", + "authors": [ + { + "n": "Lumberbot (aka Jack)", + "e": null + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/sphinxext/mne_doc_utils.py": { + "a": 2, + "d": 0 + }, + "doc/sphinxext/related_software.py": { + "a": 16, + "d": 8 + }, + "tools/circleci_dependencies.sh": { + "a": 1, + "d": 1 + }, + "tutorials/intro/70_report.py": { + "a": 5, + "d": 5 + }, + "tutorials/inverse/20_dipole_fit.py": { + "a": 1, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13081.json b/doc/sphinxext/prs/13081.json new file mode 100644 index 00000000000..2d430f5a953 --- /dev/null +++ b/doc/sphinxext/prs/13081.json @@ -0,0 +1,31 @@ +{ + "merge_commit_sha": "d596b6ddedac0680da889c5305ab4ab5d7626743", + "authors": [ + { + "n": "Lumberbot (aka Jack)", + "e": null + }, + { + "n": "Roy Eric", + "e": "139973278+Randomidous@users.noreply.github.com" + } + ], + "changes": { + "doc/changes/devel/13070.bugfix.rst": { + "a": 1, + "d": 0 + }, + "doc/changes/names.inc": { + "a": 1, + "d": 0 + }, + "mne/io/base.py": { + "a": 4, + "d": 1 + }, + "mne/io/fiff/tests/test_raw_fiff.py": { + "a": 10, + "d": 0 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13082.json b/doc/sphinxext/prs/13082.json new file mode 100644 index 00000000000..1eb2d448059 --- /dev/null +++ b/doc/sphinxext/prs/13082.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "3db12ff5357d4d6666f3d2257e91cee877e83234", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + }, + { + "n": "autofix-ci[bot]", + "e": "114827586+autofix-ci[bot]@users.noreply.github.com" + } + ], + "changes": { + "doc/changes/devel/13082.bugfix.rst": { + "a": 1, + "d": 0 + }, + "examples/preprocessing/movement_detection.py": { + "a": 1, + "d": 0 + }, + "mne/conftest.py": { + "a": 1, + "d": 0 + }, + "mne/viz/_brain/tests/test_brain.py": { + "a": 0, + "d": 3 + }, + "mne/viz/backends/_pyvista.py": { + "a": 1, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13084.json b/doc/sphinxext/prs/13084.json new file mode 100644 index 00000000000..c08d21af74b --- /dev/null +++ b/doc/sphinxext/prs/13084.json @@ -0,0 +1,35 @@ +{ + "merge_commit_sha": "d3d0bf520624a4eea2e8d34a927284c4bb19b87a", + "authors": [ + { + "n": "Lumberbot (aka Jack)", + "e": null + }, + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "doc/changes/devel/13082.bugfix.rst": { + "a": 1, + "d": 0 + }, + "examples/preprocessing/movement_detection.py": { + "a": 1, + "d": 0 + }, + "mne/conftest.py": { + "a": 1, + "d": 0 + }, + "mne/viz/_brain/tests/test_brain.py": { + "a": 0, + "d": 3 + }, + "mne/viz/backends/_pyvista.py": { + "a": 1, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13086.json b/doc/sphinxext/prs/13086.json new file mode 100644 index 00000000000..261d6a9a0e3 --- /dev/null +++ b/doc/sphinxext/prs/13086.json @@ -0,0 +1,23 @@ +{ + "merge_commit_sha": "631ddb3e9da67456947e23c6a070aa869d853a26", + "authors": [ + { + "n": "Marijn van Vliet", + "e": "w.m.vanvliet@gmail.com" + }, + { + "n": "Richard Höchenberger", + "e": "richard.hoechenberger@gmail.com" + } + ], + "changes": { + "mne/viz/_mpl_figure.py": { + "a": 2, + "d": 2 + }, + "tutorials/epochs/60_make_fixed_length_epochs.py": { + "a": 1, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13087.json b/doc/sphinxext/prs/13087.json new file mode 100644 index 00000000000..1606b92a8c4 --- /dev/null +++ b/doc/sphinxext/prs/13087.json @@ -0,0 +1,19 @@ +{ + "merge_commit_sha": "4037ead8fe9ec27d7342263c574b99a7bc537104", + "authors": [ + { + "n": "Marijn van Vliet", + "e": "w.m.vanvliet@gmail.com" + } + ], + "changes": { + "mne/viz/_figure.py": { + "a": 1, + "d": 1 + }, + "mne/viz/_mpl_figure.py": { + "a": 2, + "d": 2 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13088.json b/doc/sphinxext/prs/13088.json new file mode 100644 index 00000000000..f23c44206b3 --- /dev/null +++ b/doc/sphinxext/prs/13088.json @@ -0,0 +1,23 @@ +{ + "merge_commit_sha": "45fb777fbc53c88888032d40a14940c985079a93", + "authors": [ + { + "n": "pre-commit-ci[bot]", + "e": "66853113+pre-commit-ci[bot]@users.noreply.github.com" + }, + { + "n": "Daniel McCloy", + "e": "dan@mccloy.info" + } + ], + "changes": { + ".pre-commit-config.yaml": { + "a": 2, + "d": 2 + }, + "mne/_fiff/proj.py": { + "a": 1, + "d": 1 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/prs/13089.json b/doc/sphinxext/prs/13089.json new file mode 100644 index 00000000000..b0ad8b2fce2 --- /dev/null +++ b/doc/sphinxext/prs/13089.json @@ -0,0 +1,23 @@ +{ + "merge_commit_sha": "715540a823ae5dec335bee0b2499f1f7183c19c4", + "authors": [ + { + "n": "Eric Larson", + "e": "larson.eric.d@gmail.com" + } + ], + "changes": { + "mne/viz/utils.py": { + "a": 1, + "d": 1 + }, + "tutorials/epochs/60_make_fixed_length_epochs.py": { + "a": 4, + "d": 5 + }, + "tutorials/evoked/10_evoked_overview.py": { + "a": 5, + "d": 6 + } + } +} \ No newline at end of file diff --git a/doc/sphinxext/related_software.py b/doc/sphinxext/related_software.py index 2548725390a..a739e545f8f 100644 --- a/doc/sphinxext/related_software.py +++ b/doc/sphinxext/related_software.py @@ -85,6 +85,10 @@ "Home-page": "https://eeg-positions.readthedocs.io", "Summary": "Compute and plot standard EEG electrode positions.", }, + "mne-faster": { + "Home-page": "https://github.com/wmvanvliet/mne-faster", + "Summary": "MNE-FASTER: automatic bad channel/epoch/component detection.", # noqa: E501 + }, "mne-features": { "Home-page": "https://mne.tools/mne-features", "Summary": "MNE-Features software for extracting features from multivariate time series", # noqa: E501 diff --git a/examples/io/read_xdf.py b/examples/io/read_xdf.py index 8ed69a3289b..ee8524702a5 100644 --- a/examples/io/read_xdf.py +++ b/examples/io/read_xdf.py @@ -7,8 +7,7 @@ Here we read some sample XDF data. Although we do not analyze it here, this recording is of a short parallel auditory response (pABR) experiment -:footcite:`PolonenkoMaddox2019` and was provided by the `Maddox Lab -`__. +:footcite:`PolonenkoMaddox2019` and was provided by the `Maddox Lab `_. """ # Authors: Clemens Brunner # Eric Larson diff --git a/examples/preprocessing/interpolate_to.py b/examples/preprocessing/interpolate_to.py new file mode 100644 index 00000000000..b97a7251cbb --- /dev/null +++ b/examples/preprocessing/interpolate_to.py @@ -0,0 +1,81 @@ +""" +.. _ex-interpolate-to-any-montage: + +====================================================== +Interpolate EEG data to any montage +====================================================== + +This example demonstrates how to interpolate EEG channels to match a given montage. +This can be useful for standardizing +EEG channel layouts across different datasets (see :footcite:`MellotEtAl2024`). + +- Using the field interpolation for EEG data. +- Using the target montage "biosemi16". + +In this example, the data from the original EEG channels will be +interpolated onto the positions defined by the "biosemi16" montage. +""" + +# Authors: Antoine Collas +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +import matplotlib.pyplot as plt + +import mne +from mne.channels import make_standard_montage +from mne.datasets import sample + +print(__doc__) +ylim = (-10, 10) + +# %% +# Load EEG data +data_path = sample.data_path() +eeg_file_path = data_path / "MEG" / "sample" / "sample_audvis-ave.fif" +evoked = mne.read_evokeds(eeg_file_path, condition="Left Auditory", baseline=(None, 0)) + +# Select only EEG channels +evoked.pick("eeg") + +# Plot the original EEG layout +evoked.plot(exclude=[], picks="eeg", ylim=dict(eeg=ylim)) + +# %% +# Define the target montage +standard_montage = make_standard_montage("biosemi16") + +# %% +# Use interpolate_to to project EEG data to the standard montage +evoked_interpolated_spline = evoked.copy().interpolate_to( + standard_montage, method="spline" +) + +# Plot the interpolated EEG layout +evoked_interpolated_spline.plot(exclude=[], picks="eeg", ylim=dict(eeg=ylim)) + +# %% +# Use interpolate_to to project EEG data to the standard montage +evoked_interpolated_mne = evoked.copy().interpolate_to(standard_montage, method="MNE") + +# Plot the interpolated EEG layout +evoked_interpolated_mne.plot(exclude=[], picks="eeg", ylim=dict(eeg=ylim)) + +# %% +# Comparing before and after interpolation +fig, axs = plt.subplots(3, 1, figsize=(8, 6), constrained_layout=True) +evoked.plot(exclude=[], picks="eeg", axes=axs[0], show=False, ylim=dict(eeg=ylim)) +axs[0].set_title("Original EEG Layout") +evoked_interpolated_spline.plot( + exclude=[], picks="eeg", axes=axs[1], show=False, ylim=dict(eeg=ylim) +) +axs[1].set_title("Interpolated to Standard 1020 Montage using spline interpolation") +evoked_interpolated_mne.plot( + exclude=[], picks="eeg", axes=axs[2], show=False, ylim=dict(eeg=ylim) +) +axs[2].set_title("Interpolated to Standard 1020 Montage using MNE interpolation") + +# %% +# References +# ---------- +# .. footbibliography:: diff --git a/examples/preprocessing/movement_detection.py b/examples/preprocessing/movement_detection.py index 9bcac562588..dd468feb464 100644 --- a/examples/preprocessing/movement_detection.py +++ b/examples/preprocessing/movement_detection.py @@ -81,6 +81,7 @@ ############################################################################## # After checking the annotated movement artifacts, calculate the new transform # and plot it: + new_dev_head_t = compute_average_dev_head_t(raw, head_pos) raw.info["dev_head_t"] = new_dev_head_t fig = mne.viz.plot_alignment( diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index dbe1a407731..76d78782ac8 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -1935,15 +1935,24 @@ def _repr_html_(self): info_template = _get_html_template("repr", "info.html.jinja") return info_template.render(info=self) - def save(self, fname): + @verbose + def save(self, fname, *, overwrite=False, verbose=None): """Write measurement info in fif file. Parameters ---------- fname : path-like The name of the file. Should end by ``'-info.fif'``. + %(overwrite)s + + .. versionadded:: 1.10 + %(verbose)s + + See Also + -------- + mne.io.write_info """ - write_info(fname, self) + write_info(fname, self, overwrite=overwrite) def _simplify_info(info, *, keep=()): diff --git a/mne/_fiff/proj.py b/mne/_fiff/proj.py index d6ec108e34d..aa010085904 100644 --- a/mne/_fiff/proj.py +++ b/mne/_fiff/proj.py @@ -1100,7 +1100,7 @@ def _has_eeg_average_ref_proj( def _needs_eeg_average_ref_proj(info): - """Determine if the EEG needs an averge EEG reference. + """Determine if the EEG needs an average EEG reference. This returns True if no custom reference has been applied and no average reference projection is present in the list of projections. diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index a38ecaade50..9c0639a830c 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -975,7 +975,7 @@ def test_field_round_trip(tmp_path): meas_date=_stamp_to_dt((1, 2)), ) fname = tmp_path / "temp-info.fif" - write_info(fname, info) + info.save(fname) info_read = read_info(fname) assert_object_equal(info, info_read) with pytest.raises(TypeError, match="datetime"): diff --git a/mne/channels/channels.py b/mne/channels/channels.py index bf9e58f2819..d0e57eecb5f 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -41,7 +41,7 @@ pick_info, pick_types, ) -from .._fiff.proj import setup_proj +from .._fiff.proj import _has_eeg_average_ref_proj, setup_proj from .._fiff.reference import add_reference_channels, set_eeg_reference from .._fiff.tag import _rename_list from ..bem import _check_origin @@ -960,6 +960,162 @@ def interpolate_bads( return self + def interpolate_to(self, sensors, origin="auto", method="spline", reg=0.0): + """Interpolate EEG data onto a new montage. + + .. warning:: + Be careful, only EEG channels are interpolated. Other channel types are + not interpolated. + + Parameters + ---------- + sensors : DigMontage + The target montage containing channel positions to interpolate onto. + origin : array-like, shape (3,) | str + Origin of the sphere in the head coordinate frame and in meters. + Can be ``'auto'`` (default), which means a head-digitization-based + origin fit. + method : str + Method to use for EEG channels. + Supported methods are 'spline' (default) and 'MNE'. + reg : float + The regularization parameter for the interpolation method + (only used when the method is 'spline'). + + Returns + ------- + inst : instance of Raw, Epochs, or Evoked + The instance with updated channel locations and data. + + Notes + ----- + This method is useful for standardizing EEG layouts across datasets. + However, some attributes may be lost after interpolation. + + .. versionadded:: 1.10.0 + """ + from ..epochs import BaseEpochs, EpochsArray + from ..evoked import Evoked, EvokedArray + from ..forward._field_interpolation import _map_meg_or_eeg_channels + from ..io import RawArray + from ..io.base import BaseRaw + from .interpolation import _make_interpolation_matrix + from .montage import DigMontage + + # Check that the method option is valid. + _check_option("method", method, ["spline", "MNE"]) + _validate_type(sensors, DigMontage, "sensors") + + # Get target positions from the montage + ch_pos = sensors.get_positions().get("ch_pos", {}) + target_ch_names = list(ch_pos.keys()) + if not target_ch_names: + raise ValueError( + "The provided sensors configuration has no channel positions." + ) + + # Get original channel order + orig_names = self.info["ch_names"] + + # Identify EEG channel + picks_good_eeg = pick_types(self.info, meg=False, eeg=True, exclude="bads") + if len(picks_good_eeg) == 0: + raise ValueError("No good EEG channels available for interpolation.") + # Also get the full list of EEG channel indices (including bad channels) + picks_remove_eeg = pick_types(self.info, meg=False, eeg=True, exclude=[]) + eeg_names_orig = [orig_names[i] for i in picks_remove_eeg] + + # Identify non-EEG channels in original order + non_eeg_names_ordered = [ch for ch in orig_names if ch not in eeg_names_orig] + + # Create destination info for new EEG channels + sfreq = self.info["sfreq"] + info_interp = create_info( + ch_names=target_ch_names, + sfreq=sfreq, + ch_types=["eeg"] * len(target_ch_names), + ) + info_interp.set_montage(sensors) + info_interp["bads"] = [ch for ch in self.info["bads"] if ch in target_ch_names] + # Do not assign "projs" directly. + + # Compute the interpolation mapping + if method == "spline": + origin_val = _check_origin(origin, self.info) + pos_from = self.info._get_channel_positions(picks_good_eeg) - origin_val + pos_to = np.stack(list(ch_pos.values()), axis=0) + + def _check_pos_sphere(pos): + d = np.linalg.norm(pos, axis=-1) + d_norm = np.mean(d / np.mean(d)) + if np.abs(1.0 - d_norm) > 0.1: + warn("Your spherical fit is poor; interpolation may be inaccurate.") + + _check_pos_sphere(pos_from) + _check_pos_sphere(pos_to) + mapping = _make_interpolation_matrix(pos_from, pos_to, alpha=reg) + + else: + assert method == "MNE" + info_eeg = pick_info(self.info, picks_good_eeg) + # If the original info has an average EEG reference projector but + # the destination info does not, + # update info_interp via a temporary RawArray. + if _has_eeg_average_ref_proj(self.info) and not _has_eeg_average_ref_proj( + info_interp + ): + # Create dummy data: shape (n_channels, 1) + temp_data = np.zeros((len(info_interp["ch_names"]), 1)) + temp_raw = RawArray(temp_data, info_interp, first_samp=0) + # Using the public API, add an average reference projector. + temp_raw.set_eeg_reference( + ref_channels="average", projection=True, verbose=False + ) + # Extract the updated info. + info_interp = temp_raw.info + mapping = _map_meg_or_eeg_channels( + info_eeg, info_interp, mode="accurate", origin=origin + ) + + # Interpolate EEG data + data_good = self.get_data(picks=picks_good_eeg) + data_interp = mapping @ data_good + + # Create a new instance for the interpolated EEG channels + # TODO: Creating a new instance leads to a loss of information. + # We should consider updating the existing instance in the future + # by 1) drop channels, 2) add channels, 3) re-order channels. + if isinstance(self, BaseRaw): + inst_interp = RawArray(data_interp, info_interp, first_samp=self.first_samp) + elif isinstance(self, BaseEpochs): + inst_interp = EpochsArray(data_interp, info_interp) + else: + assert isinstance(self, Evoked) + inst_interp = EvokedArray(data_interp, info_interp) + + # Merge only if non-EEG channels exist + if not non_eeg_names_ordered: + return inst_interp + + inst_non_eeg = self.copy().pick(non_eeg_names_ordered).load_data() + inst_out = inst_non_eeg.add_channels([inst_interp], force_update_info=True) + + # Reorder channels + # Insert the entire new EEG block at the position of the first EEG channel. + orig_names_arr = np.array(orig_names) + mask_eeg = np.isin(orig_names_arr, eeg_names_orig) + if mask_eeg.any(): + first_eeg_index = np.where(mask_eeg)[0][0] + pre = orig_names_arr[:first_eeg_index] + new_eeg = np.array(info_interp["ch_names"]) + post = orig_names_arr[first_eeg_index:] + post = post[~np.isin(orig_names_arr[first_eeg_index:], eeg_names_orig)] + new_order = np.concatenate((pre, new_eeg, post)).tolist() + else: + new_order = orig_names + inst_out.reorder_channels(new_order) + return inst_out + @verbose def rename_channels(info, mapping, allow_duplicates=False, *, verbose=None): diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index a881a41edcc..62c7d79e3eb 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -12,7 +12,7 @@ from mne import Epochs, pick_channels, pick_types, read_events from mne._fiff.constants import FIFF from mne._fiff.proj import _has_eeg_average_ref_proj -from mne.channels import make_dig_montage +from mne.channels import make_dig_montage, make_standard_montage from mne.channels.interpolation import _make_interpolation_matrix from mne.datasets import testing from mne.io import RawArray, read_raw_ctf, read_raw_fif, read_raw_nirx @@ -439,3 +439,85 @@ def test_method_str(): raw.interpolate_bads(method="spline") raw.pick("eeg", exclude=()) raw.interpolate_bads(method="spline") + + +@pytest.mark.parametrize("montage_name", ["biosemi16", "standard_1020"]) +@pytest.mark.parametrize("method", ["spline", "MNE"]) +@pytest.mark.parametrize("data_type", ["raw", "epochs", "evoked"]) +def test_interpolate_to_eeg(montage_name, method, data_type): + """Test the interpolate_to method for EEG for raw, epochs, and evoked.""" + # Load EEG data + raw, epochs_eeg = _load_data("eeg") + epochs_eeg = epochs_eeg.copy() + + # Load data for raw + raw.load_data() + + # Create a target montage + montage = make_standard_montage(montage_name) + + # Prepare data to interpolate to + if data_type == "raw": + inst = raw.copy() + elif data_type == "epochs": + inst = epochs_eeg.copy() + elif data_type == "evoked": + inst = epochs_eeg.average() + shape = list(inst._data.shape) + orig_total = len(inst.info["ch_names"]) + n_eeg_orig = len(pick_types(inst.info, eeg=True)) + + # Assert first and last channels are not EEG + if data_type == "raw": + ch_types = inst.get_channel_types() + assert ch_types[0] != "eeg" + assert ch_types[-1] != "eeg" + + # Record the names and data of the first and last channels. + if data_type == "raw": + first_name = inst.info["ch_names"][0] + last_name = inst.info["ch_names"][-1] + data_first = inst._data[..., 0, :].copy() + data_last = inst._data[..., -1, :].copy() + + # Interpolate the EEG channels. + inst_interp = inst.copy().interpolate_to(montage, method=method) + + # Check that the new channel names include the montage channels. + assert set(montage.ch_names).issubset(set(inst_interp.info["ch_names"])) + # Check that the overall channel order is changed. + assert inst.info["ch_names"] != inst_interp.info["ch_names"] + + # Check that the data shape is as expected. + new_nchan_expected = orig_total - n_eeg_orig + len(montage.ch_names) + expected_shape = (new_nchan_expected, shape[-1]) + if len(shape) == 3: + expected_shape = (shape[0],) + expected_shape + assert inst_interp._data.shape == expected_shape + + # Verify that the first and last channels retain their positions. + if data_type == "raw": + assert inst_interp.info["ch_names"][0] == first_name + assert inst_interp.info["ch_names"][-1] == last_name + + # Verify that the data for the first and last channels is unchanged. + if data_type == "raw": + np.testing.assert_allclose( + inst_interp._data[..., 0, :], + data_first, + err_msg="Data for the first non-EEG channel has changed.", + ) + np.testing.assert_allclose( + inst_interp._data[..., -1, :], + data_last, + err_msg="Data for the last non-EEG channel has changed.", + ) + + # Validate that bad channels are carried over. + # Mark the first non eeg channel as bad + all_ch = inst_interp.info["ch_names"] + eeg_ch = [all_ch[i] for i in pick_types(inst_interp.info, eeg=True)] + bads = [ch for ch in all_ch if ch not in eeg_ch][:1] + inst.info["bads"] = bads + inst_interp = inst.copy().interpolate_to(montage, method=method) + assert inst_interp.info["bads"] == bads diff --git a/mne/conftest.py b/mne/conftest.py index 8a4586067b3..fabf9f93e5a 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -6,6 +6,7 @@ import inspect import os import os.path as op +import platform import re import shutil import sys @@ -174,6 +175,7 @@ def pytest_configure(config: pytest.Config): # pandas ignore:\n*Pyarrow will become a required dependency of pandas.*:DeprecationWarning ignore:np\.find_common_type is deprecated.*:DeprecationWarning + ignore:Python binding for RankQuantileOptions.*: # pyvista <-> NumPy 2.0 ignore:__array_wrap__ must accept context and return_scalar arguments.*:DeprecationWarning # nibabel <-> NumPy 2.0 @@ -189,6 +191,8 @@ def pytest_configure(config: pytest.Config): ignore:Starting field name with a underscore.*: # joblib ignore:process .* is multi-threaded, use of fork/exec.*:DeprecationWarning + # sklearn + ignore:Python binding for RankQuantileOptions.*:RuntimeWarning """ # noqa: E501 for warning_line in warning_lines.split("\n"): warning_line = warning_line.strip() @@ -284,9 +288,10 @@ def __init__(self, exception_handler=None, signals=None): @pytest.fixture(scope="session") def azure_windows(): """Determine if running on Azure Windows.""" - return os.getenv( - "AZURE_CI_WINDOWS", "false" - ).lower() == "true" and sys.platform.startswith("win") + return ( + os.getenv("AZURE_CI_WINDOWS", "false").lower() == "true" + and platform.system() == "Windows" + ) @pytest.fixture(scope="function") diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 07be6ab388a..f9dfffccd55 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -32,18 +32,18 @@ class RawBrainVision(BaseRaw): ---------- vhdr_fname : path-like Path to the EEG header file. - eog : list or tuple - Names of channels or list of indices that should be designated - EOG channels. Values should correspond to the header file. - Default is ``('HEOGL', 'HEOGR', 'VEOGb')``. - misc : list or tuple of str | ``'auto'`` - Names of channels or list of indices that should be designated - MISC channels. Values should correspond to the electrodes - in the header file. If ``'auto'``, units in header file are used for - inferring misc channels. Default is ``'auto'``. + eog : list of (int | str) | tuple of (int | str) + Names of channels or list of indices that should be designated EOG channels. + Values should correspond to the header file. Default is ``('HEOGL', 'HEOGR', + 'VEOGb')``. + misc : list of (int | str) | tuple of (int | str) | ``'auto'`` + Names of channels or list of indices that should be designated MISC channels. + Values should correspond to the electrodes in the header file. If ``'auto'``, + units in header file are used for inferring misc channels. Default is + ``'auto'``. scale : float - The scaling factor for EEG data. Unless specified otherwise by - header file, units are in microvolts. Default scale factor is 1. + The scaling factor for EEG data. Unless specified otherwise by header file, + units are in microvolts. Default scale factor is 1. ignore_marker_types : bool If ``True``, ignore marker types and only use marker descriptions. Default is ``False``. @@ -64,10 +64,9 @@ class RawBrainVision(BaseRaw): Notes ----- If the BrainVision header file contains impedance measurements, these may be - accessed using ``raw.impedances`` after reading using this function. However, - this attribute will NOT be available after a save and re-load of the data. - That is, it is only available when reading data directly from the BrainVision - header file. + accessed using ``raw.impedances`` after reading using this function. However, this + attribute will NOT be available after a save and re-load of the data. That is, it is + only available when reading data directly from the BrainVision header file. BrainVision markers consist of a type and a description (in addition to other fields like onset and duration). In contrast, annotations in MNE only have a description. @@ -75,6 +74,10 @@ class RawBrainVision(BaseRaw): converted to an annotation "Stimulus/S 1" by default. If you want to ignore the type and instead only use the description, set ``ignore_marker_types=True``, which will convert the same marker to an annotation "S 1". + + The first marker in a BrainVision file is usually a "New Segment" marker, which + contains the recording time. This time is stored in the ``info['meas_date']`` + attribute of the returned object and is not converted to an annotation. """ _extra_attributes = ("impedances",) @@ -110,7 +113,7 @@ def __init__( if isinstance(fmt, dict): # ASCII, this will be slow :( if order == "F": # multiplexed, channels in columns n_skip = 0 - for ii in range(int(fmt["skiplines"])): + for _ in range(int(fmt["skiplines"])): n_skip += len(f.readline()) offsets = np.cumsum([n_skip] + [len(line) for line in f]) n_samples = len(offsets) - 1 @@ -183,9 +186,8 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): line = fid.readline().decode("ASCII") line = line.strip() - # Not sure why we special-handle the "," character here, - # but let's just keep this for historical and backward- - # compat reasons + # Not sure why we special-handle the "," character here, but let's + # just keep this for historical and backward- compat reasons if ( isinstance(fmt, dict) and "decimalsymbol" in fmt @@ -224,69 +226,66 @@ def _read_segments_c(raw, data, idx, fi, start, stop, cals, mult): _mult_cal_one(data, block, idx, cals, mult) -def _read_mrk(fname, ignore_marker_types=False): +def _read_mrk(fname): """Read annotations from a vmrk/amrk file. Parameters ---------- fname : str vmrk/amrk file to be read. - ignore_marker_types : bool - If True, ignore marker types and only use marker descriptions. Default is False. Returns ------- - onset : array, shape (n_annots,) + onset : list of float The onsets in seconds. - duration : array, shape (n_annots,) + duration : list of float The onsets in seconds. - description : array, shape (n_annots,) - The description of each annotation. + type_ : list of str + The marker types. + description : list of str + The marker descriptions. date_str : str - The recording time as a string. Defaults to empty string if no - recording time is found. + The recording time. Defaults to empty string if no recording time is found. """ # read marker file with open(fname, "rb") as fid: txt = fid.read() - # we don't actually need to know the coding for the header line. - # the characters in it all belong to ASCII and are thus the - # same in Latin-1 and UTF-8 + # we don't actually need to know the encoding for the header line. the characters in + # it all belong to ASCII and are thus the same in Latin-1 and UTF-8 header = txt.decode("ascii", "ignore").split("\n")[0].strip() _check_bv_version(header, "marker") - # although the markers themselves are guaranteed to be ASCII (they - # consist of numbers and a few reserved words), we should still - # decode the file properly here because other (currently unused) - # blocks, such as that the filename are specifying are not - # guaranteed to be ASCII. + # although the markers themselves are guaranteed to be ASCII (they consist of + # numbers and a few reserved words), we should still decode the file properly here + # because other (currently unused) blocks are not guaranteed to be ASCII try: - # if there is an explicit codepage set, use it - # we pretend like it's ascii when searching for the codepage + # if there is an explicit codepage set, use it; we pretend like it's ASCII when + # searching for the codepage cp_setting = re.search( "Codepage=(.+)", txt.decode("ascii", "ignore"), re.IGNORECASE & re.MULTILINE ) codepage = "utf-8" if cp_setting: codepage = cp_setting.group(1).strip() - # BrainAmp Recorder also uses ANSI codepage - # an ANSI codepage raises a LookupError exception - # python recognize ANSI decoding as cp1252 + # BrainAmp Recorder also uses ANSI codepage; an ANSI codepage raises a + # LookupError exception; Python recognize ANSI decoding as cp1252 if codepage == "ANSI": codepage = "cp1252" txt = txt.decode(codepage) except UnicodeDecodeError: - # if UTF-8 (new standard) or explicit codepage setting fails, - # fallback to Latin-1, which is Windows default and implicit - # standard in older recordings + # if UTF-8 (new standard) or explicit codepage setting fails, fallback to + # Latin-1, which is Windows default and implicit standard in older recordings txt = txt.decode("latin-1") # extract Marker Infos block + onset, duration, type_, description = [], [], [], [] + date_str = "" + m = re.search(r"\[Marker Infos\]", txt, re.IGNORECASE) if not m: - return np.array(list()), np.array(list()), np.array(list()), "" + return onset, duration, type_, description, date_str mk_txt = txt[m.end() :] m = re.search(r"^\[.*\]$", mk_txt) @@ -295,29 +294,25 @@ def _read_mrk(fname, ignore_marker_types=False): # extract event information items = re.findall(r"^Mk\d+=(.*)", mk_txt, re.MULTILINE) - onset, duration, description = list(), list(), list() - date_str = "" for info in items: info_data = info.split(",") mtype, mdesc, this_onset, this_duration = info_data[:4] - # commas in mtype and mdesc are handled as "\1". convert back to comma + # commas in mtype and mdesc are handled as "\1", convert back to comma mtype = mtype.replace(r"\1", ",") mdesc = mdesc.replace(r"\1", ",") if date_str == "" and len(info_data) == 5 and mtype == "New Segment": - # to handle the origin of time and handle the presence of multiple - # New Segment annotations. We only keep the first one that is - # different from an empty string for date_str. + # to handle the origin of time and handle the presence of multiple New + # Segment annotations, we only keep the first one that is different from an + # empty string for date_str date_str = info_data[-1] this_duration = int(this_duration) if this_duration.isdigit() else 0 duration.append(this_duration) onset.append(int(this_onset) - 1) # BV is 1-indexed, not 0-indexed - if not ignore_marker_types: - description.append(mtype + "/" + mdesc) - else: - description.append(mdesc) + type_.append(mtype) + description.append(mdesc) - return np.array(onset), np.array(duration), np.array(description), date_str + return onset, duration, type_, description, date_str def _read_annotations_brainvision(fname, sfreq="auto", ignore_marker_types=False): @@ -344,9 +339,7 @@ def _read_annotations_brainvision(fname, sfreq="auto", ignore_marker_types=False annotations : instance of Annotations The annotations present in the file. """ - onset, duration, description, date_str = _read_mrk( - fname, ignore_marker_types=ignore_marker_types - ) + onset, duration, type_, description, date_str = _read_mrk(fname) orig_time = _str_to_meas_date(date_str) if sfreq == "auto": @@ -358,8 +351,17 @@ def _read_annotations_brainvision(fname, sfreq="auto", ignore_marker_types=False _, _, _, info = _aux_hdr_info(hdr_fname) sfreq = info["sfreq"] + # skip the first "New Segment" marker (as it only contains the recording time) + if len(type_) > 0 and type_[0] == "New Segment": + onset = onset[1:] + duration = duration[1:] + type_ = type_[1:] + description = description[1:] + onset = np.array(onset, dtype=float) / sfreq duration = np.array(duration, dtype=float) / sfreq + if not ignore_marker_types: + description = [f"{t}/{d}" for t, d in zip(type_, description)] annotations = Annotations( onset=onset, duration=duration, description=description, orig_time=orig_time ) @@ -372,8 +374,8 @@ def _check_bv_version(header, kind): "MNE-Python currently only supports %s versions 1.0 and 2.0, got unparsable " "%r. Contact MNE-Python developers for support." ) - # optional space, optional Core or V-Amp, optional Exchange, - # Version/Header, optional comma, 1/2 + # optional space, optional Core or V-Amp, optional Exchange, Version/Header, + # optional comma, 1/2 _data_re = r"Brain ?Vision( Core| V-Amp)? Data( Exchange)? %s File,? Version %s\.0" assert kind in ("header", "marker") @@ -416,8 +418,8 @@ def _str_to_meas_date(date_str): if date_str in ["", "0", "00000000000000000000"]: return None - # these calculations are in naive time but should be okay since - # they are relative (subtraction below) + # these calculations are in naive time but should be okay since they are relative + # (subtraction below) try: meas_date = datetime.strptime(date_str, "%Y%m%d%H%M%S%f") except ValueError as e: @@ -436,16 +438,15 @@ def _aux_hdr_info(hdr_fname): # extract the first section to resemble a cfg header = f.readline() codepage = "utf-8" - # we don't actually need to know the coding for the header line. - # the characters in it all belong to ASCII and are thus the - # same in Latin-1 and UTF-8 + # we don't actually need to know the coding for the header line; the characters + # in it all belong to ASCII and are thus the same in Latin-1 and UTF-8 header = header.decode("ascii", "ignore").strip() _check_bv_version(header, "header") settings = f.read() try: # if there is an explicit codepage set, use it - # we pretend like it's ascii when searching for the codepage + # we pretend like it's ASCII when searching for the codepage cp_setting = re.search( "Codepage=(.+)", settings.decode("ascii", "ignore"), @@ -453,16 +454,15 @@ def _aux_hdr_info(hdr_fname): ) if cp_setting: codepage = cp_setting.group(1).strip() - # BrainAmp Recorder also uses ANSI codepage - # an ANSI codepage raises a LookupError exception - # python recognize ANSI decoding as cp1252 + # BrainAmp Recorder also uses ANSI codepage; an ANSI codepage raises a + # LookupError exception; Python recognize ANSI decoding as cp1252 if codepage == "ANSI": codepage = "cp1252" settings = settings.decode(codepage) except UnicodeDecodeError: - # if UTF-8 (new standard) or explicit codepage setting fails, - # fallback to Latin-1, which is Windows default and implicit - # standard in older recordings + # if UTF-8 (new standard) or explicit codepage setting fails, fallback to + # Latin-1, which is Windows default and implicit standard in older + # recordings settings = settings.decode("latin-1") if settings.find("[Comment]") != -1: @@ -499,13 +499,12 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): Names of channels that should be designated EOG channels. Names should correspond to the header file. misc : list or tuple of str | 'auto' - Names of channels or list of indices that should be designated - MISC channels. Values should correspond to the electrodes in the - header file. If 'auto', units in header file are used for inferring - misc channels. Default is ``'auto'``. + Names of channels or list of indices that should be designated MISC channels. + Values should correspond to the electrodes in the header file. If 'auto', units + in header file are used for inferring misc channels. Default is ``'auto'``. scale : float - The scaling factor for EEG data. Unless specified otherwise by - header file, units are in microvolts. Default scale factor is 1. + The scaling factor for EEG data. Unless specified otherwise by header file, + units are in microvolts. Default scale factor is 1. Returns ------- @@ -523,16 +522,16 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): montage : DigMontage Coordinates of the channels, if present in the header file. orig_units : dict - Dictionary mapping channel names to their units as specified in - the header file. Example: {'FC1': 'nV'} + Dictionary mapping channel names to their units as specified in the header file. + Example: {'FC1': 'nV'} """ scale = float(scale) ext = op.splitext(hdr_fname)[-1] ahdr_format = ext == ".ahdr" if ext not in (".vhdr", ".ahdr"): raise OSError( - "The header file must be given to read the data, " - f"not a file with extension '{ext}'." + "The header file must be given to read the data, not a file with extension " + f"'{ext}'." ) settings, cfg, cinfostr, info = _aux_hdr_info(hdr_fname) @@ -552,9 +551,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): else: if order == "C": # channels in rows raise NotImplementedError( - "BrainVision files with ASCII data in " - "vectorized order (i.e. channels in rows" - ") are not supported yet." + "BrainVision files with ASCII data in vectorized order (i.e. channels " + "in rows) are not supported yet." ) fmt = {key: cfg.get("ASCII Infos", key) for key in cfg.options("ASCII Infos")} @@ -590,8 +588,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): n_samples = cfg.getint(cinfostr, "DataPoints") except configparser.NoOptionError: warn( - "No info on DataPoints found. Inferring number of " - "samples from the data file size." + "No info on DataPoints found. Inferring number of samples from the " + "data file size." ) with open(data_fname, "rb") as fid: fid.seek(0, 2) @@ -644,12 +642,11 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): misc = list(misc_chs.keys()) if misc == "auto" else misc - # create montage: 'Coordinates' section in VHDR/AHDR file corresponds to - # "BVEF" BrainVision Electrode File. The data are based on BrainVision - # Analyzer coordinate system: Defined between standard electrode positions: - # X-axis from T7 to T8, Y-axis from Oz to Fpz, Z-axis orthogonal from - # XY-plane through Cz, fit to a sphere if idealized (when radius=1), - # specified in mm + # create montage: 'Coordinates' section in VHDR/AHDR file corresponds to "BVEF" + # BrainVision Electrode File. The data are based on BrainVision Analyzer coordinate + # system: Defined between standard electrode positions: X-axis from T7 to T8, Y-axis + # from Oz to Fpz, Z-axis orthogonal from XY-plane through Cz, fit to a sphere if + # idealized (when radius=1), specified in mm montage = None if cfg.has_section("Coordinates"): montage_pos = list() @@ -671,8 +668,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): if (pos == 0).all() and ch_name not in list(eog) + misc: to_misc.append(ch_name) montage_pos.append(pos) - # Make a montage, normalizing from BrainVision units "mm" to "m", the - # unit used for montages in MNE + # Make a montage, normalizing from BrainVision units "mm" to "m", the unit used + # for montages in MNE montage_pos = np.array(montage_pos) / 1e3 montage = make_dig_montage( ch_pos=dict(zip(montage_names, montage_pos)), coord_frame="head" @@ -688,8 +685,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): if np.isnan(cals).any(): raise RuntimeError("Missing channel units") - # Attempts to extract filtering info from header. If not found, both are - # set to zero. + # Attempts to extract filtering info from header. If not found, both are set to + # zero. settings = settings.splitlines() idx = None @@ -703,9 +700,9 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): else: idx = None - # If software filters are active, then they override the hardware setup - # But we still want to be able to double check the channel names - # for alignment purposes, we keep track of the hardware setting idx + # If software filters are active, then they override the hardware setup; we still + # want to be able to double check the channel names for alignment purposes, we keep + # track of the hardware setting idx idx_amp = idx filter_list_has_ch_name = True @@ -716,8 +713,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): hp_col, lp_col = 1, 2 filter_list_has_ch_name = False warn( - "Online software filter detected. Using software " - "filter settings and ignoring hardware values" + "Online software filter detected. Using software filter settings " + "and ignoring hardware values" ) break else: @@ -727,19 +724,18 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): lowpass = [] highpass = [] - # for newer BV files, the unit is specified for every channel - # separated by a single space, while for older files, the unit is - # specified in the column headers + # for newer BV files, the unit is specified for every channel separated by a + # single space, while for older files, the unit is specified in the column + # headers divider = r"\s+" if "Resolution / Unit" in settings[idx]: shift = 1 # shift for unit else: shift = 0 - # Extract filter units and convert from seconds to Hz if necessary. - # this cannot be done as post-processing as the inverse t-f - # relationship means that the min/max comparisons don't make sense - # unless we know the units. + # Extract filter units and convert from seconds to Hz if necessary. this cannot + # be done as post-processing as the inverse t-f relationship means that the + # min/max comparisons don't make sense unless we know the units. # # For reasoning about the s to Hz conversion, see this reference: # `Ebersole, J. S., & Pedley, T. A. (Eds.). (2003). @@ -787,20 +783,20 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): else: heterogeneous_hp_filter = True if hp_s: - # We convert channels with disabled filters to having - # highpass relaxed / no filters + # We convert channels with disabled filters to having highpass relaxed / + # no filters highpass = [ float(filt) if filt not in ("NaN", "Off", "DC") else np.inf for filt in highpass ] info["highpass"] = np.max(np.array(highpass, dtype=np.float64)) - # Conveniently enough 1 / np.inf = 0.0, so this works for - # DC / no highpass filter + # Conveniently enough 1 / np.inf = 0.0, so this works for DC / no + # highpass filter # filter time constant t [secs] to Hz conversion: 1/2*pi*t info["highpass"] = 1.0 / (2 * np.pi * info["highpass"]) - # not exactly the cleanest use of FP, but this makes us - # more conservative in *not* warning. + # not exactly the cleanest use of FP, but this makes us more + # conservative in *not* warning. if info["highpass"] == 0.0 and len(set(highpass)) == 1: # not actually heterogeneous in effect # ... just heterogeneously disabled @@ -818,9 +814,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): if heterogeneous_hp_filter: warn( - "Channels contain different highpass filters. " - f"Lowest (weakest) filter setting ({info['highpass']:0.2f} Hz) " - "will be stored." + "Channels contain different highpass filters. Lowest (weakest) " + f"filter setting ({info['highpass']:0.2f} Hz) will be stored." ) if len(lowpass) == 0: @@ -837,8 +832,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): else: heterogeneous_lp_filter = True if lp_s: - # We convert channels with disabled filters to having - # infinitely relaxed / no filters + # We convert channels with disabled filters to having infinitely relaxed + # / no filters lowpass = [ float(filt) if filt not in ("NaN", "Off", "0") else 0.0 for filt in lowpass @@ -850,19 +845,19 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): except ZeroDivisionError: if len(set(lowpass)) == 1: - # No lowpass actually set for the weakest setting - # so we set lowpass to the Nyquist frequency + # No lowpass actually set for the weakest setting so we set + # lowpass to the Nyquist frequency info["lowpass"] = info["sfreq"] / 2.0 # not actually heterogeneous in effect # ... just heterogeneously disabled heterogeneous_lp_filter = False else: - # no lowpass filter is the weakest filter, - # but it wasn't the only filter + # no lowpass filter is the weakest filter, but it wasn't the + # only filter pass else: - # We convert channels with disabled filters to having - # infinitely relaxed / no filters + # We convert channels with disabled filters to having infinitely relaxed + # / no filters lowpass = [ float(filt) if filt not in ("NaN", "Off", "0") else np.inf for filt in lowpass @@ -870,8 +865,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): info["lowpass"] = np.max(np.array(lowpass, dtype=np.float64)) if np.isinf(info["lowpass"]): - # No lowpass actually set for the weakest setting - # so we set lowpass to the Nyquist frequency + # No lowpass actually set for the weakest setting so we set lowpass + # to the Nyquist frequency info["lowpass"] = info["sfreq"] / 2.0 if len(set(lowpass)) == 1: # not actually heterogeneous in effect @@ -879,10 +874,10 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): heterogeneous_lp_filter = False if heterogeneous_lp_filter: - # this isn't clean FP, but then again, we only want to provide - # the Nyquist hint when the lowpass filter was actually - # calculated from dividing the sampling frequency by 2, so the - # exact/direct comparison (instead of tolerance) makes sense + # this isn't clean FP, but then again, we only want to provide the + # Nyquist hint when the lowpass filter was actually calculated from + # dividing the sampling frequency by 2, so the exact/direct comparison + # (instead of tolerance) makes sense if info["lowpass"] == info["sfreq"] / 2.0: nyquist = ", Nyquist limit" else: @@ -953,29 +948,31 @@ def read_raw_brainvision( ---------- vhdr_fname : path-like Path to the EEG header file. - eog : list or tuple of str - Names of channels or list of indices that should be designated - EOG channels. Values should correspond to the header file - Default is ``('HEOGL', 'HEOGR', 'VEOGb')``. - misc : list or tuple of str | ``'auto'`` - Names of channels or list of indices that should be designated - MISC channels. Values should correspond to the electrodes in the - header file. If ``'auto'``, units in header file are used for inferring - misc channels. Default is ``'auto'``. + eog : list of (int | str) | tuple of (int | str) + Names of channels or list of indices that should be designated EOG channels. + Values should correspond to the header file Default is ``('HEOGL', 'HEOGR', + 'VEOGb')``. + misc : list of (int | str) | tuple of (int | str) | ``'auto'`` + Names of channels or list of indices that should be designated MISC channels. + Values should correspond to the electrodes in the header file. If ``'auto'``, + units in header file are used for inferring misc channels. Default is + ``'auto'``. scale : float - The scaling factor for EEG data. Unless specified otherwise by - header file, units are in microvolts. Default scale factor is 1. + The scaling factor for EEG data. Unless specified otherwise by header file, + units are in microvolts. Default scale factor is 1. ignore_marker_types : bool If ``True``, ignore marker types and only use marker descriptions. Default is ``False``. + + .. versionadded:: 1.8 %(preload)s %(verbose)s Returns ------- raw : instance of RawBrainVision - A Raw object containing BrainVision data. - See :class:`mne.io.Raw` for documentation of attributes and methods. + A Raw object containing BrainVision data. See :class:`mne.io.Raw` for + documentation of attributes and methods. See Also -------- @@ -984,10 +981,9 @@ def read_raw_brainvision( Notes ----- If the BrainVision header file contains impedance measurements, these may be - accessed using ``raw.impedances`` after reading using this function. However, - this attribute will NOT be available after a save and re-load of the data. - That is, it is only available when reading data directly from the BrainVision - header file. + accessed using ``raw.impedances`` after reading using this function. However, this + attribute will NOT be available after a save and re-load of the data. That is, it is + only available when reading data directly from the BrainVision header file. BrainVision markers consist of a type and a description (in addition to other fields like onset and duration). In contrast, annotations in MNE only have a description. @@ -995,6 +991,10 @@ def read_raw_brainvision( converted to an annotation "Stimulus/S 1" by default. If you want to ignore the type and instead only use the description, set ``ignore_marker_types=True``, which will convert the same marker to an annotation "S 1". + + The first marker in a BrainVision file is usually a "New Segment" marker, which + contains the recording time. This time is stored in the ``info['meas_date']`` + attribute of the returned object and is not converted to an annotation. """ return RawBrainVision( vhdr_fname=vhdr_fname, @@ -1070,8 +1070,8 @@ def _parse_impedance(settings, recording_date=None): impedance_unit = impedance_setting[1].lstrip("[").rstrip("]") impedance_time = None - # If we have a recording date, we can update it with the time of - # impedance measurement + # If we have a recording date, we can update it with the time of impedance + # measurement if recording_date is not None: meas_time = [int(i) for i in impedance_setting[3].split(":")] impedance_time = recording_date.replace( @@ -1081,8 +1081,8 @@ def _parse_impedance(settings, recording_date=None): microsecond=0, ) for setting in settings[idx + 1 :]: - # Parse channel impedances until we find a line that doesn't start - # with a channel name and optional +/- polarity for passive elecs + # Parse channel impedances until we find a line that doesn't start with a + # channel name and optional +/- polarity for passive elecs match = re.match(r"[ a-zA-Z0-9_+-]+:", setting) if match: channel_name = match.group().rstrip(":") diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 704af064b48..8366f15dc3a 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -665,7 +665,6 @@ def test_ignore_marker_types(): # default behavior (do not ignore marker types) raw = read_raw_brainvision(vhdr_path) expected_descriptions = [ - "New Segment/", "Stimulus/S253", "Stimulus/S255", "Event/254", @@ -685,7 +684,6 @@ def test_ignore_marker_types(): # ignore marker types raw = read_raw_brainvision(vhdr_path, ignore_marker_types=True) expected_descriptions = [ - "", "S253", "S255", "254", @@ -720,7 +718,6 @@ def test_read_vhdr_annotations_and_events(tmp_path): expected_orig_time = _stamp_to_dt((1384359243, 794232)) expected_onset_latency = np.array( [ - 0, 486.0, 496.0, 1769.0, @@ -738,7 +735,6 @@ def test_read_vhdr_annotations_and_events(tmp_path): ] ) expected_annot_description = [ - "New Segment/", "Stimulus/S253", "Stimulus/S255", "Event/254", @@ -760,7 +756,6 @@ def test_read_vhdr_annotations_and_events(tmp_path): expected_onset_latency, np.zeros_like(expected_onset_latency), [ - 99999, 253, 255, 254, @@ -782,7 +777,6 @@ def test_read_vhdr_annotations_and_events(tmp_path): .T ) expected_event_id = { - "New Segment/": 99999, "Stimulus/S253": 253, "Stimulus/S255": 255, "Event/254": 254, diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index 3aa611b4e28..10e23b811a0 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -162,7 +162,7 @@ def _get_montage_information(eeg, get_pos, *, montage_units): ) lpa, rpa, nasion = None, None, None - if hasattr(eeg, "chaninfo") and isinstance(eeg.chaninfo["nodatchans"], dict): + if hasattr(eeg, "chaninfo") and isinstance(eeg.chaninfo.get("nodatchans"), dict): nodatchans = eeg.chaninfo["nodatchans"] types = nodatchans.get("type", []) descriptions = nodatchans.get("description", []) diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index f4a01e87895..0c8bb0f4fb0 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -4291,19 +4291,20 @@ def _tfr_from_mt(x_mt, weights): Parameters ---------- - x_mt : array, shape (n_channels, n_tapers, n_freqs, n_times) + x_mt : array, shape (..., n_tapers, n_freqs, n_times) The complex-valued multitaper coefficients. weights : array, shape (n_tapers, n_freqs) The weights to use to combine the tapered estimates. Returns ------- - tfr : array, shape (n_channels, n_freqs, n_times) + tfr : array, shape (..., n_freqs, n_times) The time-frequency power estimates. """ - weights = weights[np.newaxis, :, :, np.newaxis] # add singleton channel & time dims + # add singleton dim for time and any dims preceding the tapers + weights = weights[..., np.newaxis] tfr = weights * x_mt tfr *= tfr.conj() - tfr = tfr.real.sum(axis=1) - tfr *= 2 / (weights * weights.conj()).real.sum(axis=1) + tfr = tfr.real.sum(axis=-3) + tfr *= 2 / (weights * weights.conj()).real.sum(axis=-3) return tfr diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 5d092c21713..46406542b5c 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -773,9 +773,6 @@ def test_single_hemi(hemi, renderer_interactive_pyvistaqt, brain_gc): def test_brain_save_movie(tmp_path, renderer, brain_gc, interactive_state): """Test saving a movie of a Brain instance.""" imageio_ffmpeg = pytest.importorskip("imageio_ffmpeg") - # TODO: Figure out why this fails -- some imageio_ffmpeg error - if os.getenv("MNE_CI_KIND", "") == "conda" and platform.system() == "Linux": - pytest.skip("Test broken for unknown reason on conda linux") brain = _create_testing_brain( hemi="lh", time_viewer=False, cortex=["r", "b"] diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index f492c4b7fde..090c661f633 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -424,7 +424,7 @@ def _redraw(self, update_data=True, annotations=False): if annotations and not self.mne.is_epochs: self._draw_annotations() - def _close(self, event): + def _close(self, event=None): """Handle close events (via keypress or window [x]).""" from matplotlib.pyplot import close diff --git a/mne/viz/_mpl_figure.py b/mne/viz/_mpl_figure.py index 3987b641dff..f3563b454f0 100644 --- a/mne/viz/_mpl_figure.py +++ b/mne/viz/_mpl_figure.py @@ -186,7 +186,7 @@ def _inch_to_rel(self, dim_inches, horiz=True): class MNEAnnotationFigure(MNEFigure): """Interactive dialog figure for annotations.""" - def _close(self, event): + def _close(self, event=None): """Handle close events (via keypress or window [x]).""" parent = self.mne.parent_fig # disable span selector @@ -275,7 +275,7 @@ def _set_active_button(self, idx, *, draw=True): class MNESelectionFigure(MNEFigure): """Interactive dialog figure for channel selections.""" - def _close(self, event): + def _close(self, event=None): """Handle close events.""" self.mne.parent_fig.mne.child_figs.remove(self) self.mne.fig_selection = None diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index ee5b62404d3..0bd1ae1d3ca 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -1344,7 +1344,7 @@ def _is_osmesa(plotter): "surface rendering, consider upgrading to 18.3.6 or " "later." ) - is_osmesa = "via llvmpipe" in gpu_info + is_osmesa = "llvmpipe" in gpu_info return is_osmesa diff --git a/mne/viz/evoked_field.py b/mne/viz/evoked_field.py index cf5a9996216..839259ee117 100644 --- a/mne/viz/evoked_field.py +++ b/mne/viz/evoked_field.py @@ -7,6 +7,7 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +from copy import deepcopy from functools import partial import numpy as np @@ -185,6 +186,7 @@ def __init__( if isinstance(fig, Brain): self._renderer = fig._renderer self._in_brain_figure = True + self._units = fig._units if _get_3d_backend() == "notebook": raise NotImplementedError( "Plotting on top of an existing Brain figure " @@ -195,6 +197,7 @@ def __init__( fig, bgcolor=(0.0, 0.0, 0.0), size=(600, 600) ) self._in_brain_figure = False + self._units = "m" self.plotter = self._renderer.plotter self.interaction = interaction @@ -277,7 +280,8 @@ def _prepare_surf_map(self, surf_map, color, alpha): # Make a solid surface surf = surf_map["surf"] - if self._in_brain_figure: + if self._units == "mm": + surf = deepcopy(surf) surf["rr"] *= 1000 map_vmax = self._vmax.get(surf_map["kind"]) if map_vmax is None: diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 34022d59768..97b10621108 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -191,10 +191,21 @@ def test_plot_evoked_field(renderer): ch_type=t, ) evoked.plot_field(maps, time=0.1, n_contours=n_contours) + renderer.backend._close_all() - # Test plotting inside an existing Brain figure. - brain = Brain("fsaverage", "lh", "inflated", subjects_dir=subjects_dir) - fig = evoked.plot_field(maps, time=0.1, fig=brain) + # Test plotting inside an existing Brain figure. Check that units are taken into + # account. + for units in ["mm", "m"]: + brain = Brain( + "fsaverage", "lh", "inflated", units=units, subjects_dir=subjects_dir + ) + fig = evoked.plot_field(maps, time=0.1, fig=brain) + assert brain._units == fig._units + scale = 1000 if units == "mm" else 1 + assert ( + fig._surf_maps[0]["surf"]["rr"][0, 0] == scale * maps[0]["surf"]["rr"][0, 0] + ) + renderer.backend._close_all() # Test some methods fig = evoked.plot_field(maps, time_viewer=True) @@ -214,6 +225,7 @@ def test_plot_evoked_field(renderer): fig = evoked.plot_field(maps, time_viewer=False) assert isinstance(fig, Figure3D) + renderer.backend._close_all() @testing.requires_testing_data diff --git a/mne/viz/utils.py b/mne/viz/utils.py index f9d64c49ec8..b9b844b321a 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1185,7 +1185,7 @@ def _onpick_sensor(event, fig, ax, pos, ch_names, show_names): fig.canvas.draw() -def _close_event(event, fig): +def _close_event(event=None, fig=None): """Listen for sensor plotter close event.""" if getattr(fig, "lasso", None) is not None: fig.lasso.disconnect() diff --git a/tutorials/epochs/60_make_fixed_length_epochs.py b/tutorials/epochs/60_make_fixed_length_epochs.py index 04a4ec87c7d..10b8c12ea19 100644 --- a/tutorials/epochs/60_make_fixed_length_epochs.py +++ b/tutorials/epochs/60_make_fixed_length_epochs.py @@ -5,13 +5,12 @@ ================================================= This tutorial shows how to segment continuous data into a set of epochs spaced -equidistantly in time. The epochs will not be created based on experimental -events; instead, the continuous data will be "chunked" into consecutive epochs -(which may be temporally overlapping, adjacent, or separated). -We will also briefly demonstrate how to use these epochs in connectivity -analysis. +equidistantly in time. The epochs will not be created based on experimental events; +instead, the continuous data will be "chunked" into consecutive epochs (which may be +temporally overlapping, adjacent, or separated). We will also briefly demonstrate how +to use these epochs in connectivity analysis. -First, we import necessary modules and read in a sample raw data set. +First, we import the necessary modules and read in a sample raw data set. This data set contains brain activity that is event-related, i.e., synchronized to the onset of auditory stimuli. However, rather than creating epochs by segmenting the data around the onset of each stimulus, we will diff --git a/tutorials/evoked/10_evoked_overview.py b/tutorials/evoked/10_evoked_overview.py index 75e63692bd2..b251a1f8239 100644 --- a/tutorials/evoked/10_evoked_overview.py +++ b/tutorials/evoked/10_evoked_overview.py @@ -5,12 +5,11 @@ The Evoked data structure: evoked/averaged data =============================================== -This tutorial covers the basics of creating and working with :term:`evoked` -data. It introduces the :class:`~mne.Evoked` data structure in detail, -including how to load, query, subset, export, and plot data from an -:class:`~mne.Evoked` object. For details on creating an :class:`~mne.Evoked` -object from (possibly simulated) data in a :class:`NumPy array -`, see :ref:`tut-creating-data-structures`. +This tutorial covers the basics of creating and working with :term:`evoked` data. It +introduces the :class:`~mne.Evoked` data structure in detail, including how to load, +query, subset, export, and plot data from an :class:`~mne.Evoked` object. For details +on creating an :class:`~mne.Evoked` object from (possibly simulated) data in a +:class:`NumPy array `, see :ref:`tut-creating-data-structures`. As usual, we start by importing the modules we need: """