diff --git a/.github/workflows/credit.yml b/.github/workflows/credit.yml index 5e538f6a974..9e6aa00b663 100644 --- a/.github/workflows/credit.yml +++ b/.github/workflows/credit.yml @@ -40,6 +40,6 @@ jobs: git checkout -b credit git commit -am "MAINT: Update code credit" git push origin credit - PR_NUM=$(gh pr create --base main --head credit --title "MAINT: Update code credit" --body "Created by credit [GitHub action](https://github.com/mne-tools/mne-python/actions/runs/${{ github.run_id }})." --label "no-changelog-entry-needed") + PR_NUM=$(gh pr create --base main --head credit --title "MAINT: Update code credit" --body "Created by credit [GitHub action](https://github.com/mne-tools/mne-python/actions/runs/${{ github.run_id }}).

*Adjustments may need to be made to `doc/changes/credit_tools.py` or `.mailmap` etc. to get CircleCI to pass.*" --label "no-changelog-entry-needed") echo "Opened https://github.com/mne-tools/mne-python/pull/${PR_NUM}" >> $GITHUB_STEP_SUMMARY if: steps.status.outputs.dirty == 'true' diff --git a/.github/workflows/spec_zero.yml b/.github/workflows/spec_zero.yml index a8eeb7b9c56..3f3698190fc 100644 --- a/.github/workflows/spec_zero.yml +++ b/.github/workflows/spec_zero.yml @@ -57,6 +57,6 @@ jobs: git checkout -b spec_zero git commit -am "MAINT: Update dependency specifiers" git push origin spec_zero - PR_NUM=$(gh pr create --base main --head spec_zero --title "MAINT: Update dependency specifiers" --body "Created by spec_zero [GitHub action](https://github.com/mne-tools/mne-python/actions/runs/${{ github.run_id }})." --label "no-changelog-entry-needed") + PR_NUM=$(gh pr create --base main --head spec_zero --title "MAINT: Update dependency specifiers" --body "Created by spec_zero [GitHub action](https://github.com/mne-tools/mne-python/actions/runs/${{ github.run_id }}).

*Adjustments may need to be made to shims in mne/fixes.py in this or another PR. `git grep TODO VERSION` is a good starting point for finding potential updates.*" --label "no-changelog-entry-needed") echo "Opened https://github.com/mne-tools/mne-python/pull/${PR_NUM}" >> $GITHUB_STEP_SUMMARY if: steps.status.outputs.dirty == 'true' diff --git a/doc/changes/dev/13448.newfeature.rst b/doc/changes/dev/13448.newfeature.rst new file mode 100644 index 00000000000..d329015520f --- /dev/null +++ b/doc/changes/dev/13448.newfeature.rst @@ -0,0 +1 @@ +Add support for Nihon Kohden EEG-1200A V01.00, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/dev/13458.apichange.rst b/doc/changes/dev/13458.apichange.rst new file mode 100644 index 00000000000..f2b9ec5a007 --- /dev/null +++ b/doc/changes/dev/13458.apichange.rst @@ -0,0 +1 @@ +Add ``encoding`` parameter to :func:`mne.io.read_raw_nihon` for better handling of annotation decoding, by `Tom Ma`_. diff --git a/environment.yml b/environment.yml index 8e7e083563b..e7620d41fa4 100644 --- a/environment.yml +++ b/environment.yml @@ -45,11 +45,11 @@ dependencies: - PySide6 !=6.9.1 - python-neo - python-picard - - 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 + - pyvista >=0.42.1 + - pyvistaqt >=0.11 - qdarkstyle !=3.2.2 - qtpy - - scikit-learn >=1.3.0 + - scikit-learn >=1.3 - scipy >=1.11 - sip - snirf diff --git a/mne/io/artemis123/tests/test_artemis123.py b/mne/io/artemis123/tests/test_artemis123.py index cf905f82530..b39f35febb9 100644 --- a/mne/io/artemis123/tests/test_artemis123.py +++ b/mne/io/artemis123/tests/test_artemis123.py @@ -41,7 +41,6 @@ def _assert_trans(actual, desired, dist_tol=0.017, angle_tol=5.0): assert angle <= angle_tol, f"{angle:0.3f} > {angle_tol:0.3f}° rotation" -@pytest.mark.timeout(60) # ~25 s on Travis Linux OpenBLAS @testing.requires_testing_data def test_artemis_reader(): """Test reading raw Artemis123 files.""" @@ -49,6 +48,7 @@ def test_artemis_reader(): read_raw_artemis123, input_fname=short_hpi_1kz_fname, pos_fname=dig_fname, + add_head_trans=False, verbose="error", ) diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index 49d87f822cb..2b05d8da14d 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -9,6 +9,7 @@ import numpy as np import pytest +from flaky import flaky from numpy.testing import ( assert_allclose, assert_array_almost_equal, @@ -23,7 +24,6 @@ from mne.channels import read_custom_montage from mne.datasets import testing from mne.io import read_raw_eeglab -from mne.io.eeglab import _eeglab as eeglab_mod from mne.io.eeglab._eeglab import _readmat from mne.io.eeglab.eeglab import _dol_to_lod, _get_montage_information from mne.io.tests.test_raw import _test_raw_reader @@ -769,25 +769,20 @@ def test_eeglab_drop_nan_annotations(tmp_path): raw = read_raw_eeglab(file_path, preload=True) +@flaky @testing.requires_testing_data @pytest.mark.timeout(10) -def test_io_set_preload_false_is_faster(monkeypatch): +@pytest.mark.slowtest # has the advantage of not running on macOS where it errs a lot +def test_io_set_preload_false_is_faster(): """Using preload=False should skip the expensive data read branch.""" - real_loadmat = eeglab_mod.loadmat - call_counts = {"n": 0} - - def counting_loadmat(*args, **kwargs): - call_counts["n"] += 1 - return real_loadmat(*args, **kwargs) - - monkeypatch.setattr(eeglab_mod, "loadmat", counting_loadmat) + # warm start + read_raw_eeglab(raw_fname_mat, preload=False) durations = {} - with _record_warnings(): - for preload in (False, True): - start = time.perf_counter() - _ = read_raw_eeglab(raw_fname_mat, preload=preload) - durations[preload] = time.perf_counter() - start + for preload in (True, False): + start = time.perf_counter() + _ = read_raw_eeglab(raw_fname_mat, preload=preload) + durations[preload] = time.perf_counter() - start # preload=True should not be faster than preload=False (timings may vary # across systems, so avoid strict thresholds) diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index c53c9e8d5c2..91db6a083d2 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -23,7 +23,9 @@ def _ensure_path(fname): @fill_doc -def read_raw_nihon(fname, preload=False, verbose=None) -> "RawNihon": +def read_raw_nihon( + fname, preload=False, *, encoding="utf-8", verbose=None +) -> "RawNihon": """Reader for an Nihon Kohden EEG file. Parameters @@ -32,6 +34,9 @@ def read_raw_nihon(fname, preload=False, verbose=None) -> "RawNihon": Path to the Nihon Kohden data file (``.EEG``). preload : bool If True, all data are loaded at initialization. + %(encoding_nihon)s + + .. versionadded:: 1.11 %(verbose)s Returns @@ -44,7 +49,7 @@ def read_raw_nihon(fname, preload=False, verbose=None) -> "RawNihon": -------- mne.io.Raw : Documentation of attributes and methods of RawNihon. """ - return RawNihon(fname, preload, verbose) + return RawNihon(fname, preload, encoding=encoding, verbose=verbose) _valid_headers = [ @@ -57,7 +62,7 @@ def read_raw_nihon(fname, preload=False, verbose=None) -> "RawNihon": "EEG-2100 V02.00", "DAE-2100D V01.30", "DAE-2100D V02.00", - # 'EEG-1200A V01.00', # Not working for the moment. + "EEG-1200A V01.00", ] @@ -66,8 +71,10 @@ def _read_nihon_metadata(fname): fname = _ensure_path(fname) pnt_fname = fname.with_suffix(".PNT") if not pnt_fname.exists(): - warn("No PNT file exists. Metadata will be blank") - return metadata + pnt_fname = fname.with_suffix(".pnt") + if not pnt_fname.exists(): + warn("No PNT file exists. Metadata will be blank") + return metadata logger.info("Found PNT file, reading metadata.") with open(pnt_fname) as fid: version = np.fromfile(fid, "|S16", 1).astype("U16")[0] @@ -315,7 +322,7 @@ def _parse_sub_event_log(sub_event_log): return t_sub_desc, t_sub_onset -def _read_nihon_annotations(fname): +def _read_nihon_annotations(fname, encoding="utf-8"): fname = _ensure_path(fname) log_fname = fname.with_suffix(".LOG") if not log_fname.exists(): @@ -346,15 +353,10 @@ def _read_nihon_annotations(fname): t_onset += t_sub_onset t_desc = t_desc.rstrip(b"\x00") - for enc in _encodings: - try: - t_desc = t_desc.decode(enc) - except UnicodeDecodeError: - pass - else: - break - else: - warn(f"Could not decode log as one of {_encodings}") + try: + t_desc = t_desc.decode(encoding) + except UnicodeDecodeError: + warn(f"Could not decode log as {encoding}") continue all_onsets.append(t_onset) @@ -414,6 +416,9 @@ class RawNihon(BaseRaw): Path to the Nihon Kohden data ``.eeg`` file. preload : bool If True, all data are loaded at initialization. + %(encoding_nihon)s + + .. versionadded:: 1.11 %(verbose)s See Also @@ -422,7 +427,7 @@ class RawNihon(BaseRaw): """ @verbose - def __init__(self, fname, preload=False, verbose=None): + def __init__(self, fname, preload=False, *, encoding="utf-8", verbose=None): fname = _check_fname(fname, "read", True, "fname") data_name = fname.name logger.info(f"Loading {data_name}") @@ -468,7 +473,7 @@ def __init__(self, fname, preload=False, verbose=None): ) # Get annotations from LOG file - annots = _read_nihon_annotations(fname) + annots = _read_nihon_annotations(fname, encoding) # Annotate acquisition skips controlblock = header["controlblocks"][0] diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 6adb4e361e1..72c8a68ae4c 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -614,6 +614,7 @@ def test_tfr_decim_and_shift_time(epochs, method, freqs, decim): assert_array_equal(freqs, tfr.freqs) +@pytest.mark.slowtest @pytest.mark.parametrize("inst", ("raw_tfr", "epochs_tfr", "average_tfr")) def test_tfr_io(inst, average_tfr, request, tmp_path): """Test TFR I/O.""" diff --git a/mne/utils/docs.py b/mne/utils/docs.py index de47ba6e192..337d50b462a 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1285,6 +1285,11 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): encoding according to the EDF+ standard). """ +docdict["encoding_nihon"] = """ +encoding : str + Text encoding of Nihon Kohden annotations. See :ref:`standard-encodings`. +""" + docdict["encoding_nirx"] = """ encoding : str Text encoding of the NIRX header file. See :ref:`standard-encodings`. diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 66d05df5b73..500319467fd 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -212,7 +212,8 @@ def __init__( ): from .._3d import _get_3d_option - _require_version("pyvista", "use 3D rendering", "0.32") + # TODO VERSION change whenever PyVista min gets updated: + _require_version("pyvista", "use 3D rendering", "0.42") multi_samples = _get_3d_option("multi_samples") # multi_samples > 1 is broken on macOS + Intel Iris + volume rendering if platform.system() == "Darwin": diff --git a/pyproject.toml b/pyproject.toml index 43c36d66eec..3455d707657 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,11 +23,11 @@ dependencies = [ "decorator", "jinja2", "lazy_loader >= 0.3", - "matplotlib >= 3.8", # released: 2023/09/15 - "numpy >= 1.26,<3", # released: 2023/09/16 + "matplotlib >= 3.8", # released 2023-09-15, will become 3.9 on 2026-05-15 + "numpy >= 1.26, < 3", # released 2023-09-16, will become 2.0 on 2026-06-16 "packaging", "pooch >= 1.5", - "scipy >= 1.11", # released: 2023/06/25 + "scipy >= 1.11", # released 2023-06-28, will become 1.12 on 2026-01-19 "tqdm", ] description = "MNE-Python project for MEG and EEG data analysis." @@ -78,7 +78,7 @@ doc = [ "sphinx-gallery >= 0.16", "sphinx_copybutton", "sphinxcontrib-bibtex >= 2.5", - "sphinxcontrib-towncrier >=0.5.0a0", + "sphinxcontrib-towncrier >= 0.5.0a0", "sphinxcontrib-youtube", ] full = ["mne[full-no-qt]", "PyQt6 != 6.6.0", "PyQt6-Qt6 != 6.6.0, != 6.7.0"] @@ -112,17 +112,17 @@ full-no-qt = [ "nilearn", "numba", "openmeeg >= 2.5.7", - "pandas >= 2.1", # released: 2023/08/30 + "pandas >= 2.1", # released 2023-08-30, will become 2.2 on 2026-01-19 "pillow", # for `Brain.save_image` and `mne.Report` "pyarrow", # only needed to avoid a deprecation warning in pandas "pybv", "pyobjc-framework-Cocoa >= 5.2.0; platform_system == 'Darwin'", "python-picard", - "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", + "pyvista >= 0.42.1", # released 2023-09-06, will become 0.43 on 2025-12-06 + "pyvistaqt >= 0.11", # released 2023-06-30, no newer version available "qdarkstyle != 3.2.2", "qtpy", - "scikit-learn >=1.3.0", # released: 2023/06/30 + "scikit-learn >= 1.3", # released 2023-06-30, will become 1.4 on 2026-01-17 "sip", "snirf", "statsmodels", @@ -152,7 +152,7 @@ test = [ "pytest-timeout", "ruff", "toml-sort", - "tomli; python_version<'3.11'", + "tomli; python_version < '3.11'", "twine", "vulture", "wheel",