diff --git a/.circleci/config.yml b/.circleci/config.yml index bd94268c1ad..6618188621b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -296,8 +296,14 @@ jobs: # Build docs - run: name: make html + command: | # we have -o pipefail in #BASH_ENV so we should be okay + set -x + PATTERN=$(cat pattern.txt) make -C doc $(cat build.txt) 2>&1 | tee sphinx_log.txt + - run: + name: Check sphinx log for warnings (which are treated as errors) + when: always command: | - PATTERN=$(cat pattern.txt) make -C doc $(cat build.txt); + ! grep "^.* WARNING: .*$" sphinx_log.txt - run: name: Show profiling output when: always diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 00000000000..8fb235d7045 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..00a7b00c94e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 09555ac5eb9..d09ed2529d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,16 +63,16 @@ jobs: python: '3.11' kind: pip-pre - os: macos-latest - python: '3.8' + python: '3.9' kind: mamba - os: windows-latest python: '3.10' kind: mamba - os: ubuntu-latest - python: '3.8' + python: '3.9' kind: minimal - os: ubuntu-20.04 - python: '3.8' + python: '3.9' kind: old steps: - uses: actions/checkout@v4 diff --git a/.mailmap b/.mailmap index fcfc7edea01..e6d5377c402 100644 --- a/.mailmap +++ b/.mailmap @@ -240,6 +240,7 @@ Olaf Hauk olafhauk Omer Shubi Omer S Paul Pasler ppasler Paul Roujansky Paul ROUJANSKY +Paul Roujansky paulroujansky Pedro Silva pbnsilva Phillip Alday Phillip Alday Phillip Alday Phillip Alday @@ -254,6 +255,7 @@ Praveen Sripad prav Proloy Das pdas6 Ram Pari Ram Ramonapariciog Apariciogarcia ramonapariciog +Rasmus Aagaard roraa Reza Nasri Reza Reza Nasri RezaNasri Roan LaPlante aestrivex diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 436fbbb80a7..25d15b2157d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.5 + rev: v0.1.6 hooks: - id: ruff name: ruff mne @@ -16,7 +16,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.5 + rev: v0.1.6 hooks: - id: ruff name: ruff tutorials and examples @@ -50,3 +50,6 @@ repos: additional_dependencies: - tomli files: ^doc/.*\.(rst|inc)$ + +ci: + autofix_prs: false diff --git a/CITATION.cff b/CITATION.cff index edda3668c4c..c1850a2f55b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,9 +1,9 @@ cff-version: 1.2.0 title: "MNE-Python" message: "If you use this software, please cite both the software itself, and the paper listed in the preferred-citation field." -version: 1.5.0 -date-released: "2023-08-15" -commit: 5645d375f52727eb563bd46a45422fc1a55e9f1a +version: 1.6.0 +date-released: "2023-11-20" +commit: 498cf789685ede0b29e712a1e7220c69443e8744 doi: 10.5281/zenodo.592483 keywords: - MEG @@ -61,14 +61,14 @@ authors: given-names: Joan - family-names: Bekhti given-names: Yousra + - family-names: Scheltienne + given-names: Mathieu - family-names: Appelhoff given-names: Stefan - family-names: Leggitt given-names: Alan - family-names: Dykstra given-names: Andrew - - family-names: Scheltienne - given-names: Mathieu - family-names: Luke given-names: Rob - family-names: Trachel @@ -121,10 +121,14 @@ authors: given-names: Cathy - family-names: García Alanis given-names: José C + - family-names: Huberty + given-names: Scott - family-names: Hauk given-names: Olaf - family-names: Maddox given-names: Ross + - family-names: Orfanos + given-names: Dimitri Papadopoulos - family-names: LaPlante given-names: Roan - family-names: Drew @@ -133,16 +137,12 @@ authors: given-names: Christoph - family-names: Dumas given-names: Guillaume - - family-names: Huberty - given-names: Scott + - family-names: Benerradi + given-names: Johann - family-names: Hartmann given-names: Thomas - - family-names: Orfanos - given-names: Dimitri Papadopoulos - family-names: Ort given-names: Eduard - - family-names: Benerradi - given-names: Johann - family-names: Pasler given-names: Paul - family-names: Repplinger @@ -185,6 +185,8 @@ authors: given-names: Tanay - family-names: Nunes given-names: Adonay + - family-names: Gramfort + given-names: Alexandre - family-names: Gütlin given-names: Dirk - name: kjs @@ -196,6 +198,10 @@ authors: given-names: Catalina María - family-names: Moënne-Loccoz given-names: Cristóbal + - family-names: Altukhov + given-names: Dmitrii + - family-names: Peterson + given-names: Erica - family-names: Heinila given-names: Erkka - family-names: Hanna @@ -206,6 +212,8 @@ authors: given-names: Michiru - family-names: Klein given-names: Natalie + - family-names: Roujansky + given-names: Paul - family-names: Kern given-names: Simon - family-names: Rantala @@ -214,24 +222,26 @@ authors: given-names: Burkhard - family-names: O'Reilly given-names: Christian - - family-names: Peterson - given-names: Erica - family-names: Kolkhorst given-names: Henrich - family-names: Banville given-names: Hubert + - family-names: Zhang + given-names: Jack + - family-names: Woessner + given-names: Jacob - family-names: Maksymenko given-names: Kostiantyn - family-names: Clarke given-names: Maggie - family-names: Anelli given-names: Matteo + - family-names: Chapochnikov + given-names: Nikolai - family-names: Bannier given-names: Pierre-Antoine - family-names: Choudhary given-names: Saket - - family-names: Gramfort - given-names: Alexandre - family-names: Forster given-names: Carina - family-names: Kim @@ -242,8 +252,6 @@ authors: given-names: Fu-Te - family-names: Kojcic given-names: Ivana - - family-names: Zhang - given-names: Jack - family-names: Nielsen given-names: Jesper Duemose - family-names: Lankinen @@ -258,6 +266,10 @@ authors: given-names: Nathalie - family-names: Ward given-names: Nick + - family-names: Ruuskanen + given-names: Santeri + - family-names: Radanovic + given-names: Ana - family-names: Quinn given-names: Andrew - family-names: Gauthier @@ -266,6 +278,8 @@ authors: given-names: Basile - family-names: Welke given-names: Dominik + - family-names: Welke + given-names: Dominik - family-names: Stephen given-names: Emily - family-names: Hornberger @@ -326,12 +340,8 @@ authors: given-names: Chetan - family-names: Zhao given-names: Christina - - family-names: Altukhov - given-names: Dmitrii - family-names: Krzemiński given-names: Dominik - - family-names: Welke - given-names: Dominik - family-names: Makowski given-names: Dominique - family-names: Mikulan @@ -340,16 +350,22 @@ authors: given-names: Gennadiy - family-names: O'Neill given-names: George - - family-names: Woessner - given-names: Jacob + - family-names: Abdelhedi + given-names: Hamza - family-names: Schiratti given-names: Jean-Baptiste - family-names: Evans given-names: Jen + - family-names: Veillette + given-names: John - family-names: Drew given-names: Jordan - family-names: Teves given-names: Joshua + - family-names: Zhu + given-names: Judy D + - family-names: Armeni + given-names: Kristijan - family-names: Mathewson given-names: Kyle - family-names: Gwilliams @@ -375,8 +391,6 @@ authors: given-names: Naveen - family-names: Wilming given-names: Niklas - - family-names: Chapochnikov - given-names: Nikolai - family-names: Kozynets given-names: Oleh - family-names: Ablin @@ -427,6 +441,8 @@ authors: given-names: Adina - family-names: Ciok given-names: Alex + - family-names: Gilbert + given-names: Andy - family-names: Pradhan given-names: Aniket - family-names: Padee @@ -510,10 +526,10 @@ authors: - family-names: O'Neill given-names: George - name: Giulio + - family-names: Reina + given-names: Gonzalo - family-names: Maymandi given-names: Hamid - - family-names: Abdelhedi - given-names: Hamza - family-names: Sonntag given-names: Hermann - family-names: Ye @@ -524,6 +540,10 @@ authors: given-names: Hüseyin Orkun - family-names: Machairas given-names: Ilias + - family-names: Skelin + given-names: Ivan + - family-names: Zubarev + given-names: Ivan - family-names: Kaczmarzyk given-names: Jakub - family-names: Zerfowski @@ -536,14 +556,10 @@ authors: given-names: Johan - family-names: Niediek given-names: Johannes - - family-names: Veillette - given-names: John - family-names: Koen given-names: Josh - family-names: Bear given-names: Joshua J - - family-names: Zhu - given-names: Judy D - family-names: Dammers given-names: Juergen - family-names: Galán @@ -566,6 +582,8 @@ authors: given-names: Lorenzo - family-names: Hejtmánek given-names: Lukáš + - family-names: Balatsko + given-names: Maksym - family-names: Kitzbichler given-names: Manfred - family-names: Kumar @@ -578,6 +596,8 @@ authors: given-names: Marcin - family-names: Henney given-names: Mark Alexander + - family-names: Schulz + given-names: Martin - family-names: van Harmelen given-names: Martin - name: MartinBaBer @@ -613,16 +633,18 @@ authors: given-names: Nikolas - family-names: Shubi given-names: Omer + - family-names: Mainar + given-names: Pablo - family-names: Sundaram given-names: Padma - - family-names: Roujansky - given-names: Paul - family-names: Silva given-names: Pedro - family-names: Molfese given-names: Peter J - family-names: Das given-names: Proloy + - family-names: Chu + given-names: Qian - family-names: Li given-names: Quanliang - family-names: Barthélemy @@ -633,6 +655,8 @@ authors: given-names: Ramiro - family-names: Apariciogarcia given-names: Ramonapariciog + - family-names: Aagaard + given-names: Rasmus - family-names: Nasri given-names: Reza - family-names: Koehler @@ -653,8 +677,6 @@ authors: given-names: Sam - family-names: Louviot given-names: Samuel - - family-names: Ruuskanen - given-names: Santeri - family-names: Saha given-names: Sawradip - family-names: Mathot diff --git a/Makefile b/Makefile index 2843e0193b5..7d5488258d8 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ codespell: # running manually @codespell --builtin clear,rare,informal,names,usage -w -i 3 -q 3 -S $(CODESPELL_SKIPS) --ignore-words=ignore_words.txt --uri-ignore-words-list=bu $(CODESPELL_DIRS) check-manifest: - check-manifest -q --ignore .circleci/config.yml,doc,logo,mne/io/*/tests/data*,mne/io/tests/data,mne/preprocessing/tests/data,.DS_Store,mne/_version.py + check-manifest -q --ignore .circleci/config.yml,doc,logo,mne/io/*/tests/data*,mne/io/tests/data,mne/preprocessing/tests/data,.DS_Store,.git_archival.txt check-readme: clean wheel twine check dist/* diff --git a/README.rst b/README.rst index e8690281bcb..ca4e08becba 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,7 @@ only, use pip_ in a terminal: $ pip install --upgrade mne -The current MNE-Python release requires Python 3.8 or higher. MNE-Python 0.17 +The current MNE-Python release requires Python 3.9 or higher. MNE-Python 0.17 was the last release to support Python 2.7. For more complete instructions, including our standalone installers and more @@ -73,7 +73,7 @@ Dependencies The minimum required dependencies to run MNE-Python are: -- `Python `__ ≥ 3.8 +- `Python `__ ≥ 3.9 - `NumPy `__ ≥ 1.21.2 - `SciPy `__ ≥ 1.7.1 - `Matplotlib `__ ≥ 3.5.0 diff --git a/SECURITY.md b/SECURITY.md index c61f3bfae87..e627242d244 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,9 +10,9 @@ without a proper 6-month deprecation cycle. | Version | Supported | | ------- | ------------------------ | -| 1.6.x | :heavy_check_mark: (dev) | -| 1.5.x | :heavy_check_mark: | -| < 1.5 | :x: | +| 1.7.x | :heavy_check_mark: (dev) | +| 1.6.x | :heavy_check_mark: | +| < 1.6 | :x: | ## Reporting a Vulnerability @@ -21,7 +21,7 @@ recorded with a variety of devices/modalities (EEG, MEG, ECoG, fNIRS, etc). It is not expected that using MNE-Python will lead to security vulnerabilities under normal use cases (i.e., running without administrator privileges). However, if you think you have found a security vulnerability -in MNE-Python, **please do not report it as a GitHub issue**, in order to +in MNE-Python, **please do not report it as a GitHub issue**, in order to keep the vulnerability confidential. Instead, please report it to mne-core-dev-team@groups.io and include a description and proof-of-concept that is [short and self-contained](http://www.sscce.org/). diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5cee5568623..5e70fe270ea 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -108,7 +108,7 @@ stages: - bash: | set -e python -m pip install --progress-bar off --upgrade pip - python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" qtpy nibabel sphinx-gallery + python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" "PyQt6-Qt6!=6.6.1" qtpy nibabel sphinx-gallery python -m pip uninstall -yq mne python -m pip install --progress-bar off --upgrade -e .[test] displayName: 'Install dependencies with pip' @@ -117,10 +117,9 @@ stages: mne sys_info -pd mne sys_info -pd | grep "qtpy .*(PyQt6=.*)$" displayName: Print config - # Uncomment if "xcb not found" Qt errors/segfaults come up again - # - bash: | - # set -e - # LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" + - bash: | + set -e + LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" - bash: source tools/get_testing_version.sh displayName: 'Get testing version' - task: Cache@2 @@ -188,9 +187,9 @@ stages: displayName: 'Get test data' - bash: | set -e - python -m pip install PyQt6 - # Uncomment if "xcb not found" Qt errors/segfaults come up again - # LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" + python -m pip install PyQt6 "PyQt6-Qt6!=6.6.1" + LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" + - bash: | mne sys_info -pd mne sys_info -pd | grep "qtpy .* (PyQt6=.*)$" PYTEST_QT_API=PyQt6 pytest -m "not slowtest" ${TEST_OPTIONS} diff --git a/codemeta.json b/codemeta.json index 7e8b2cca70a..b2922b2194d 100644 --- a/codemeta.json +++ b/codemeta.json @@ -5,11 +5,11 @@ "codeRepository": "git+https://github.com/mne-tools/mne-python.git", "dateCreated": "2010-12-26", "datePublished": "2014-08-04", - "dateModified": "2023-08-15", - "downloadUrl": "https://github.com/mne-tools/mne-python/archive/v1.5.0.zip", + "dateModified": "2023-11-20", + "downloadUrl": "https://github.com/mne-tools/mne-python/archive/v1.6.0.zip", "issueTracker": "https://github.com/mne-tools/mne-python/issues", "name": "MNE-Python", - "version": "1.5.0", + "version": "1.6.0", "description": "MNE-Python is an open-source Python package for exploring, visualizing, and analyzing human neurophysiological data. It provides methods for data input/output, preprocessing, visualization, source estimation, time-frequency analysis, connectivity analysis, machine learning, and statistics.", "applicationCategory": "Neuroscience", "developmentStatus": "active", @@ -38,8 +38,17 @@ ], "softwareRequirements": [ "python>=3.8", - "numpy>=1.15.4", - "scipy>=1.6.3" + "numpy>=1.21.2", + "scipy>=1.7.1", + "matplotlib>=3.5.0", + "tqdm", + "pooch>=1.5", + "decorator", + "packaging", + "jinja2", + "importlib_resources>=5.10.2; python_version<'3.9'", + "lazy_loader>=0.3", + "defusedxml" ], "author": [ { @@ -168,6 +177,12 @@ "givenName":"Yousra", "familyName": "Bekhti" }, + { + "@type":"Person", + "email":"mathieu.scheltienne@gmail.com", + "givenName":"Mathieu", + "familyName": "Scheltienne" + }, { "@type":"Person", "email":"stefan.appelhoff@mailbox.org", @@ -186,12 +201,6 @@ "givenName":"Andrew", "familyName": "Dykstra" }, - { - "@type":"Person", - "email":"mathieu.scheltienne@gmail.com", - "givenName":"Mathieu", - "familyName": "Scheltienne" - }, { "@type":"Person", "email":"code@robertluke.net", @@ -348,6 +357,12 @@ "givenName":"José C", "familyName": "García Alanis" }, + { + "@type":"Person", + "email":"", + "givenName":"Scott", + "familyName": "Huberty" + }, { "@type":"Person", "email":"olaf.hauk@mrc-cbu.cam.ac.uk", @@ -360,6 +375,12 @@ "givenName":"Ross", "familyName": "Maddox" }, + { + "@type":"Person", + "email":"", + "givenName":"Dimitri Papadopoulos", + "familyName": "Orfanos" + }, { "@type":"Person", "email":"aestrivex@gmail.com", @@ -386,9 +407,9 @@ }, { "@type":"Person", - "email":"", - "givenName":"Scott", - "familyName": "Huberty" + "email":"johann.benerradi@gmail.com", + "givenName":"Johann", + "familyName": "Benerradi" }, { "@type":"Person", @@ -396,24 +417,12 @@ "givenName":"Thomas", "familyName": "Hartmann" }, - { - "@type":"Person", - "email":"", - "givenName":"Dimitri Papadopoulos", - "familyName": "Orfanos" - }, { "@type":"Person", "email":"eduardxort@gmail.com", "givenName":"Eduard", "familyName": "Ort" }, - { - "@type":"Person", - "email":"johann.benerradi@gmail.com", - "givenName":"Johann", - "familyName": "Benerradi" - }, { "@type":"Person", "email":"paul@ppasler.de", @@ -540,6 +549,12 @@ "givenName":"Adonay", "familyName": "Nunes" }, + { + "@type":"Person", + "email":"agramfort@fb.com", + "givenName":"Alexandre", + "familyName": "Gramfort" + }, { "@type":"Person", "email":"", @@ -576,6 +591,18 @@ "givenName":"Cristóbal", "familyName": "Moënne-Loccoz" }, + { + "@type":"Person", + "email":"dm.altukhov@ya.ru", + "givenName":"Dmitrii", + "familyName": "Altukhov" + }, + { + "@type":"Person", + "email":"nordme@uw.edu", + "givenName":"Erica", + "familyName": "Peterson" + }, { "@type":"Person", "email":"erkkahe@gmail.com", @@ -606,6 +633,12 @@ "givenName":"Natalie", "familyName": "Klein" }, + { + "@type":"Person", + "email":"paul@roujansky.eu", + "givenName":"Paul", + "familyName": "Roujansky" + }, { "@type":"Person", "email":"simon.kern@online.de", @@ -630,12 +663,6 @@ "givenName":"Christian", "familyName": "O'Reilly" }, - { - "@type":"Person", - "email":"nordme@uw.edu", - "givenName":"Erica", - "familyName": "Peterson" - }, { "@type":"Person", "email":"", @@ -648,6 +675,18 @@ "givenName":"Hubert", "familyName": "Banville" }, + { + "@type":"Person", + "email":"zhangmengyu10@gmail.com", + "givenName":"Jack", + "familyName": "Zhang" + }, + { + "@type":"Person", + "email":"Woessner.jacob@gmail.com", + "givenName":"Jacob", + "familyName": "Woessner" + }, { "@type":"Person", "email":"makkostya@ukr.net", @@ -666,6 +705,12 @@ "givenName":"Matteo", "familyName": "Anelli" }, + { + "@type":"Person", + "email":"", + "givenName":"Nikolai", + "familyName": "Chapochnikov" + }, { "@type":"Person", "email":"pierreantoine.bannier@gmail.com", @@ -678,12 +723,6 @@ "givenName":"Saket", "familyName": "Choudhary" }, - { - "@type":"Person", - "email":"agramfort@fb.com", - "givenName":"Alexandre", - "familyName": "Gramfort" - }, { "@type":"Person", "email":"carinaforster0611@gmail.com", @@ -714,12 +753,6 @@ "givenName":"Ivana", "familyName": "Kojcic" }, - { - "@type":"Person", - "email":"zhangmengyu10@gmail.com", - "givenName":"Jack", - "familyName": "Zhang" - }, { "@type":"Person", "email":"jdue@dtu.dk", @@ -762,6 +795,18 @@ "givenName":"Nick", "familyName": "Ward" }, + { + "@type":"Person", + "email":"", + "givenName":"Santeri", + "familyName": "Ruuskanen" + }, + { + "@type":"Person", + "email":"", + "givenName":"Ana", + "familyName": "Radanovic" + }, { "@type":"Person", "email":"", @@ -780,6 +825,12 @@ "givenName":"Basile", "familyName": "Pinsard" }, + { + "@type":"Person", + "email":"dominik.welke@ae.mpg.de", + "givenName":"Dominik", + "familyName": "Welke" + }, { "@type":"Person", "email":"dominik.welke@web.de", @@ -966,24 +1017,12 @@ "givenName":"Christina", "familyName": "Zhao" }, - { - "@type":"Person", - "email":"dm.altukhov@ya.ru", - "givenName":"Dmitrii", - "familyName": "Altukhov" - }, { "@type":"Person", "email":"raymon92@gmail.com", "givenName":"Dominik", "familyName": "Krzemiński" }, - { - "@type":"Person", - "email":"dominik.welke@ae.mpg.de", - "givenName":"Dominik", - "familyName": "Welke" - }, { "@type":"Person", "email":"dom.mak19@gmail.com", @@ -1010,9 +1049,9 @@ }, { "@type":"Person", - "email":"Woessner.jacob@gmail.com", - "givenName":"Jacob", - "familyName": "Woessner" + "email":"hamza.abdelhedii@gmail.com", + "givenName":"Hamza", + "familyName": "Abdelhedi" }, { "@type":"Person", @@ -1026,6 +1065,12 @@ "givenName":"Jen", "familyName": "Evans" }, + { + "@type":"Person", + "email":"johnv@uchicago.edu", + "givenName":"John", + "familyName": "Veillette" + }, { "@type":"Person", "email":"", @@ -1038,6 +1083,18 @@ "givenName":"Joshua", "familyName": "Teves" }, + { + "@type":"Person", + "email":"", + "givenName":"Judy D", + "familyName": "Zhu" + }, + { + "@type":"Person", + "email":"kristijan.armeni@gmail.com", + "givenName":"Kristijan", + "familyName": "Armeni" + }, { "@type":"Person", "email":"kylemath@gmail.com", @@ -1116,12 +1173,6 @@ "givenName":"Niklas", "familyName": "Wilming" }, - { - "@type":"Person", - "email":"", - "givenName":"Nikolai", - "familyName": "Chapochnikov" - }, { "@type":"Person", "email":"", @@ -1278,6 +1329,12 @@ "givenName":"Alex", "familyName": "Ciok" }, + { + "@type":"Person", + "email":"7andy121@gmail.com", + "givenName":"Andy", + "familyName": "Gilbert" + }, { "@type":"Person", "email":"aniket17133@iiitd.ac.in", @@ -1533,14 +1590,14 @@ { "@type":"Person", "email":"", - "givenName":"Hamid", - "familyName": "Maymandi" + "givenName":"Gonzalo", + "familyName": "Reina" }, { "@type":"Person", - "email":"hamza.abdelhedii@gmail.com", - "givenName":"Hamza", - "familyName": "Abdelhedi" + "email":"", + "givenName":"Hamid", + "familyName": "Maymandi" }, { "@type":"Person", @@ -1572,6 +1629,18 @@ "givenName":"Ilias", "familyName": "Machairas" }, + { + "@type":"Person", + "email":"", + "givenName":"Ivan", + "familyName": "Skelin" + }, + { + "@type":"Person", + "email":"ivan.zubarev@aalto.fi", + "givenName":"Ivan", + "familyName": "Zubarev" + }, { "@type":"Person", "email":"", @@ -1608,12 +1677,6 @@ "givenName":"Johannes", "familyName": "Niediek" }, - { - "@type":"Person", - "email":"johnv@uchicago.edu", - "givenName":"John", - "familyName": "Veillette" - }, { "@type":"Person", "email":"koen.joshua@gmail.com", @@ -1626,12 +1689,6 @@ "givenName":"Joshua J", "familyName": "Bear" }, - { - "@type":"Person", - "email":"", - "givenName":"Judy D", - "familyName": "Zhu" - }, { "@type":"Person", "email":"j.dammers@fz-juelich.de", @@ -1698,6 +1755,12 @@ "givenName":"Lukáš", "familyName": "Hejtmánek" }, + { + "@type":"Person", + "email":"mbalatsko@gmail.com", + "givenName":"Maksym", + "familyName": "Balatsko" + }, { "@type":"Person", "email":"manfredg@nmr.mgh.harvard.edu", @@ -1734,6 +1797,12 @@ "givenName":"Mark Alexander", "familyName": "Henney" }, + { + "@type":"Person", + "email":"dev@mgschulz.de", + "givenName":"Martin", + "familyName": "Schulz" + }, { "@type":"Person", "email":"", @@ -1844,15 +1913,15 @@ }, { "@type":"Person", - "email":"tottochan@gmail.com", - "givenName":"Padma", - "familyName": "Sundaram" + "email":"pablomainar.pm@gmail.com", + "givenName":"Pablo", + "familyName": "Mainar" }, { "@type":"Person", - "email":"paul@roujansky.eu", - "givenName":"Paul", - "familyName": "Roujansky" + "email":"tottochan@gmail.com", + "givenName":"Padma", + "familyName": "Sundaram" }, { "@type":"Person", @@ -1872,6 +1941,12 @@ "givenName":"Proloy", "familyName": "Das" }, + { + "@type":"Person", + "email":"", + "givenName":"Qian", + "familyName": "Chu" + }, { "@type":"Person", "email":"glia@dtu.dk", @@ -1902,6 +1977,12 @@ "givenName":"Ramonapariciog", "familyName": "Apariciogarcia" }, + { + "@type":"Person", + "email":"raagaard97@gmail.com", + "givenName":"Rasmus", + "familyName": "Aagaard" + }, { "@type":"Person", "email":"reza@ddpo.ir", @@ -1962,12 +2043,6 @@ "givenName":"Samuel", "familyName": "Louviot" }, - { - "@type":"Person", - "email":"", - "givenName":"Santeri", - "familyName": "Ruuskanen" - }, { "@type":"Person", "email":"", diff --git a/doc/_static/style.css b/doc/_static/style.css index b10216ddcb3..61eea678830 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -59,6 +59,7 @@ html[data-theme="dark"] { --pst-color-border: #333; --pst-color-background: #000; --pst-color-link: #66b0ff; + --pst-color-on-background: #1e1e1e; /* sphinx-gallery overrides */ --sg-download-a-background-color: var(--pst-color-primary); --sg-download-a-background-image: unset; diff --git a/doc/_static/versions.json b/doc/_static/versions.json index 9845f298fb6..8141440bd16 100644 --- a/doc/_static/versions.json +++ b/doc/_static/versions.json @@ -1,14 +1,19 @@ [ { - "name": "1.6 (devel)", + "name": "1.7 (devel)", "version": "dev", "url": "https://mne.tools/dev/" }, { - "name": "1.5 (stable)", + "name": "1.6 (stable)", "version": "stable", "url": "https://mne.tools/stable/" }, + { + "name": "1.5", + "version": "1.5", + "url": "https://mne.tools/1.5/" + }, { "name": "1.4", "version": "1.4", diff --git a/doc/api/reading_raw_data.rst b/doc/api/reading_raw_data.rst index 1b8ebae2abf..50f524ce7c8 100644 --- a/doc/api/reading_raw_data.rst +++ b/doc/api/reading_raw_data.rst @@ -40,6 +40,7 @@ Reading raw data read_raw_nihon read_raw_fil read_raw_nsx + read_raw_neuralynx Base class: diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 9558f8fe0ea..3f3e8036419 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -18,92 +18,20 @@ .. _current: -Version 1.6.dev0 (development) +Version 1.7.dev0 (development) ------------------------------ Enhancements ~~~~~~~~~~~~ -- Add support for Neuralynx data files with ``mne.io.read_raw_neuralynx`` (:gh:`11969` by :newcontrib:`Kristijan Armeni` and :newcontrib:`Ivan Skelin`) -- Improve tests for saving splits with :class:`mne.Epochs` (:gh:`11884` by `Dmitrii Altukhov`_) -- Added functionality for linking interactive figures together, such that changing one figure will affect another, see :ref:`tut-ui-events` and :mod:`mne.viz.ui_events`. Current figures implementing UI events are :func:`mne.viz.plot_topomap` and :func:`mne.viz.plot_source_estimates` (:gh:`11685` :gh:`11891` by `Marijn van Vliet`_) -- HTML anchors for :class:`mne.Report` now reflect the ``section-title`` of the report items rather than using a global incrementor ``global-N`` (:gh:`11890` by `Eric Larson`_) -- Added public :func:`mne.io.write_info` to complement :func:`mne.io.read_info` (:gh:`11918` by `Eric Larson`_) -- Added option ``remove_dc`` to to :meth:`Raw.compute_psd() `, :meth:`Epochs.compute_psd() `, and :meth:`Evoked.compute_psd() `, to allow skipping DC removal when computing Welch or multitaper spectra (:gh:`11769` by `Nikolai Chapochnikov`_) -- Add the possibility to provide a float between 0 and 1 as ``n_grad``, ``n_mag`` and ``n_eeg`` in `~mne.compute_proj_raw`, `~mne.compute_proj_epochs` and `~mne.compute_proj_evoked` to select the number of vectors based on the cumulative explained variance (:gh:`11919` by `Mathieu Scheltienne`_) -- Add extracting all time courses in a label using :func:`mne.extract_label_time_course` without applying an aggregation function (like ``mean``) (:gh:`12001` by `Hamza Abdelhedi`_) -- Added support for Artinis fNIRS data files to :func:`mne.io.read_raw_snirf` (:gh:`11926` by `Robert Luke`_) -- Add helpful error messages when using methods on empty :class:`mne.Epochs`-objects (:gh:`11306` by `Martin Schulz`_) -- Add support for passing a :class:`python:dict` as ``sensor_color`` to specify per-channel-type colors in :func:`mne.viz.plot_alignment` (:gh:`12067` by `Eric Larson`_) -- Add inferring EEGLAB files' montage unit automatically based on estimated head radius using :func:`read_raw_eeglab(..., montage_units="auto") ` (:gh:`11925` by `Jack Zhang`_, :gh:`11951` by `Eric Larson`_) -- Add :class:`~mne.time_frequency.EpochsSpectrumArray` and :class:`~mne.time_frequency.SpectrumArray` to support creating power spectra from :class:`NumPy array ` data (:gh:`11803` by `Alex Rockhill`_) -- Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) -- Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) -- Add support for drawing MEG sensors in :ref:`mne coreg` (:gh:`12098` by `Eric Larson`_) -- Improve string representation of :class:`mne.Covariance` (:gh:`12181` by `Eric Larson`_) -- Add ``check_version=True`` to :ref:`mne sys_info` to check for a new release on GitHub (:gh:`12146` by `Eric Larson`_) -- Bad channels are now colored gray in addition to being dashed when spatial colors are used in :func:`mne.viz.plot_evoked` and related functions (:gh:`12142` by `Eric Larson`_) -- By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_) -- Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) -- Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_) -- Add example KIT phantom dataset in :func:`mne.datasets.phantom_kit.data_path` and :ref:`tut-phantom-kit` (:gh:`12105` by `Judy D Zhu`_ and `Eric Larson`_) -- :func:`~mne.epochs.make_metadata` now accepts ``tmin=None`` and ``tmax=None``, which will bound the time window used for metadata generation by event names (instead of a fixed time). That way, you can now for example generate metadata spanning from one cue or fixation cross to the next, even if trial durations vary throughout the recording (:gh:`12118` by `Richard Höchenberger`_) -- Add support for passing multiple labels to :func:`mne.minimum_norm.source_induced_power` (:gh:`12026` by `Erica Peterson`_, `Eric Larson`_, and `Daniel McCloy`_ ) -- Added documentation to :meth:`mne.io.Raw.set_montage` and :func:`mne.add_reference_channels` to specify that montages should be set after adding reference channels (:gh:`12160` by `Jacob Woessner`_) -- Add argument ``splash`` to the function using the ``qt`` browser backend to allow enabling/disabling the splash screen (:gh:`12185` by `Mathieu Scheltienne`_) -- :class:`~mne.preprocessing.ICA`'s HTML representation (displayed in Jupyter notebooks and :class:`mne.Report`) now includes all optional fit parameters (e.g., max. number of iterations) (:gh:`12194`, by `Richard Höchenberger`_) +- Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) + Bugs ~~~~ -- Fix bug where :func:`mne.io.read_raw_gdf` would fail due to improper usage of ``np.clip`` (:gh:`12168` by :newcontrib:`Rasmus Aagaard`) -- Fix bugs with :func:`mne.preprocessing.realign_raw` where the start of ``other`` was incorrectly cropped; and onsets and durations in ``other.annotations`` were left unsynced with the resampled data (:gh:`11950` by :newcontrib:`Qian Chu`) -- Fix bug where ``encoding`` argument was ignored when reading annotations from an EDF file (:gh:`11958` by :newcontrib:`Andrew Gilbert`) -- Mark tests ``test_adjacency_matches_ft`` and ``test_fetch_uncompressed_file`` as network tests (:gh:`12041` by :newcontrib:`Maksym Balatsko`) -- Fix bug with :func:`mne.channels.read_ch_adjacency` (:gh:`11608` by :newcontrib:`Ivan Zubarev`) -- Fix bug where ``epochs.get_data(..., scalings=...)`` would errantly modify the preloaded data (:gh:`12121` by :newcontrib:`Pablo Mainar` and `Eric Larson`_) -- Fix bugs with saving splits for :class:`~mne.Epochs` (:gh:`11876` by `Dmitrii Altukhov`_) -- Fix bug with multi-plot 3D rendering where only one plot was updated (:gh:`11896` by `Eric Larson`_) -- Fix bug where ``verbose`` level was not respected inside parallel jobs (:gh:`12154` by `Eric Larson`_) -- Fix bug where subject birthdays were not correctly read by :func:`mne.io.read_raw_snirf` (:gh:`11912` by `Eric Larson`_) -- Fix bug where warnings were emitted when computing spectra for channels marked as bad (:gh:`12186` by `Eric Larson`_) -- Fix bug with :func:`mne.chpi.compute_head_pos` for CTF data where digitization points were modified in-place, producing an incorrect result during a save-load round-trip (:gh:`11934` by `Eric Larson`_) -- Fix bug where non-compliant stimulus data streams were not ignored by :func:`mne.io.read_raw_snirf` (:gh:`11915` by `Johann Benerradi`_) -- Fix bug with ``pca=False`` in :func:`mne.minimum_norm.compute_source_psd` (:gh:`11927` by `Alex Gramfort`_) -- Fix bug with notebooks when using PyVista 0.42 by implementing ``trame`` backend support (:gh:`11956` by `Eric Larson`_) -- Removed preload parameter from :func:`mne.io.read_raw_eyelink`, because data are always preloaded no matter what preload is set to (:gh:`11910` by `Scott Huberty`_) -- Fix bug with :meth:`mne.viz.Brain.get_view` where calling :meth:`~mne.viz.Brain.show_view` with returned parameters would change the view (:gh:`12000` by `Eric Larson`_) -- Fix bug with :meth:`mne.viz.Brain.show_view` where ``distance=None`` would change the view distance (:gh:`12000` by `Eric Larson`_) -- Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_) -- Fix bug with reported component number and errant reporting of PCA explained variance as ICA explained variance in :meth:`mne.Report.add_ica` (:gh:`12155`, :gh:`12167` by `Eric Larson`_ and `Richard Höchenberger`_) -- Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_) -- Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_) -- Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_) -- Fix rendering glitches when plotting Neuromag/TRIUX sensors in :func:`mne.viz.plot_alignment` and related functions (:gh:`12098` by `Eric Larson`_) -- Fix bug with delayed checking of :class:`info["bads"] ` (:gh:`12038` by `Eric Larson`_) -- Fix bug with :ref:`mne coreg` where points inside the head surface were not shown (:gh:`12147`, :gh:`12164` by `Eric Larson`_) -- Fix bug with :func:`mne.viz.plot_alignment` where ``sensor_colors`` were not handled properly on a per-channel-type basis (:gh:`12067` by `Eric Larson`_) -- Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` :gh:`12044` by `Paul Roujansky`_) -- Add missing ``overwrite`` and ``verbose`` parameters to :meth:`Transform.save() ` (:gh:`12004` by `Marijn van Vliet`_) -- Fix parsing of eye-link :class:`~mne.Annotations` when ``apply_offsets=False`` is provided to :func:`~mne.io.read_raw_eyelink` (:gh:`12003` by `Mathieu Scheltienne`_) -- Correctly prune channel-specific :class:`~mne.Annotations` when creating :class:`~mne.Epochs` without the channel(s) included in the channel specific annotations (:gh:`12010` by `Mathieu Scheltienne`_) -- Fix :func:`~mne.viz.plot_volume_source_estimates` with :class:`~mne.VolSourceEstimate` which include a list of vertices (:gh:`12025` by `Mathieu Scheltienne`_) -- Add support for non-ASCII characters in Annotations, Evoked comments, etc when saving to FIFF format (:gh:`12080` by `Daniel McCloy`_) -- Correctly handle passing ``"eyegaze"`` or ``"pupil"`` to :meth:`mne.io.Raw.pick` (:gh:`12019` by `Scott Huberty`_) -- Fix bug with :func:`mne.time_frequency.Spectrum.plot` and related functions where bad channels were not marked (:gh:`12142` by `Eric Larson`_) -- Fix bug with :func:`~mne.viz.plot_raw` where changing ``MNE_BROWSER_BACKEND`` via :func:`~mne.set_config` would have no effect within a Python session (:gh:`12078` by `Santeri Ruuskanen`_) -- Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) -- Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_) -- Fix bug with :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` where automatic fiducial detection would fail for certain files (:gh:`12165` by `Clemens Brunner`_) -- Fix concatenation of ``raws`` with ``np.nan`` in the device to head transformation (:gh:`12198` by `Mathieu Scheltienne`_) -- Fix bug with :func:`mne.viz.plot_compare_evokeds` where the title was not displayed when ``axes='topo'`` (:gh:`12192` by `Jacob Woessner`_) -- Fix bug with :func:`mne.io.read_raw_cnt` where the bad channels were not properly read (:gh:`12189` by `Jacob Woessner`_) -- Fix bug where iterating over :class:`~mne.io.Raw` would result in an error (:gh:`12205` by `Clemens Brunner`_) - +- Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling (:gh:`12190` by `Scott Huberty`_) +- Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned (:gh:`12248` by `Eric Larson`_) +- Fix bug with type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) API changes ~~~~~~~~~~~ -- The default for :meth:`mne.Epochs.get_data` of ``copy=False`` will change to ``copy=True`` in 1.7. Set it explicitly to avoid a warning (:gh:`12121` by :newcontrib:`Pablo Mainar` and `Eric Larson`_) -- ``mne.preprocessing.apply_maxfilter`` and ``mne maxfilter`` have been deprecated and will be removed in 1.7. Use :func:`mne.preprocessing.maxwell_filter` (see :ref:`this tutorial `) in Python or the command-line utility from MEGIN ``maxfilter`` and :func:`mne.bem.fit_sphere_to_headshape` instead (:gh:`11938` by `Eric Larson`_) -- :func:`mne.io.kit.read_mrk` reading pickled files is deprecated using something like ``np.savetxt(fid, pts, delimiter="\t", newline="\n")`` to save your points instead (:gh:`11937` by `Eric Larson`_) -- Replace legacy ``inst.pick_channels`` and ``inst.pick_types`` with ``inst.pick`` (where ``inst`` is an instance of :class:`~mne.io.Raw`, :class:`~mne.Epochs`, or :class:`~mne.Evoked`) wherever possible (:gh:`11907` by `Clemens Brunner`_) -- The ``reset_camera`` parameter has been removed in favor of ``distance="auto"`` in :func:`mne.viz.set_3d_view`, :meth:`mne.viz.Brain.show_view`, and related functions (:gh:`12000` by `Eric Larson`_) -- Several unused parameters from :func:`mne.gui.coregistration` are now deprecated: tabbed, split, scrollable, head_inside, guess_mri_subject, scale, and ``advanced_rendering``. All arguments are also now keyword-only. (:gh:`12147` by `Eric Larson`_) +- None yet diff --git a/doc/changes/names.inc b/doc/changes/names.inc index da884792c4f..2ec8f2268be 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -172,6 +172,8 @@ .. _Felix Raimundo: https://github.com/gamazeps +.. _Florian Hofer: https://github.com/hofaflo + .. _Florin Pop: https://github.com/florin-pop .. _Frederik Weber: https://github.com/Frederik-D-Weber diff --git a/doc/changes/v1.6.rst b/doc/changes/v1.6.rst new file mode 100644 index 00000000000..f770b5046d2 --- /dev/null +++ b/doc/changes/v1.6.rst @@ -0,0 +1,132 @@ +.. _changes_1_6_0: + +Version 1.6.0 (2023-11-20) +-------------------------- + +Enhancements +~~~~~~~~~~~~ +- Add support for Neuralynx data files with :func:`mne.io.read_raw_neuralynx` (:gh:`11969` by :newcontrib:`Kristijan Armeni` and :newcontrib:`Ivan Skelin`) +- Improve tests for saving splits with :class:`mne.Epochs` (:gh:`11884` by `Dmitrii Altukhov`_) +- Added functionality for linking interactive figures together, such that changing one figure will affect another, see :ref:`tut-ui-events` and :mod:`mne.viz.ui_events`. Current figures implementing UI events are :func:`mne.viz.plot_topomap` and :func:`mne.viz.plot_source_estimates` (:gh:`11685` :gh:`11891` by `Marijn van Vliet`_) +- HTML anchors for :class:`mne.Report` now reflect the ``section-title`` of the report items rather than using a global incrementor ``global-N`` (:gh:`11890` by `Eric Larson`_) +- Added public :func:`mne.io.write_info` to complement :func:`mne.io.read_info` (:gh:`11918` by `Eric Larson`_) +- Added option ``remove_dc`` to to :meth:`Raw.compute_psd() `, :meth:`Epochs.compute_psd() `, and :meth:`Evoked.compute_psd() `, to allow skipping DC removal when computing Welch or multitaper spectra (:gh:`11769` by `Nikolai Chapochnikov`_) +- Add the possibility to provide a float between 0 and 1 as ``n_grad``, ``n_mag`` and ``n_eeg`` in `~mne.compute_proj_raw`, `~mne.compute_proj_epochs` and `~mne.compute_proj_evoked` to select the number of vectors based on the cumulative explained variance (:gh:`11919` by `Mathieu Scheltienne`_) +- Add extracting all time courses in a label using :func:`mne.extract_label_time_course` without applying an aggregation function (like ``mean``) (:gh:`12001` by `Hamza Abdelhedi`_) +- Added support for Artinis fNIRS data files to :func:`mne.io.read_raw_snirf` (:gh:`11926` by `Robert Luke`_) +- Add helpful error messages when using methods on empty :class:`mne.Epochs`-objects (:gh:`11306` by `Martin Schulz`_) +- Add support for passing a :class:`python:dict` as ``sensor_color`` to specify per-channel-type colors in :func:`mne.viz.plot_alignment` (:gh:`12067` by `Eric Larson`_) +- Add inferring EEGLAB files' montage unit automatically based on estimated head radius using :func:`read_raw_eeglab(..., montage_units="auto") ` (:gh:`11925` by `Jack Zhang`_, :gh:`11951` by `Eric Larson`_) +- Add :class:`~mne.time_frequency.EpochsSpectrumArray` and :class:`~mne.time_frequency.SpectrumArray` to support creating power spectra from :class:`NumPy array ` data (:gh:`11803` by `Alex Rockhill`_) +- Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) +- Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) +- Add support for drawing MEG sensors in :ref:`mne coreg` (:gh:`12098` by `Eric Larson`_) +- Improve string representation of :class:`mne.Covariance` (:gh:`12181` by `Eric Larson`_) +- Add ``check_version=True`` to :ref:`mne sys_info` to check for a new release on GitHub (:gh:`12146` by `Eric Larson`_) +- Bad channels are now colored gray in addition to being dashed when spatial colors are used in :func:`mne.viz.plot_evoked` and related functions (:gh:`12142` by `Eric Larson`_) +- By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_) +- Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) +- Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_) +- Add example KIT phantom dataset in :func:`mne.datasets.phantom_kit.data_path` and :ref:`tut-phantom-kit` (:gh:`12105` by `Judy D Zhu`_ and `Eric Larson`_) +- :func:`~mne.epochs.make_metadata` now accepts ``tmin=None`` and ``tmax=None``, which will bound the time window used for metadata generation by event names (instead of a fixed time). That way, you can now for example generate metadata spanning from one cue or fixation cross to the next, even if trial durations vary throughout the recording (:gh:`12118` by `Richard Höchenberger`_) +- Add support for passing multiple labels to :func:`mne.minimum_norm.source_induced_power` (:gh:`12026` by `Erica Peterson`_, `Eric Larson`_, and `Daniel McCloy`_ ) +- Added documentation to :meth:`mne.io.Raw.set_montage` and :func:`mne.add_reference_channels` to specify that montages should be set after adding reference channels (:gh:`12160` by `Jacob Woessner`_) +- Add argument ``splash`` to the function using the ``qt`` browser backend to allow enabling/disabling the splash screen (:gh:`12185` by `Mathieu Scheltienne`_) +- :class:`~mne.preprocessing.ICA`'s HTML representation (displayed in Jupyter notebooks and :class:`mne.Report`) now includes all optional fit parameters (e.g., max. number of iterations) (:gh:`12194`, by `Richard Höchenberger`_) + +Bugs +~~~~ +- Fix bug where :func:`mne.io.read_raw_gdf` would fail due to improper usage of ``np.clip`` (:gh:`12168` by :newcontrib:`Rasmus Aagaard`) +- Fix bugs with :func:`mne.preprocessing.realign_raw` where the start of ``other`` was incorrectly cropped; and onsets and durations in ``other.annotations`` were left unsynced with the resampled data (:gh:`11950` by :newcontrib:`Qian Chu`) +- Fix bug where ``encoding`` argument was ignored when reading annotations from an EDF file (:gh:`11958` by :newcontrib:`Andrew Gilbert`) +- Mark tests ``test_adjacency_matches_ft`` and ``test_fetch_uncompressed_file`` as network tests (:gh:`12041` by :newcontrib:`Maksym Balatsko`) +- Fix bug with :func:`mne.channels.read_ch_adjacency` (:gh:`11608` by :newcontrib:`Ivan Zubarev`) +- Fix bug where ``epochs.get_data(..., scalings=...)`` would errantly modify the preloaded data (:gh:`12121` by :newcontrib:`Pablo Mainar` and `Eric Larson`_) +- Fix bugs with saving splits for :class:`~mne.Epochs` (:gh:`11876` by `Dmitrii Altukhov`_) +- Fix bug with multi-plot 3D rendering where only one plot was updated (:gh:`11896` by `Eric Larson`_) +- Fix bug where ``verbose`` level was not respected inside parallel jobs (:gh:`12154` by `Eric Larson`_) +- Fix bug where subject birthdays were not correctly read by :func:`mne.io.read_raw_snirf` (:gh:`11912` by `Eric Larson`_) +- Fix bug where warnings were emitted when computing spectra for channels marked as bad (:gh:`12186` by `Eric Larson`_) +- Fix bug with :func:`mne.chpi.compute_head_pos` for CTF data where digitization points were modified in-place, producing an incorrect result during a save-load round-trip (:gh:`11934` by `Eric Larson`_) +- Fix bug where non-compliant stimulus data streams were not ignored by :func:`mne.io.read_raw_snirf` (:gh:`11915` by `Johann Benerradi`_) +- Fix bug with ``pca=False`` in :func:`mne.minimum_norm.compute_source_psd` (:gh:`11927` by `Alex Gramfort`_) +- Fix bug with notebooks when using PyVista 0.42 by implementing ``trame`` backend support (:gh:`11956` by `Eric Larson`_) +- Removed preload parameter from :func:`mne.io.read_raw_eyelink`, because data are always preloaded no matter what preload is set to (:gh:`11910` by `Scott Huberty`_) +- Fix bug with :meth:`mne.viz.Brain.get_view` where calling :meth:`~mne.viz.Brain.show_view` with returned parameters would change the view (:gh:`12000` by `Eric Larson`_) +- Fix bug with :meth:`mne.viz.Brain.show_view` where ``distance=None`` would change the view distance (:gh:`12000` by `Eric Larson`_) +- Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_) +- Fix bug with reported component number and errant reporting of PCA explained variance as ICA explained variance in :meth:`mne.Report.add_ica` (:gh:`12155`, :gh:`12167` by `Eric Larson`_ and `Richard Höchenberger`_) +- Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_) +- Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_) +- Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_) +- Fix rendering glitches when plotting Neuromag/TRIUX sensors in :func:`mne.viz.plot_alignment` and related functions (:gh:`12098` by `Eric Larson`_) +- Fix bug with delayed checking of :class:`info["bads"] ` (:gh:`12038` by `Eric Larson`_) +- Fix bug with :ref:`mne coreg` where points inside the head surface were not shown (:gh:`12147`, :gh:`12164` by `Eric Larson`_) +- Fix bug with :func:`mne.viz.plot_alignment` where ``sensor_colors`` were not handled properly on a per-channel-type basis (:gh:`12067` by `Eric Larson`_) +- Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` :gh:`12044` by `Paul Roujansky`_) +- Add missing ``overwrite`` and ``verbose`` parameters to :meth:`Transform.save() ` (:gh:`12004` by `Marijn van Vliet`_) +- Fix parsing of eye-link :class:`~mne.Annotations` when ``apply_offsets=False`` is provided to :func:`~mne.io.read_raw_eyelink` (:gh:`12003` by `Mathieu Scheltienne`_) +- Correctly prune channel-specific :class:`~mne.Annotations` when creating :class:`~mne.Epochs` without the channel(s) included in the channel specific annotations (:gh:`12010` by `Mathieu Scheltienne`_) +- Fix :func:`~mne.viz.plot_volume_source_estimates` with :class:`~mne.VolSourceEstimate` which include a list of vertices (:gh:`12025` by `Mathieu Scheltienne`_) +- Add support for non-ASCII characters in Annotations, Evoked comments, etc when saving to FIFF format (:gh:`12080` by `Daniel McCloy`_) +- Correctly handle passing ``"eyegaze"`` or ``"pupil"`` to :meth:`mne.io.Raw.pick` (:gh:`12019` by `Scott Huberty`_) +- Fix bug with :func:`mne.time_frequency.Spectrum.plot` and related functions where bad channels were not marked (:gh:`12142` by `Eric Larson`_) +- Fix bug with :func:`~mne.viz.plot_raw` where changing ``MNE_BROWSER_BACKEND`` via :func:`~mne.set_config` would have no effect within a Python session (:gh:`12078` by `Santeri Ruuskanen`_) +- Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) +- Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_) +- Fix bug with :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` where automatic fiducial detection would fail for certain files (:gh:`12165` by `Clemens Brunner`_) +- Fix concatenation of ``raws`` with ``np.nan`` in the device to head transformation (:gh:`12198` by `Mathieu Scheltienne`_) +- Fix bug with :func:`mne.viz.plot_compare_evokeds` where the title was not displayed when ``axes='topo'`` (:gh:`12192` by `Jacob Woessner`_) +- Fix bug with :func:`mne.io.read_raw_cnt` where the bad channels were not properly read (:gh:`12189` by `Jacob Woessner`_) +- Fix bug where iterating over :class:`~mne.io.Raw` would result in an error (:gh:`12205` by `Clemens Brunner`_) + + +API changes +~~~~~~~~~~~ +- The default for :meth:`mne.Epochs.get_data` of ``copy=False`` will change to ``copy=True`` in 1.7. Set it explicitly to avoid a warning (:gh:`12121` by :newcontrib:`Pablo Mainar` and `Eric Larson`_) +- ``mne.preprocessing.apply_maxfilter`` and ``mne maxfilter`` have been deprecated and will be removed in 1.7. Use :func:`mne.preprocessing.maxwell_filter` (see :ref:`this tutorial `) in Python or the command-line utility from MEGIN ``maxfilter`` and :func:`mne.bem.fit_sphere_to_headshape` instead (:gh:`11938` by `Eric Larson`_) +- :func:`mne.io.kit.read_mrk` reading pickled files is deprecated using something like ``np.savetxt(fid, pts, delimiter="\t", newline="\n")`` to save your points instead (:gh:`11937` by `Eric Larson`_) +- Replace legacy ``inst.pick_channels`` and ``inst.pick_types`` with ``inst.pick`` (where ``inst`` is an instance of :class:`~mne.io.Raw`, :class:`~mne.Epochs`, or :class:`~mne.Evoked`) wherever possible (:gh:`11907` by `Clemens Brunner`_) +- The ``reset_camera`` parameter has been removed in favor of ``distance="auto"`` in :func:`mne.viz.set_3d_view`, :meth:`mne.viz.Brain.show_view`, and related functions (:gh:`12000` by `Eric Larson`_) +- Several unused parameters from :func:`mne.gui.coregistration` are now deprecated: tabbed, split, scrollable, head_inside, guess_mri_subject, scale, and ``advanced_rendering``. All arguments are also now keyword-only. (:gh:`12147` by `Eric Larson`_) + +Authors +~~~~~~~ +* Adam Li +* Alex Rockhill +* Alexandre Gramfort +* Ana Radanovic +* Andy Gilbert+ +* Clemens Brunner +* Daniel McCloy +* Denis A. Engemann +* Dimitri Papadopoulos Orfanos +* Dmitrii Altukhov +* Dominik Welke +* Eric Larson +* Erica Peterson +* Gonzalo Reina +* Hamza Abdelhedi +* Ivan Skelin+ +* Ivan Zubarev+ +* Jack Zhang +* Jacob Woessner +* Johann Benerradi +* John Veillette +* Judy D Zhu +* Kristijan Armeni+ +* Mainak Jas +* Maksym Balatsko+ +* Marijn van Vliet +* Martin Schulz +* Mathieu Scheltienne +* Nikolai Chapochnikov +* Pablo Mainar+ +* Paul Roujansky +* Qian Chu+ +* Rasmus Aagaard+ +* Richard Höchenberger +* Rob Luke +* Santeri Ruuskanen +* Scott Huberty +* Stefan Appelhoff diff --git a/doc/conf.py b/doc/conf.py index 2267fcb1026..585274426fe 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -382,6 +382,7 @@ "RawBrainVision", "RawCurry", "RawNIRX", + "RawNeuralynx", "RawGDF", "RawSNIRF", "RawBOXY", diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 4a9e7f52d0e..2957b434751 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -243,7 +243,7 @@ Creating the virtual environment These instructions will set up a Python environment that is separated from your system-level Python and any other managed Python environments on your computer. This lets you switch between different versions of Python (MNE-Python requires -version 3.8 or higher) and also switch between the stable and development +version 3.9 or higher) and also switch between the stable and development versions of MNE-Python (so you can, for example, use the same computer to analyze your data with the stable release, and also work with the latest development version to fix bugs or add new features). Even if you've already @@ -304,11 +304,11 @@ be reflected the next time you open a Python interpreter and ``import mne`` Finally, we'll add a few dependencies that are not needed for running MNE-Python, but are needed for locally running our test suite:: - $ pip install -e .[test] + $ pip install -e ".[test]" And for building our documentation:: - $ pip install -e .[doc] + $ pip install -e ".[doc]" $ conda install graphviz .. note:: diff --git a/doc/development/whats_new.rst b/doc/development/whats_new.rst index f9cb6e1c4b0..61c14a876f9 100644 --- a/doc/development/whats_new.rst +++ b/doc/development/whats_new.rst @@ -9,6 +9,7 @@ Changes for each version of MNE-Python are listed below. :maxdepth: 1 ../changes/devel.rst + ../changes/v1.6.rst ../changes/v1.5.rst ../changes/v1.4.rst ../changes/v1.3.rst diff --git a/doc/install/installers.rst b/doc/install/installers.rst index 2d1d75323b8..39583ac9135 100644 --- a/doc/install/installers.rst +++ b/doc/install/installers.rst @@ -15,7 +15,7 @@ Got any questions? Let us know on the `MNE Forum`_! :class-content: text-center :name: linux-installers - .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.5.1/MNE-Python-1.5.1_0-Linux.sh + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.0/MNE-Python-1.6.0_0-Linux.sh :ref-type: ref :color: primary :shadow: @@ -29,14 +29,14 @@ Got any questions? Let us know on the `MNE Forum`_! .. code-block:: console - $ sh ./MNE-Python-1.5.1_0-Linux.sh + $ sh ./MNE-Python-1.6.0_0-Linux.sh .. tab-item:: macOS (Intel) :class-content: text-center :name: macos-intel-installers - .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.5.1/MNE-Python-1.5.1_0-macOS_Intel.pkg + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.0/MNE-Python-1.6.0_0-macOS_Intel.pkg :ref-type: ref :color: primary :shadow: @@ -52,7 +52,7 @@ Got any questions? Let us know on the `MNE Forum`_! :class-content: text-center :name: macos-apple-installers - .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.5.1/MNE-Python-1.5.1_0-macOS_M1.pkg + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.0/MNE-Python-1.6.0_0-macOS_M1.pkg :ref-type: ref :color: primary :shadow: @@ -68,7 +68,7 @@ Got any questions? Let us know on the `MNE Forum`_! :class-content: text-center :name: windows-installers - .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.5.1/MNE-Python-1.5.1_0-Windows.exe + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.0/MNE-Python-1.6.0_0-Windows.exe :ref-type: ref :color: primary :shadow: @@ -120,7 +120,7 @@ information, including a line that will read something like: .. code-block:: - Using Python: /some/directory/mne-python_1.5.1_0/bin/python + Using Python: /some/directory/mne-python_1.6.0_0/bin/python This path is what you need to enter in VS Code when selecting the Python interpreter. diff --git a/doc/install/manual_install.rst b/doc/install/manual_install.rst index 57932648bf6..c95db0ae2d6 100644 --- a/doc/install/manual_install.rst +++ b/doc/install/manual_install.rst @@ -67,7 +67,7 @@ others), you should run via :code:`pip`: .. code-block:: console - $ pip install mne[hdf5] + $ pip install "mne[hdf5]" or via :code:`conda`: diff --git a/doc/install/updating.rst b/doc/install/updating.rst index 0737ee7c6a0..c946d5e496e 100644 --- a/doc/install/updating.rst +++ b/doc/install/updating.rst @@ -78,8 +78,8 @@ Sometimes, new features or bugfixes become available that are important to your research and you just can't wait for the next official release of MNE-Python to start taking advantage of them. In such cases, you can use ``pip`` to install the *development version* of MNE-Python. Ensure to activate the MNE conda -environment first by running ``conda activate name_of_environment``. +environment first by running ``conda activate mne``. .. code-block:: console - $ pip install -U --no-deps git+https://github.com/mne-tools/mne-python@main + $ pip install -U --no-deps https://github.com/mne-tools/mne-python/archive/refs/heads/main.zip diff --git a/environment.yml b/environment.yml index 9f0971b2fb3..8978dfc64e8 100644 --- a/environment.yml +++ b/environment.yml @@ -2,7 +2,7 @@ name: mne channels: - conda-forge dependencies: - - python>=3.8 + - python>=3.9 - pip - numpy - scipy @@ -33,7 +33,7 @@ dependencies: - traitlets - pyvista>=0.32,!=0.35.2,!=0.38.0,!=0.38.1,!=0.38.2,!=0.38.3,!=0.38.4,!=0.38.5,!=0.38.6,!=0.42.0 - pyvistaqt>=0.4 - - qdarkstyle + - qdarkstyle!=3.2.2 - darkdetect - dipy - nibabel @@ -56,7 +56,7 @@ dependencies: - mne-qt-browser - pymatreader - eeglabio - - edflib-python + - edfio>=0.2.1 - pybv - mamba - lazy_loader diff --git a/mne/__init__.py b/mne/__init__.py index 594eddefdd2..10ff0c23738 100644 --- a/mne/__init__.py +++ b/mne/__init__.py @@ -23,11 +23,10 @@ __version__ = version("mne") except Exception: - try: - from ._version import __version__ - except ImportError: - __version__ = "0.0.0" + __version__ = "0.0.0" + (__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__) + # initialize logging from .utils import set_log_level, set_log_file diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 8630053e78e..483ddc34b52 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -1380,7 +1380,7 @@ class Info(dict, SetChannelsMixin, MontageMixin, ContainsMixin): The distance limit. accept : int Whether or not the fit was accepted. - coord_trans : instance of Transformation + coord_trans : instance of Transform The resulting MEG<->head transformation. * ``hpi_subsystem`` dict: diff --git a/mne/commands/mne_browse_raw.py b/mne/commands/mne_browse_raw.py index 17a8f8bcb88..0c3d81a16e9 100644 --- a/mne/commands/mne_browse_raw.py +++ b/mne/commands/mne_browse_raw.py @@ -125,7 +125,8 @@ def run(): parser.add_option( "--clipping", dest="clipping", - help="Enable trace clipping mode, either 'clamp' or " "'transparent'", + help="Enable trace clipping mode. Can be 'clamp', 'transparent', a float, " + "or 'none'.", default=_RAW_CLIP_DEF, ) parser.add_option( diff --git a/mne/datasets/eegbci/eegbci.py b/mne/datasets/eegbci/eegbci.py index 3af5661e5f7..93c6c731932 100644 --- a/mne/datasets/eegbci/eegbci.py +++ b/mne/datasets/eegbci/eegbci.py @@ -7,19 +7,13 @@ import os import re import time +from importlib.resources import files from os import path as op from pathlib import Path from ...utils import _url_to_local_path, logger, verbose from ..utils import _do_path_update, _downloader_params, _get_path, _log_time_size -# TODO: remove try/except when our min version is py 3.9 -try: - from importlib.resources import files -except ImportError: - from importlib_resources import files - - EEGMI_URL = "https://physionet.org/files/eegmmidb/1.0.0/" diff --git a/mne/export/_edf.py b/mne/export/_edf.py index 7097f7bd85d..04590f042da 100644 --- a/mne/export/_edf.py +++ b/mne/export/_edf.py @@ -3,46 +3,15 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -from contextlib import contextmanager +import datetime as dt import numpy as np -from ..utils import _check_edflib_installed, warn +from ..utils import _check_edfio_installed, warn -_check_edflib_installed() -from EDFlib.edfwriter import EDFwriter # noqa: E402 - - -def _try_to_set_value(header, key, value, channel_index=None): - """Set key/value pairs in EDF header.""" - # all EDFLib set functions are set - # for example "setPatientName()" - func_name = f"set{key}" - func = getattr(header, func_name) - - # some setter functions are indexed by channels - if channel_index is None: - return_val = func(value) - else: - return_val = func(channel_index, value) - - # a nonzero return value indicates an error - if return_val != 0: - raise RuntimeError( - f"Setting {key} with {value} " f"returned an error value " f"{return_val}." - ) - - -@contextmanager -def _auto_close(fid): - # try to close the handle no matter what - try: - yield fid - finally: - try: - fid.close() - except Exception: - pass # we did our best +_check_edfio_installed() +from edfio import Edf, EdfAnnotation, EdfSignal, Patient, Recording # noqa: E402 +from edfio._utils import round_float_to_8_characters # noqa: E402 def _export_raw(fname, raw, physical_range, add_ch_type): @@ -51,9 +20,6 @@ def _export_raw(fname, raw, physical_range, add_ch_type): TODO: if in future the Info object supports transducer or technician information, allow writing those here. """ - # scale to save data in EDF - phys_dims = "uV" - # get EEG-related data in uV units = dict( eeg="uV", ecog="uV", seeg="uV", eog="uV", ecg="uV", emg="uV", bio="uV", dbs="uV" @@ -61,7 +27,6 @@ def _export_raw(fname, raw, physical_range, add_ch_type): digital_min = -32767 digital_max = 32767 - file_type = EDFwriter.EDFLIB_FILETYPE_EDFPLUS # load data first raw.load_data() @@ -73,6 +38,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type): stim_index = np.argwhere(np.array(orig_ch_types) == "stim") stim_index = np.atleast_1d(stim_index.squeeze()).tolist() drop_chs.extend([raw.ch_names[idx] for idx in stim_index]) + warn(f"Exporting STIM channels is not supported, dropping indices {stim_index}") # Add warning if any channel types are not voltage based. # Users are expected to only export data that is voltage based, @@ -97,9 +63,11 @@ def _export_raw(fname, raw, physical_range, add_ch_type): ch_names = [ch for ch in raw.ch_names if ch not in drop_chs] ch_types = np.array(raw.get_channel_types(picks=ch_names)) - n_channels = len(ch_names) n_times = raw.n_times + # get the entire dataset in uV + data = raw.get_data(units=units, picks=ch_names) + # Sampling frequency in EDF only supports integers, so to allow for # float sampling rates from Raw, we adjust the output sampling rate # for all channels and the data record duration. @@ -107,10 +75,20 @@ def _export_raw(fname, raw, physical_range, add_ch_type): if float(sfreq).is_integer(): out_sfreq = int(sfreq) data_record_duration = None + # make non-integer second durations work + if (pad_width := int(np.ceil(n_times / sfreq) * sfreq - n_times)) > 0: + warn( + f"EDF format requires equal-length data blocks, " + f"so {pad_width / sfreq} seconds of " + "zeros were appended to all channels when writing the " + "final block." + ) + data = np.pad(data, (0, int(pad_width))) else: - out_sfreq = np.floor(sfreq).astype(int) - data_record_duration = int(np.around(out_sfreq / sfreq, decimals=6) * 1e6) - + data_record_duration = round_float_to_8_characters( + np.floor(sfreq) / sfreq, round + ) + out_sfreq = np.floor(sfreq) / data_record_duration warn( f"Data has a non-integer sampling rate of {sfreq}; writing to " "EDF format may cause a small change to sample times." @@ -122,16 +100,13 @@ def _export_raw(fname, raw, physical_range, add_ch_type): linefreq = raw.info["line_freq"] filter_str_info = f"HP:{highpass}Hz LP:{lowpass}Hz N:{linefreq}Hz" - # get the entire dataset in uV - data = raw.get_data(units=units, picks=ch_names) - if physical_range == "auto": # get max and min for each channel type data ch_types_phys_max = dict() ch_types_phys_min = dict() for _type in np.unique(ch_types): - _picks = np.nonzero(ch_types == _type)[0] + _picks = [n for n, t in zip(ch_names, ch_types) if t == _type] _data = raw.get_data(units=units, picks=_picks) ch_types_phys_max[_type] = _data.max() ch_types_phys_min[_type] = _data.min() @@ -157,178 +132,106 @@ def _export_raw(fname, raw, physical_range, add_ch_type): f"The minimum μV of the data {data.min()} is " f"less than the physical min passed in {pmin}.", ) + data = np.clip(data, pmin, pmax) + signals = [] + for idx, ch in enumerate(ch_names): + ch_type = ch_types[idx] + signal_label = f"{ch_type.upper()} {ch}" if add_ch_type else ch + if len(signal_label) > 16: + raise RuntimeError( + f"Signal label for {ch} ({ch_type}) is " + f"longer than 16 characters, which is not " + f"supported in EDF. Please shorten the " + f"channel name before exporting to EDF." + ) - # create instance of EDF Writer - with _auto_close(EDFwriter(fname, file_type, n_channels)) as hdl: - # set channel data - for idx, ch in enumerate(ch_names): - ch_type = ch_types[idx] - signal_label = f"{ch_type.upper()} {ch}" if add_ch_type else ch - if len(signal_label) > 16: - raise RuntimeError( - f"Signal label for {ch} ({ch_type}) is " - f"longer than 16 characters, which is not " - f"supported in EDF. Please shorten the " - f"channel name before exporting to EDF." - ) - - if physical_range == "auto": - # take the channel type minimum and maximum - pmin = ch_types_phys_min[ch_type] - pmax = ch_types_phys_max[ch_type] - for key, val in [ - ("PhysicalMaximum", pmax), - ("PhysicalMinimum", pmin), - ("DigitalMaximum", digital_max), - ("DigitalMinimum", digital_min), - ("PhysicalDimension", phys_dims), - ("SampleFrequency", out_sfreq), - ("SignalLabel", signal_label), - ("PreFilter", filter_str_info), - ]: - _try_to_set_value(hdl, key, val, channel_index=idx) - - # set patient info - subj_info = raw.info.get("subject_info") - if subj_info is not None: - # get the full name of subject if available - first_name = subj_info.get("first_name", "") - middle_name = subj_info.get("middle_name", "") - last_name = subj_info.get("last_name", "") - name = " ".join(filter(None, [first_name, middle_name, last_name])) - - birthday = subj_info.get("birthday") - hand = subj_info.get("hand") - weight = subj_info.get("weight") - height = subj_info.get("height") - sex = subj_info.get("sex") - - additional_patient_info = [] - for key, value in [("height", height), ("weight", weight), ("hand", hand)]: - if value: - additional_patient_info.append(f"{key}={value}") - if len(additional_patient_info) == 0: - additional_patient_info = None - else: - additional_patient_info = " ".join(additional_patient_info) - - if birthday is not None: - if hdl.setPatientBirthDate(birthday[0], birthday[1], birthday[2]) != 0: - raise RuntimeError( - f"Setting patient birth date to {birthday} " - f"returned an error" - ) - for key, val in [ - ("PatientCode", subj_info.get("his_id", "")), - ("PatientName", name), - ("PatientGender", sex), - ("AdditionalPatientInfo", additional_patient_info), - ]: - # EDFwriter compares integer encodings of sex and will - # raise a TypeError if value is None as returned by - # subj_info.get(key) if key is missing. - if val is not None: - _try_to_set_value(hdl, key, val) - - # set measurement date - meas_date = raw.info["meas_date"] - if meas_date: - subsecond = int(meas_date.microsecond / 100) - if ( - hdl.setStartDateTime( - year=meas_date.year, - month=meas_date.month, - day=meas_date.day, - hour=meas_date.hour, - minute=meas_date.minute, - second=meas_date.second, - subsecond=subsecond, - ) - != 0 - ): - raise RuntimeError( - f"Setting start date time {meas_date} " f"returned an error" - ) - - device_info = raw.info.get("device_info") - if device_info is not None: - device_type = device_info.get("type") - _try_to_set_value(hdl, "Equipment", device_type) - - # set data record duration - if data_record_duration is not None: - _try_to_set_value(hdl, "DataRecordDuration", data_record_duration) - - # compute number of data records to loop over - n_blocks = np.ceil(n_times / out_sfreq).astype(int) - - # increase the number of annotation signals if necessary - annots = raw.annotations - if annots is not None: - n_annotations = len(raw.annotations) - n_annot_chans = int(n_annotations / n_blocks) + 1 - if n_annot_chans > 1: - hdl.setNumberOfAnnotationSignals(n_annot_chans) - - # Write each data record sequentially - for idx in range(n_blocks): - end_samp = (idx + 1) * out_sfreq - if end_samp > n_times: - end_samp = n_times - start_samp = idx * out_sfreq - - # then for each datarecord write each channel - for jdx in range(n_channels): - # create a buffer with sampling rate - buf = np.zeros(out_sfreq, np.float64, "C") + if physical_range == "auto": + # take the channel type minimum and maximum + pmin = ch_types_phys_min[ch_type] + pmax = ch_types_phys_max[ch_type] + + signals.append( + EdfSignal( + data[idx], + out_sfreq, + label=signal_label, + transducer_type="", + physical_dimension="uV", + physical_range=(pmin, pmax), + digital_range=(digital_min, digital_max), + prefiltering=filter_str_info, + ) + ) - # get channel data for this block - ch_data = data[jdx, start_samp:end_samp] + # set patient info + subj_info = raw.info.get("subject_info") + if subj_info is not None: + # get the full name of subject if available + first_name = subj_info.get("first_name", "") + middle_name = subj_info.get("middle_name", "") + last_name = subj_info.get("last_name", "") + name = "_".join(filter(None, [first_name, middle_name, last_name])) + + birthday = subj_info.get("birthday") + if birthday is not None: + birthday = dt.date(*birthday) + hand = subj_info.get("hand") + weight = subj_info.get("weight") + height = subj_info.get("height") + sex = subj_info.get("sex") + + additional_patient_info = [] + for key, value in [("height", height), ("weight", weight), ("hand", hand)]: + if value: + additional_patient_info.append(f"{key}={value}") + + patient = Patient( + code=subj_info.get("his_id") or "X", + sex={0: "X", 1: "M", 2: "F", None: "X"}[sex], + birthdate=birthday, + name=name or "X", + additional=additional_patient_info, + ) + else: + patient = None - # assign channel data to the buffer and write to EDF - buf[: len(ch_data)] = ch_data - err = hdl.writeSamples(buf) - if err != 0: - raise RuntimeError( - f"writeSamples() for channel{ch_names[jdx]} " - f"returned error: {err}" - ) + # set measurement date + if (meas_date := raw.info["meas_date"]) is not None: + startdate = dt.date(meas_date.year, meas_date.month, meas_date.day) + starttime = dt.time( + meas_date.hour, meas_date.minute, meas_date.second, meas_date.microsecond + ) + else: + startdate = None + starttime = None - # there was an incomplete datarecord - if len(ch_data) != len(buf): - warn( - f"EDF format requires equal-length data blocks, " - f"so {(len(buf) - len(ch_data)) / sfreq} seconds of " - "zeros were appended to all channels when writing the " - "final block." + device_info = raw.info.get("device_info") + if device_info is not None: + device_type = device_info.get("type") or "X" + recording = Recording(startdate=startdate, equipment_code=device_type) + else: + recording = Recording(startdate=startdate) + + annotations = [] + for desc, onset, duration, ch_names in zip( + raw.annotations.description, + raw.annotations.onset, + raw.annotations.duration, + raw.annotations.ch_names, + ): + if ch_names: + for ch_name in ch_names: + annotations.append( + EdfAnnotation(onset, duration, desc + f"@@{ch_name}") ) - - # write annotations - if annots is not None: - for desc, onset, duration, ch_names in zip( - raw.annotations.description, - raw.annotations.onset, - raw.annotations.duration, - raw.annotations.ch_names, - ): - # annotations are written in terms of 100 microseconds - onset = onset * 10000 - duration = duration * 10000 - if ch_names: - for ch_name in ch_names: - if ( - hdl.writeAnnotation(onset, duration, desc + f"@@{ch_name}") - != 0 - ): - raise RuntimeError( - f"writeAnnotation() returned an error " - f"trying to write {desc}@@{ch_name} at {onset} " - f"for {duration} seconds." - ) - else: - if hdl.writeAnnotation(onset, duration, desc) != 0: - raise RuntimeError( - f"writeAnnotation() returned an error " - f"trying to write {desc} at {onset} " - f"for {duration} seconds." - ) + else: + annotations.append(EdfAnnotation(onset, duration, desc)) + + Edf( + signals=signals, + patient=patient, + recording=recording, + starttime=starttime, + data_record_duration=data_record_duration, + annotations=annotations, + ).write(fname) diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 67bd417bb50..4e86c3bb6d3 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -6,7 +6,6 @@ from contextlib import nullcontext from datetime import datetime, timezone -from os import remove from pathlib import Path import numpy as np @@ -33,7 +32,7 @@ ) from mne.tests.test_epochs import _get_data from mne.utils import ( - _check_edflib_installed, + _check_edfio_installed, _record_warnings, _resource_path, object_diff, @@ -120,17 +119,11 @@ def test_export_raw_eeglab(tmp_path): raw.export(temp_fname, overwrite=True) -@pytest.mark.skipif( - not _check_edflib_installed(strict=False), reason="edflib-python not installed" -) -def test_double_export_edf(tmp_path): - """Test exporting an EDF file multiple times.""" - rng = np.random.RandomState(123456) - format = "edf" +def _create_raw_for_edf_tests(stim_channel_index=None): + rng = np.random.RandomState(12345) ch_types = [ "eeg", "eeg", - "stim", "ecog", "ecog", "seeg", @@ -140,12 +133,27 @@ def test_double_export_edf(tmp_path): "dbs", "bio", ] - info = create_info(len(ch_types), sfreq=1000, ch_types=ch_types) - info = info.set_meas_date("2023-09-04 14:53:09.000") - data = rng.random(size=(len(ch_types), 1000)) * 1e-5 + if stim_channel_index is not None: + ch_types.insert(stim_channel_index, "stim") + ch_names = np.arange(len(ch_types)).astype(str).tolist() + info = create_info(ch_names, sfreq=1000, ch_types=ch_types) + data = rng.random(size=(len(ch_names), 2000)) * 1e-5 + return RawArray(data, info) + + +edfio_mark = pytest.mark.skipif( + not _check_edfio_installed(strict=False), reason="edfio not installed" +) + + +@edfio_mark() +def test_double_export_edf(tmp_path): + """Test exporting an EDF file multiple times.""" + raw = _create_raw_for_edf_tests(stim_channel_index=2) + raw.info.set_meas_date("2023-09-04 14:53:09.000") # include subject info and measurement date - info["subject_info"] = dict( + raw.info["subject_info"] = dict( his_id="12345", first_name="mne", last_name="python", @@ -155,15 +163,14 @@ def test_double_export_edf(tmp_path): height=1.75, hand=3, ) - raw = RawArray(data, info) # export once - temp_fname = tmp_path / f"test.{format}" - raw.export(temp_fname, add_ch_type=True) + temp_fname = tmp_path / "test.edf" + with pytest.warns(RuntimeWarning, match="Exporting STIM channels"): + raw.export(temp_fname, add_ch_type=True) raw_read = read_raw_edf(temp_fname, infer_types=True, preload=True) # export again - raw_read.load_data() raw_read.export(temp_fname, add_ch_type=True, overwrite=True) raw_read = read_raw_edf(temp_fname, infer_types=True, preload=True) @@ -171,53 +178,22 @@ def test_double_export_edf(tmp_path): raw.drop_channels("2") assert raw.ch_names == raw_read.ch_names - # only compare the original length, since extra zeros are appended - orig_raw_len = len(raw) - assert_array_almost_equal( - raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=4 - ) - assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) + assert_array_almost_equal(raw.get_data(), raw_read.get_data(), decimal=10) + assert_array_equal(raw.times, raw_read.times) # check info for key in set(raw.info) - {"chs"}: assert raw.info[key] == raw_read.info[key] - # check channel types except for 'bio', which loses its type orig_ch_types = raw.get_channel_types() read_ch_types = raw_read.get_channel_types() assert_array_equal(orig_ch_types, read_ch_types) - # check handling of missing subject metadata - del info["subject_info"]["sex"] - raw_2 = RawArray(data, info) - raw_2.export(temp_fname, add_ch_type=True, overwrite=True) - -@pytest.mark.skipif( - not _check_edflib_installed(strict=False), reason="edflib-python not installed" -) +@edfio_mark() def test_export_edf_annotations(tmp_path): """Test that exporting EDF preserves annotations.""" - rng = np.random.RandomState(123456) - format = "edf" - ch_types = [ - "eeg", - "eeg", - "stim", - "ecog", - "ecog", - "seeg", - "eog", - "ecg", - "emg", - "dbs", - "bio", - ] - ch_names = np.arange(len(ch_types)).astype(str).tolist() - info = create_info(ch_names, sfreq=1000, ch_types=ch_types) - data = rng.random(size=(len(ch_names), 2000)) * 1.0e-5 - raw = RawArray(data, info) - + raw = _create_raw_for_edf_tests() annotations = Annotations( onset=[0.01, 0.05, 0.90, 1.05], duration=[0, 1, 0, 0], @@ -227,7 +203,7 @@ def test_export_edf_annotations(tmp_path): raw.set_annotations(annotations) # export - temp_fname = tmp_path / f"test.{format}" + temp_fname = tmp_path / "test.edf" raw.export(temp_fname) # read in the file @@ -238,24 +214,19 @@ def test_export_edf_annotations(tmp_path): assert_array_equal(raw.annotations.ch_names, raw_read.annotations.ch_names) -@pytest.mark.skipif( - not _check_edflib_installed(strict=False), reason="edflib-python not installed" -) +@edfio_mark() def test_rawarray_edf(tmp_path): """Test saving a Raw array with integer sfreq to EDF.""" - rng = np.random.RandomState(12345) - format = "edf" - ch_types = ["eeg", "eeg", "stim", "ecog", "seeg", "eog", "ecg", "emg", "dbs", "bio"] - ch_names = np.arange(len(ch_types)).astype(str).tolist() - info = create_info(ch_names, sfreq=1000, ch_types=ch_types) - data = rng.random(size=(len(ch_names), 1000)) * 1e-5 + raw = _create_raw_for_edf_tests() # include subject info and measurement date - subject_info = dict( - first_name="mne", last_name="python", birthday=(1992, 1, 20), sex=1, hand=3 + raw.info["subject_info"] = dict( + first_name="mne", + last_name="python", + birthday=(1992, 1, 20), + sex=1, + hand=3, ) - info["subject_info"] = subject_info - raw = RawArray(data, info) time_now = datetime.now() meas_date = datetime( year=time_now.year, @@ -267,125 +238,104 @@ def test_rawarray_edf(tmp_path): tzinfo=timezone.utc, ) raw.set_meas_date(meas_date) - temp_fname = tmp_path / f"test.{format}" + temp_fname = tmp_path / "test.edf" raw.export(temp_fname, add_ch_type=True) raw_read = read_raw_edf(temp_fname, infer_types=True, preload=True) - # stim channel should be dropped - raw.drop_channels("2") - assert raw.ch_names == raw_read.ch_names - # only compare the original length, since extra zeros are appended - orig_raw_len = len(raw) - assert_array_almost_equal( - raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=4 - ) - assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) + assert_array_almost_equal(raw.get_data(), raw_read.get_data(), decimal=10) + assert_array_equal(raw.times, raw_read.times) - # check channel types except for 'bio', which loses its type orig_ch_types = raw.get_channel_types() read_ch_types = raw_read.get_channel_types() assert_array_equal(orig_ch_types, read_ch_types) assert raw.info["meas_date"] == raw_read.info["meas_date"] - # channel name can't be longer than 16 characters with the type added - raw_bad = raw.copy() - raw_bad.rename_channels({"1": "abcdefghijklmnopqrstuvwxyz"}) - with pytest.raises(RuntimeError, match="Signal label"), pytest.warns( - RuntimeWarning, match="Data has a non-integer" - ): - raw_bad.export(temp_fname, overwrite=True) - # include bad birthday that is non-EDF compliant - bad_info = info.copy() - bad_info["subject_info"]["birthday"] = (1700, 1, 20) - raw = RawArray(data, bad_info) - with pytest.raises(RuntimeError, match="Setting patient birth date"): - raw.export(temp_fname, overwrite=True) +@edfio_mark() +def test_edf_export_warns_on_non_voltage_channels(tmp_path): + """Test saving a Raw array containing a non-voltage channel.""" + temp_fname = tmp_path / "test.edf" - # include bad measurement date that is non-EDF compliant - raw = RawArray(data, info) - meas_date = datetime(year=1984, month=1, day=1, tzinfo=timezone.utc) - raw.set_meas_date(meas_date) - with pytest.raises(RuntimeError, match="Setting start date time"): - raw.export(temp_fname, overwrite=True) - - # test that warning is raised if there are non-voltage based channels - raw = RawArray(data, info) + raw = _create_raw_for_edf_tests() raw.set_channel_types({"9": "hbr"}, on_unit_change="ignore") with pytest.warns(RuntimeWarning, match="Non-voltage channels"): raw.export(temp_fname, overwrite=True) # data should match up to the non-accepted channel raw_read = read_raw_edf(temp_fname, preload=True) - orig_raw_len = len(raw) - assert_array_almost_equal( - raw.get_data()[:-1, :], raw_read.get_data()[:, :orig_raw_len], decimal=4 - ) - assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) - - # the data should still match though - raw_read = read_raw_edf(temp_fname, preload=True) - raw.drop_channels("2") assert raw.ch_names == raw_read.ch_names - orig_raw_len = len(raw) - assert_array_almost_equal( - raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=4 - ) - assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) + assert_array_almost_equal(raw.get_data()[:-1], raw_read.get_data()[:-1], decimal=10) + assert_array_equal(raw.times, raw_read.times) + + +@edfio_mark() +def test_channel_label_too_long_for_edf_raises_error(tmp_path): + """Test trying to save an EDF where a channel label is longer than 16 characters.""" + raw = _create_raw_for_edf_tests() + raw.rename_channels({"1": "abcdefghijklmnopqrstuvwxyz"}) + with pytest.raises(RuntimeError, match="Signal label"): + raw.export(tmp_path / "test.edf") -@pytest.mark.skipif( - not _check_edflib_installed(strict=False), reason="edflib-python not installed" +@edfio_mark() +def test_measurement_date_outside_range_valid_for_edf(tmp_path): + """Test trying to save an EDF with a measurement date before 1985-01-01.""" + raw = _create_raw_for_edf_tests() + raw.set_meas_date(datetime(year=1984, month=1, day=1, tzinfo=timezone.utc)) + with pytest.raises(ValueError, match="EDF only allows dates from 1985 to 2084"): + raw.export(tmp_path / "test.edf", overwrite=True) + + +@pytest.mark.parametrize( + ("physical_range", "exceeded_bound"), + [ + ((-1e6, 0), "maximum"), + ((0, 1e6), "minimum"), + ], ) +@edfio_mark() +def test_export_edf_signal_clipping(tmp_path, physical_range, exceeded_bound): + """Test if exporting data exceeding physical min/max clips and emits a warning.""" + raw = read_raw_fif(fname_raw) + raw.pick(picks=["eeg", "ecog", "seeg"]).load_data() + temp_fname = tmp_path / "test.edf" + with pytest.warns(RuntimeWarning, match=f"The {exceeded_bound}"): + raw.export(temp_fname, physical_range=physical_range) + raw_read = read_raw_edf(temp_fname, preload=True) + assert raw_read.get_data().min() >= physical_range[0] + assert raw_read.get_data().max() <= physical_range[1] + + +@edfio_mark() @pytest.mark.parametrize( - ["dataset", "format"], + ("input_path", "warning_msg"), [ - ["test", "edf"], - pytest.param("misc", "edf", marks=[pytest.mark.slowtest, misc._pytest_mark()]), + (fname_raw, "Data has a non-integer"), + pytest.param( + misc_path / "ecog" / "sample_ecog_ieeg.fif", + "EDF format requires", + marks=[pytest.mark.slowtest, misc._pytest_mark()], + ), ], ) -def test_export_raw_edf(tmp_path, dataset, format): +def test_export_raw_edf(tmp_path, input_path, warning_msg): """Test saving a Raw instance to EDF format.""" - if dataset == "test": - raw = read_raw_fif(fname_raw) - elif dataset == "misc": - fname = misc_path / "ecog" / "sample_ecog_ieeg.fif" - raw = read_raw_fif(fname) + raw = read_raw_fif(input_path) # only test with EEG channels raw.pick(picks=["eeg", "ecog", "seeg"]).load_data() - orig_ch_names = raw.ch_names - temp_fname = tmp_path / f"test.{format}" - - # test runtime errors - with pytest.warns() as record: - raw.export(temp_fname, physical_range=(-1e6, 0)) - if dataset == "test": - assert any("Data has a non-integer" in str(rec.message) for rec in record) - assert any("The maximum" in str(rec.message) for rec in record) - remove(temp_fname) - - with pytest.warns() as record: - raw.export(temp_fname, physical_range=(0, 1e6)) - if dataset == "test": - assert any("Data has a non-integer" in str(rec.message) for rec in record) - assert any("The minimum" in str(rec.message) for rec in record) - remove(temp_fname) - - if dataset == "test": - with pytest.warns(RuntimeWarning, match="Data has a non-integer"): - raw.export(temp_fname) - elif dataset == "misc": - with pytest.warns(RuntimeWarning, match="EDF format requires"): - raw.export(temp_fname) + temp_fname = tmp_path / "test.edf" + + with pytest.warns(RuntimeWarning, match=warning_msg): + raw.export(temp_fname) if "epoc" in raw.ch_names: raw.drop_channels(["epoc"]) raw_read = read_raw_edf(temp_fname, preload=True) - assert orig_ch_names == raw_read.ch_names + assert raw.ch_names == raw_read.ch_names # only compare the original length, since extra zeros are appended orig_raw_len = len(raw) @@ -395,7 +345,7 @@ def test_export_raw_edf(tmp_path, dataset, format): # will result in a resolution of 0.09 uV. This resolution # though is acceptable for most EEG manufacturers. assert_array_almost_equal( - raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=4 + raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=8 ) # Due to the data record duration limitations of EDF files, one @@ -407,6 +357,27 @@ def test_export_raw_edf(tmp_path, dataset, format): assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) +@edfio_mark() +def test_export_raw_edf_does_not_fail_on_empty_header_fields(tmp_path): + """Test writing a Raw instance with empty header fields to EDF.""" + rng = np.random.RandomState(123456) + + ch_types = ["eeg"] + info = create_info(len(ch_types), sfreq=1000, ch_types=ch_types) + info["subject_info"] = { + "his_id": "", + "first_name": "", + "middle_name": "", + "last_name": "", + } + info["device_info"] = {"type": "123"} + + data = rng.random(size=(len(ch_types), 1000)) * 1e-5 + raw = RawArray(data, info) + + raw.export(tmp_path / "test.edf", add_ch_type=True) + + @pytest.mark.xfail(reason="eeglabio (usage?) bugs that should be fixed") @pytest.mark.parametrize("preload", (True, False)) def test_export_epochs_eeglab(tmp_path, preload): diff --git a/mne/gui/tests/test_coreg.py b/mne/gui/tests/test_coreg.py index aea6fba08ff..f2372f4f3d6 100644 --- a/mne/gui/tests/test_coreg.py +++ b/mne/gui/tests/test_coreg.py @@ -93,6 +93,9 @@ def test_coreg_gui_pyvista_file_support( """Test reading supported files.""" from mne.gui import coregistration + if Path(inst_path).suffix == ".snirf": + pytest.importorskip("snirf") + if inst_path == "gen_montage": # generate a montage fig to use as inst. tmp_info = read_info(raw_path) diff --git a/mne/io/base.py b/mne/io/base.py index de6f3aa589d..fd8dde30258 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -797,6 +797,9 @@ def _parse_get_set_params(self, item): item1 = int(item1) if isinstance(item1, (int, np.integer)): start, stop, step = item1, item1 + 1, 1 + # Need to special case -1, because -1:0 will be empty + if start == -1: + stop = None else: raise ValueError("Must pass int or slice to __getitem__") diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 4bfad0fea2c..06d5000fcb6 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -38,7 +38,12 @@ def read_raw_neuralynx( -------- mne.io.Raw : Documentation of attributes and methods of RawNeuralynx. """ - return RawNeuralynx(fname, preload, verbose, exclude_fname_patterns) + return RawNeuralynx( + fname, + preload=preload, + exclude_fname_patterns=exclude_fname_patterns, + verbose=verbose, + ) @fill_doc @@ -47,7 +52,12 @@ class RawNeuralynx(BaseRaw): @verbose def __init__( - self, fname, preload=False, verbose=None, exclude_fname_patterns: list = None + self, + fname, + *, + preload=False, + exclude_fname_patterns=None, + verbose=None, ): _soft_import("neo", "Reading NeuralynxIO files", strict=True) from neo.io import NeuralynxIO diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index bac32f83f65..ce5d111bcbf 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -1022,3 +1022,10 @@ def test_concatenate_raw_dev_head_t(): raw.info["dev_head_t"]["trans"][0, 0] = np.nan raw2 = raw.copy() concatenate_raws([raw, raw2]) + + +def test_last_samp(): + """Test that getting the last sample works.""" + raw = read_raw_fif(raw_fname).crop(0, 0.1).load_data() + last_data = raw._data[:, [-1]] + assert_array_equal(raw[:, -1][0], last_data) diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index eadd54a260e..d5bcfccb730 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -304,8 +304,8 @@ def compute_average_dev_head_t(raw, pos): Returns ------- - dev_head_t : array of shape (4, 4) - New trans matrix using the averaged good head positions. + dev_head_t : instance of Transform + New ``dev_head_t`` transformation using the averaged good head positions. """ sfreq = raw.info["sfreq"] seg_good = np.ones(len(raw.times)) diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 8ee465ddb8e..25430db6f9e 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -1973,7 +1973,7 @@ def _update_sss_info( The moments that were used. st_only : bool Whether tSSS only was performed. - recon_trans : instance of Transformation + recon_trans : instance of Transform The reconstruction trans. extended_proj : ndarray Extended external bases. diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 4f307367b6a..7a2f3f27ee4 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -103,6 +103,8 @@ def _make_invisible(fig, **kwargs): @testing.requires_testing_data def test_render_report(renderer_pyvistaqt, tmp_path, invisible_fig): """Test rendering *.fif files for mne report.""" + pytest.importorskip("pymatreader") + raw_fname_new = tmp_path / "temp_raw.fif" raw_fname_new_bids = tmp_path / "temp_meg.fif" ms_fname_new = tmp_path / "temp_ms_raw.fif" diff --git a/mne/utils/__init__.pyi b/mne/utils/__init__.pyi index 42694921f00..3e4d1292ee2 100644 --- a/mne/utils/__init__.pyi +++ b/mne/utils/__init__.pyi @@ -32,7 +32,7 @@ __all__ = [ "_check_depth", "_check_dict_keys", "_check_dt", - "_check_edflib_installed", + "_check_edfio_installed", "_check_eeglabio_installed", "_check_event_id", "_check_fname", @@ -230,7 +230,7 @@ from .check import ( _check_compensation_grade, _check_depth, _check_dict_keys, - _check_edflib_installed, + _check_edfio_installed, _check_eeglabio_installed, _check_event_id, _check_fname, diff --git a/mne/utils/check.py b/mne/utils/check.py index eb8e14de256..8c2bc5f919d 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -11,11 +11,9 @@ from builtins import input # no-op here but facilitates testing from difflib import get_close_matches from importlib import import_module -from importlib.metadata import version from pathlib import Path import numpy as np -from packaging.version import parse from ..defaults import HEAD_SIZE_DEFAULT, _handle_default from ..fixes import _compare_version, _median_complex @@ -368,7 +366,6 @@ def indent(x): # Mapping import namespaces to their pypi package name pip_name = dict( sklearn="scikit-learn", - EDFlib="EDFlib-Python", mne_bids="mne-bids", mne_nirs="mne-nirs", mne_features="mne-features", @@ -411,21 +408,9 @@ def _check_eeglabio_installed(strict=True): return _soft_import("eeglabio", "exporting to EEGLab", strict=strict) -def _check_edflib_installed(strict=True): +def _check_edfio_installed(strict=True): """Aux function.""" - out = _soft_import("EDFlib", "exporting to EDF", strict=strict) - if out: - # EDFlib-Python 1.0.7 is not compatible with NumPy 2.0 - # https://gitlab.com/Teuniz/EDFlib-Python/-/issues/10 - ver = version("EDFlib-Python") - if parse(ver) <= parse("1.0.7") and parse(np.__version__).major >= 2: - if strict: # pragma: no cover - raise RuntimeError( - f"EDFlib version={ver} is not compatible with NumPy 2.0, consider " - "upgrading EDFlib-Python" - ) - out = False - return out + return _soft_import("edfio", "exporting to EDF", strict=strict) def _check_pybv_installed(strict=True): diff --git a/mne/utils/config.py b/mne/utils/config.py index 2b7d10ab070..77b94508114 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -10,7 +10,6 @@ import os import os.path as op import platform -import re import shutil import subprocess import sys @@ -626,16 +625,6 @@ def sys_info( _validate_type(check_version, (bool, "numeric"), "check_version") ljust = 24 if dependencies == "developer" else 21 platform_str = platform.platform() - if platform.system() == "Darwin" and sys.version_info[:2] < (3, 8): - # platform.platform() in Python < 3.8 doesn't call - # platform.mac_ver() if we're on Darwin, so we don't get a nice macOS - # version number. Therefore, let's do this manually here. - macos_ver = platform.mac_ver()[0] - macos_architecture = re.findall("Darwin-.*?-(.*)", platform_str) - if macos_architecture: - macos_architecture = macos_architecture[0] - platform_str = f"macOS-{macos_ver}-{macos_architecture}" - del macos_ver, macos_architecture out = partial(print, end="", file=fid) out("Platform".ljust(ljust) + platform_str + "\n") @@ -805,7 +794,7 @@ def _get_latest_version(timeout): try: with urlopen(url, timeout=timeout) as f: # nosec response = json.load(f) - except URLError as err: + except (URLError, TimeoutError) as err: # Triage error type if "SSL" in str(err): return "SSL error" diff --git a/mne/utils/docs.py b/mne/utils/docs.py index d8eb668ae04..6665e7550e2 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1193,7 +1193,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Valid values for ``mode`` are: - ``'max'`` - Maximum value across vertices at each time point within each label. + Maximum absolute value across vertices at each time point within each label. - ``'mean'`` Average across vertices at each time point within each label. Ignores orientation of sources for standard source estimates, which varies @@ -1203,7 +1203,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): - ``'mean_flip'`` Finds the dominant direction of source space normal vector orientations within each label, applies a sign-flip to time series at vertices whose - orientation is more than 180° different from the dominant direction, and + orientation is more than 90° different from the dominant direction, and then averages across vertices at each time point within each label. - ``'pca_flip'`` Applies singular value decomposition to the time courses within each label, @@ -1463,7 +1463,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Supported formats: - BrainVision (``.vhdr``, ``.vmrk``, ``.eeg``, uses `pybv `_) - EEGLAB (``.set``, uses :mod:`eeglabio`) - - EDF (``.edf``, uses `EDFlib-Python `_) + - EDF (``.edf``, uses `edfio `_) """ # noqa: E501 docdict[ diff --git a/mne/utils/misc.py b/mne/utils/misc.py index 05d856c0226..3dbff7b2bc5 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -14,6 +14,7 @@ import traceback import weakref from contextlib import ExitStack, contextmanager +from importlib.resources import files from math import log from queue import Empty, Queue from string import Formatter @@ -26,12 +27,6 @@ from ._logging import logger, verbose, warn from .check import _check_option, _validate_type -# TODO: remove try/except when our min version is py 3.9 -try: - from importlib.resources import files -except ImportError: - from importlib_resources import files - # TODO: no longer needed when py3.9 is minimum supported version def _empty_hash(kind="md5"): diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index 23cb65c6c44..a8e902aa33b 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -166,11 +166,11 @@ def mesh( The scalar valued associated to the vertices. vmin : float | None vmin is used to scale the colormap. - If None, the min of the data will be used + If None, the min of the data will be used. vmax : float | None vmax is used to scale the colormap. - If None, the max of the data will be used - colormap : + If None, the max of the data will be used. + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. interpolate_before_map : Enabling makes for a smoother scalars display. Default is True. @@ -225,17 +225,17 @@ def contour( The opacity of the contour. vmin : float | None vmin is used to scale the colormap. - If None, the min of the data will be used + If None, the min of the data will be used. vmax : float | None vmax is used to scale the colormap. - If None, the max of the data will be used - colormap : + If None, the max of the data will be used. + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. normalized_colormap : bool Specify if the values of the colormap are between 0 and 1. kind : 'line' | 'tube' The type of the primitives to use to display the contours. - color : + color : tuple | str The color of the mesh as a tuple (red, green, blue) of float values between 0 and 1 or a valid color name (i.e. 'white' or 'w'). @@ -270,11 +270,11 @@ def surface( The opacity of the surface. vmin : float | None vmin is used to scale the colormap. - If None, the min of the data will be used + If None, the min of the data will be used. vmax : float | None vmax is used to scale the colormap. - If None, the max of the data will be used - colormap : + If None, the max of the data will be used. + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. scalars : ndarray, shape (n_vertices,) The scalar valued associated to the vertices. @@ -354,11 +354,11 @@ def tube( The optional scalar data to use. vmin : float | None vmin is used to scale the colormap. - If None, the min of the data will be used + If None, the min of the data will be used. vmax : float | None vmax is used to scale the colormap. - If None, the max of the data will be used - colormap : + If None, the max of the data will be used. + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. opacity : float The opacity of the tube(s). @@ -446,7 +446,7 @@ def quiver3d( The optional scalar data to use. backface_culling : bool If True, enable backface culling on the quiver. - colormap : + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. vmin : float | None vmin is used to scale the colormap. @@ -518,15 +518,15 @@ def scalarbar(self, source, color="white", title=None, n_labels=4, bgcolor=None) Parameters ---------- - source : + source The object of the scene used for the colormap. - color : + color : tuple | str The color of the label text. title : str | None The title of the scalar bar. n_labels : int | None The number of labels to display on the scalar bar. - bgcolor : + bgcolor : tuple | str The color of the background when there is transparency. """ pass diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 11a229d80d1..ccbe48eabd4 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2900,6 +2900,8 @@ def plot_compare_evokeds( "misc", # from ICA "emg", "ref_meg", + "eyegaze", + "pupil", ) ch_types = [ t for t in info.get_channel_types(picks=picks, unique=True) if t in all_types @@ -3002,7 +3004,13 @@ def plot_compare_evokeds( colorbar_ticks, ) = _handle_styles_pce(styles, linestyles, colors, cmap, conditions) # From now on there is only 1 channel type - assert len(ch_types) == 1 + if not len(ch_types): + got_idx = _picks_to_idx(info, picks=orig_picks) + got = np.unique(np.array(info.get_channel_types())[got_idx]).tolist() + raise RuntimeError( + f"No valid channel type(s) provided. Got {got}. Valid channel types are:" + f"\n{all_types}." + ) ch_type = ch_types[0] # some things that depend on ch_type: units = _handle_default("units")[ch_type] diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index b44a33385b2..b22db0bfa15 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -438,6 +438,17 @@ def test_plot_compare_evokeds(): yvals = line.get_ydata() assert (yvals < ylim[1]).all() assert (yvals > ylim[0]).all() + # test plotting eyetracking data + plt.close("all") # close the previous figures as to avoid a too many figs warning + info_tmp = mne.create_info(["pupil_left"], evoked.info["sfreq"], ["pupil"]) + evoked_et = mne.EvokedArray(np.ones_like(evoked.times).reshape(1, -1), info_tmp) + figs = plot_compare_evokeds(evoked_et, show_sensors=False) + assert len(figs) == 1 + # test plotting only invalid channel types + info_tmp = mne.create_info(["ias"], evoked.info["sfreq"], ["ias"]) + ev_invalid = mne.EvokedArray(np.ones_like(evoked.times).reshape(1, -1), info_tmp) + with pytest.raises(RuntimeError, match="No valid"): + plot_compare_evokeds(ev_invalid, picks="all") plt.close("all") # test other CI args diff --git a/pyproject.toml b/pyproject.toml index 8c172fcac70..daba4d874fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [ maintainers = [{ name = "Dan McCloy", email = "dan@mccloy.info" }] license = { text = "BSD-3-Clause" } readme = { file = "README.rst", content-type = "text/x-rst" } -requires-python = ">=3.8" +requires-python = ">=3.9" keywords = [ "neuroscience", "neuroimaging", @@ -41,7 +41,6 @@ dependencies = [ "decorator", "packaging", "jinja2", - "importlib_resources>=5.10.2; python_version<'3.9'", "lazy_loader>=0.3", "defusedxml", ] @@ -61,6 +60,7 @@ full = [ "mne[hdf5]", "qtpy", "PyQt6", + "PyQt6-Qt6!=6.6.1", "pyobjc-framework-Cocoa>=5.2.0; platform_system=='Darwin'", "sip", "scikit-learn", @@ -93,8 +93,13 @@ full = [ "trame-vuetify", "mne-qt-browser", "darkdetect", - "qdarkstyle", + "qdarkstyle!=3.2.2", "threadpoolctl", + # duplicated in test_extra: + "eeglabio", + "edfio>=0.2.1", + "pybv", + "snirf", ] # Dependencies for running the test infrastructure @@ -122,7 +127,7 @@ test_extra = [ "nbclient", "sphinx-gallery", "eeglabio", - "EDFlib-Python", + "edfio>=0.2.1", "pybv", "imageio>=2.6.1", "imageio-ffmpeg>=0.4.1", @@ -153,11 +158,11 @@ doc = [ "ipython!=8.7.0", "selenium", ] -dev = ["mne[test,doc]"] +dev = ["mne[test,doc]", "rcssmin"] [project.urls] Homepage = "https://mne.tools/" -Download = "https://pypi.org/project/scikit-learn/#files" +Download = "https://pypi.org/project/mne/#files" "Bug Tracker" = "https://github.com/mne-tools/mne-python/issues/" Documentation = "https://mne.tools/" Forum = "https://mne.discourse.group/" @@ -173,12 +178,10 @@ include = ["mne*"] namespaces = false [tool.setuptools_scm] -write_to = "mne/_version.py" version_scheme = "release-branch-semver" -[options] -zip_safe = false # the package can run out of an .egg file -include_package_data = true +[tool.setuptools] +include-package-data = true [tool.setuptools.package-data] "mne" = [ diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 371a61b60e3..32b51e631f7 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -6,31 +6,34 @@ if [ "${TEST_MODE}" == "pip" ]; then python -m pip install --only-binary="numba,llvmlite,numpy,scipy,vtk" -e .[test,full] elif [ "${TEST_MODE}" == "pip-pre" ]; then STD_ARGS="$STD_ARGS --pre" - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" PyQt6 PyQt6-sip PyQt6-Qt6 + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" PyQt6 PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" echo "Numpy etc." - # As of 2023/10/25 no pandas (or statsmodels) because they pin to NumPy < 2 - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib - echo "dipy" - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy - echo "h5py" - python -m pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py + # See github_actions_dependencies.sh for comments + python -m pip install $STD_ARGS --only-binary "numpy" numpy + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pandas statsmodels + # echo "dipy" + # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy + # echo "h5py" + # python -m pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py + # echo "OpenMEEG" + # pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg echo "vtk" python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk - echo "openmeeg" - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg + echo "nilearn" + python -m pip install $STD_ARGS git+https://github.com/nilearn/nilearn echo "pyvista/pyvistaqt" python -m pip install --progress-bar off git+https://github.com/pyvista/pyvista python -m pip install --progress-bar off git+https://github.com/pyvista/pyvistaqt echo "misc" - python -m pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard pillow traitlets pybv eeglabio + python -m pip install $STD_ARGS imageio-ffmpeg xlrd mffpy pillow traitlets pybv eeglabio echo "nibabel with workaround" python -m pip install --progress-bar off git+https://github.com/nipy/nibabel.git echo "joblib" python -m pip install --progress-bar off git+https://github.com/joblib/joblib@master echo "EDFlib-Python" - python -m pip install $STD_ARGS git+https://gitlab.com/Teuniz/EDFlib-Python@master + python -m pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio ./tools/check_qt_import.sh PyQt6 - python -m pip install $STD_ARGS -e .[hdf5,test] + python -m pip install $STD_ARGS -e .[test] else echo "Unknown run type ${TEST_MODE}" exit 1 diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 768165635a4..0c4185d6a04 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -22,20 +22,20 @@ else echo "Numpy" pip uninstall -yq numpy echo "PyQt6" - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 "PyQt6-Qt6!=6.6.1" echo "NumPy/SciPy/pandas etc." - # As of 2023/10/25 no pandas (or statsmodels, nilearn) because they pin to NumPy < 2 - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" scipy scikit-learn matplotlib pillow - echo "dipy" - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy - echo "H5py" - pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py - echo "OpenMEEG" - pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pillow pandas statsmodels + # No dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat + INSTALL_KIND="test_extra" + # echo "dipy" + # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy + # echo "H5py" + # pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py + # echo "OpenMEEG" + # pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg # No Numba because it forces an old NumPy version - echo "nilearn and openmeeg" - # pip install $STD_ARGS git+https://github.com/nilearn/nilearn - pip install $STD_ARGS openmeeg + echo "nilearn" + pip install $STD_ARGS git+https://github.com/nilearn/nilearn echo "VTK" pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk python -c "import vtk" @@ -43,18 +43,18 @@ else pip install $STD_ARGS git+https://github.com/pyvista/pyvista echo "pyvistaqt" pip install $STD_ARGS git+https://github.com/pyvista/pyvistaqt - echo "imageio-ffmpeg, xlrd, mffpy, python-picard" - pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard patsy traitlets pybv eeglabio + echo "imageio-ffmpeg, xlrd, mffpy" + pip install $STD_ARGS imageio-ffmpeg xlrd mffpy patsy traitlets pybv eeglabio echo "mne-qt-browser" pip install $STD_ARGS git+https://github.com/mne-tools/mne-qt-browser echo "nibabel with workaround" pip install $STD_ARGS git+https://github.com/nipy/nibabel.git echo "joblib" pip install $STD_ARGS git+https://github.com/joblib/joblib@master - echo "EDFlib-Python" - pip install $STD_ARGS git+https://gitlab.com/Teuniz/EDFlib-Python@master - # Until Pandas is fixed, make sure we didn't install it - ! python -c "import pandas" + echo "edfio" + pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio + # Make sure we're on a NumPy 2.0 variant + python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__" fi echo ""