From 156ae18572c8322ae75c964258f3fdc607006587 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 19 Apr 2024 14:17:08 -0400 Subject: [PATCH 001/153] MAINT: Restore changelog --- doc/changes/devel.rst | 5 +++++ doc/changes/devel.rst.template | 5 +++++ doc/development/whats_new.rst | 1 + 3 files changed, 11 insertions(+) create mode 100644 doc/changes/devel.rst create mode 100644 doc/changes/devel.rst.template diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst new file mode 100644 index 00000000000..0e80d522b51 --- /dev/null +++ b/doc/changes/devel.rst @@ -0,0 +1,5 @@ +.. See doc/development/contributing.rst for description of how to add entries. + +.. _current: + +.. towncrier-draft-entries:: Version |release| (development) diff --git a/doc/changes/devel.rst.template b/doc/changes/devel.rst.template new file mode 100644 index 00000000000..0e80d522b51 --- /dev/null +++ b/doc/changes/devel.rst.template @@ -0,0 +1,5 @@ +.. See doc/development/contributing.rst for description of how to add entries. + +.. _current: + +.. towncrier-draft-entries:: Version |release| (development) diff --git a/doc/development/whats_new.rst b/doc/development/whats_new.rst index 920194e7fb2..659157e5e39 100644 --- a/doc/development/whats_new.rst +++ b/doc/development/whats_new.rst @@ -8,6 +8,7 @@ Changes for each version of MNE-Python are listed below. .. toctree:: :maxdepth: 1 + ../changes/devel.rst ../changes/v1.7.rst ../changes/v1.6.rst ../changes/v1.5.rst From d331b02d2edc83742e6ad7bf0fc7afc581eaa098 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Sun, 21 Apr 2024 16:31:45 -0400 Subject: [PATCH 002/153] MAINT: Installers [ci skip] --- doc/install/installers.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/install/installers.rst b/doc/install/installers.rst index 26199483d60..8144f8ce31d 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.6.1/MNE-Python-1.6.1_0-Linux.sh + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.7.0/MNE-Python-1.7.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.6.1_0-Linux.sh + $ sh ./MNE-Python-1.7.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.6.1/MNE-Python-1.6.1_0-macOS_Intel.pkg + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.7.0/MNE-Python-1.7.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.6.1/MNE-Python-1.6.1_0-macOS_M1.pkg + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.7.0/MNE-Python-1.7.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.6.1/MNE-Python-1.6.1_0-Windows.exe + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.7.0/MNE-Python-1.7.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.6.1_0/bin/python + Using Python: /some/directory/mne-python_1.7.0_0/bin/python This path is what you need to enter in VS Code when selecting the Python interpreter. From f093b176565ef934bb5c0bcc6c2a5fc717f5e782 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 22 Apr 2024 10:04:26 -0400 Subject: [PATCH 003/153] MAINT: Ignore nibabel np2.0 issue (#12560) --- mne/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mne/conftest.py b/mne/conftest.py index 93657339b26..62f1d3f4cf9 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -200,6 +200,8 @@ def pytest_configure(config): ignore:np\.find_common_type is deprecated.*:DeprecationWarning # pyvista <-> NumPy 2.0 ignore:__array_wrap__ must accept context and return_scalar arguments.*:DeprecationWarning + # nibabel <-> NumPy 2.0 + ignore:__array__ implementation doesn't accept a copy.*:DeprecationWarning """ # noqa: E501 for warning_line in warning_lines.split("\n"): warning_line = warning_line.strip() From 6e2ecad41bad6b19474173e38b6e3df1efd3e863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Mon, 22 Apr 2024 18:33:15 +0200 Subject: [PATCH 004/153] In `Report`, fix scrolling when clicking on the same TOC entry multiple times (#12561) --- doc/changes/devel/12561.bugfix.rst | 1 + mne/report/js_and_css/report.js | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 doc/changes/devel/12561.bugfix.rst diff --git a/doc/changes/devel/12561.bugfix.rst b/doc/changes/devel/12561.bugfix.rst new file mode 100644 index 00000000000..e647b2770e1 --- /dev/null +++ b/doc/changes/devel/12561.bugfix.rst @@ -0,0 +1 @@ +Fix scrolling behavior in :class:`~mne.Report` when clicking on a TOC entry multiple times, by `Richard Höchenberger`_. diff --git a/mne/report/js_and_css/report.js b/mne/report/js_and_css/report.js index e2732f923ab..08020c19421 100644 --- a/mne/report/js_and_css/report.js +++ b/mne/report/js_and_css/report.js @@ -165,10 +165,12 @@ const _handleTocLinkClick = (e) => { const targetDomId = tocLinkElement.getAttribute('href'); const targetElement = document.querySelector(targetDomId); const top = $(targetElement).offset().top; - /* Update URL to reflect the current scroll position */ - var url = document.URL.replace(/#.*$/, ""); - url = url + targetDomId; - window.location.href = url; + + // Update URL to reflect the current scroll position. + // We use history.pushState to change the URL without causing the browser to scroll. + history.pushState(null, "", targetDomId); + + // Now scroll to the correct position. window.scrollTo(0, top - margin); } From b72f02a5eab8ab931839e4f9bad6d8e8faa68861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Mon, 22 Apr 2024 18:52:58 +0200 Subject: [PATCH 005/153] Allow activating slider keyboard control in `Report` by clicking on linked carousel of images; and add a fix for Safari to allow for focussing of the slider (#12556) --- doc/changes/devel/12556.newfeature.rst | 1 + mne/html_templates/report/slider.html.jinja | 17 ++++++----------- mne/report/js_and_css/report.js | 11 +++++++++++ 3 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 doc/changes/devel/12556.newfeature.rst diff --git a/doc/changes/devel/12556.newfeature.rst b/doc/changes/devel/12556.newfeature.rst new file mode 100644 index 00000000000..cbd86d984e7 --- /dev/null +++ b/doc/changes/devel/12556.newfeature.rst @@ -0,0 +1 @@ +In :class:`~mne.Report` you can now easily navigate through images and figures connected to a slider with the left and right arrow keys. Clicking on the slider or respective image will focus the slider, enabling keyboard navigation, by `Richard Höchenberger`_ diff --git a/mne/html_templates/report/slider.html.jinja b/mne/html_templates/report/slider.html.jinja index 85da7de40c7..8012918e280 100644 --- a/mne/html_templates/report/slider.html.jinja +++ b/mne/html_templates/report/slider.html.jinja @@ -1,4 +1,5 @@ -
+
- diff --git a/mne/report/js_and_css/report.js b/mne/report/js_and_css/report.js index 08020c19421..10c877c05ce 100644 --- a/mne/report/js_and_css/report.js +++ b/mne/report/js_and_css/report.js @@ -195,6 +195,17 @@ const addSliderEventHandlers = () => { const sliderValue = parseInt(e.target.value); $(carousel).carousel(sliderValue); }) + + // Allow focussing the slider with a click on the slider or carousel, so keyboard + // controls (left / right arrow) can be enabled. + // This also appears to be the only way to focus the slider in Safari: + // https://itnext.io/fixing-focus-for-safari-b5916fef1064?gi=c1b8b043fa9b + slider.addEventListener('click', () => { + slider.focus({preventScroll: true}) + }) + carousel.addEventListener('click', () => { + slider.focus({preventScroll: true}) + }) }) } From 815db609f9142b88de54c297f9016abfc38da6c3 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 22 Apr 2024 16:55:19 -0400 Subject: [PATCH 006/153] BUG: Fix bug with get_coef on CSP (#12562) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12562.bugfix.rst | 1 + mne/decoding/base.py | 38 +++++++++++++++++++++++------- mne/decoding/csp.py | 32 +++++++++++++++++++++---- mne/decoding/tests/test_base.py | 21 ++++++++++++----- mne/decoding/tests/test_csp.py | 31 ++++++++++++++++++++---- mne/decoding/transformer.py | 12 ++++++++-- 6 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 doc/changes/devel/12562.bugfix.rst diff --git a/doc/changes/devel/12562.bugfix.rst b/doc/changes/devel/12562.bugfix.rst new file mode 100644 index 00000000000..8b58e1bc109 --- /dev/null +++ b/doc/changes/devel/12562.bugfix.rst @@ -0,0 +1 @@ +Fix bug where :func:`mne.decoding.get_coef` did not work properly with :class:`mne.decoding.CSP`, by `Eric Larson`_. diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 8e36ee412a8..cf3d9f29333 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -15,7 +15,7 @@ from ..fixes import BaseEstimator, _check_fit_params, _get_check_scoring from ..parallel import parallel_func -from ..utils import verbose, warn +from ..utils import _pl, logger, verbose, warn class LinearModel(BaseEstimator): @@ -207,31 +207,47 @@ def _check_estimator(estimator, get_params=True): def _get_inverse_funcs(estimator, terminal=True): """Retrieve the inverse functions of an pipeline or an estimator.""" - inverse_func = [False] + inverse_func = list() + estimators = list() if hasattr(estimator, "steps"): # if pipeline, retrieve all steps by nesting - inverse_func = list() for _, est in estimator.steps: inverse_func.extend(_get_inverse_funcs(est, terminal=False)) + estimators.append(est.__class__.__name__) elif hasattr(estimator, "inverse_transform"): # if not pipeline attempt to retrieve inverse function - inverse_func = [estimator.inverse_transform] + inverse_func.append(estimator.inverse_transform) + estimators.append(estimator.__class__.__name__) + else: + inverse_func.append(False) + estimators.append("Unknown") # If terminal node, check that that the last estimator is a classifier, # and remove it from the transformers. if terminal: last_is_estimator = inverse_func[-1] is False - all_invertible = False not in inverse_func[:-1] - if last_is_estimator and all_invertible: + logger.debug(f" Last estimator is an estimator: {last_is_estimator}") + non_invertible = np.where( + [inv_func is False for inv_func in inverse_func[:-1]] + )[0] + if last_is_estimator and len(non_invertible) == 0: # keep all inverse transformation and remove last estimation + logger.debug(" Removing inverse transformation from inverse list.") inverse_func = inverse_func[:-1] else: + if len(non_invertible): + bad = ", ".join(estimators[ni] for ni in non_invertible) + warn( + f"Cannot inverse transform non-invertible " + f"estimator{_pl(non_invertible)}: {bad}." + ) inverse_func = list() return inverse_func -def get_coef(estimator, attr="filters_", inverse_transform=False): +@verbose +def get_coef(estimator, attr="filters_", inverse_transform=False, *, verbose=None): """Retrieve the coefficients of an estimator ending with a Linear Model. This is typically useful to retrieve "spatial filters" or "spatial @@ -247,6 +263,7 @@ def get_coef(estimator, attr="filters_", inverse_transform=False): inverse_transform : bool If True, returns the coefficients after inverse transforming them with the transformer steps of the estimator. + %(verbose)s Returns ------- @@ -259,6 +276,7 @@ def get_coef(estimator, attr="filters_", inverse_transform=False): """ # Get the coefficients of the last estimator in case of nested pipeline est = estimator + logger.debug(f"Getting coefficients from estimator: {est.__class__.__name__}") while hasattr(est, "steps"): est = est.steps[-1][1] @@ -267,7 +285,9 @@ def get_coef(estimator, attr="filters_", inverse_transform=False): # If SlidingEstimator, loop across estimators if hasattr(est, "estimators_"): coef = list() - for this_est in est.estimators_: + for ei, this_est in enumerate(est.estimators_): + if ei == 0: + logger.debug(" Extracting coefficients from SlidingEstimator.") coef.append(get_coef(this_est, attr, inverse_transform)) coef = np.transpose(coef) coef = coef[np.newaxis] # fake a sample dimension @@ -290,9 +310,11 @@ def get_coef(estimator, attr="filters_", inverse_transform=False): # The inverse_transform parameter will call this method on any # estimator contained in the pipeline, in reverse order. for inverse_func in _get_inverse_funcs(estimator)[::-1]: + logger.debug(f" Applying inverse transformation: {inverse_func}.") coef = inverse_func(coef) if squeeze_first_dim: + logger.debug(" Squeezing first dimension of coefficients.") coef = coef[0] return coef diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index ba76acd2d7c..bdf66758290 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -230,9 +230,9 @@ def transform(self, X): ------- X : ndarray If self.transform_into == 'average_power' then returns the power of - CSP features averaged over time and shape (n_epochs, n_sources) + CSP features averaged over time and shape (n_epochs, n_components) If self.transform_into == 'csp_space' then returns the data in CSP - space and shape is (n_epochs, n_sources, n_times). + space and shape is (n_epochs, n_components, n_times). """ if not isinstance(X, np.ndarray): raise ValueError("X should be of type ndarray (got %s)." % type(X)) @@ -255,6 +255,30 @@ def transform(self, X): X /= self.std_ return X + def inverse_transform(self, X): + """Project CSP features back to sensor space. + + Parameters + ---------- + X : array, shape (n_epochs, n_components) + The data in CSP power space. + + Returns + ------- + X : ndarray + The data in sensor space and shape (n_epochs, n_channels, n_components). + """ + if self.transform_into != "average_power": + raise NotImplementedError( + "Can only inverse transform CSP features when transform_into is " + "'average_power'." + ) + if not (X.ndim == 2 and X.shape[1] == self.n_components): + raise ValueError( + f"X must be 2D with X[1]={self.n_components}, got {X.shape=}" + ) + return X[:, np.newaxis, :] * self.patterns_[: self.n_components].T + @copy_doc(TransformerMixin.fit_transform) def fit_transform(self, X, y, **fit_params): # noqa: D102 return super().fit_transform(X, y=y, **fit_params) @@ -924,8 +948,8 @@ def transform(self, X): ------- X : ndarray If self.transform_into == 'average_power' then returns the power of - CSP features averaged over time and shape (n_epochs, n_sources) + CSP features averaged over time and shape (n_epochs, n_components) If self.transform_into == 'csp_space' then returns the data in CSP - space and shape is (n_epochs, n_sources, n_times). + space and shape is (n_epochs, n_components, n_times). """ return super().transform(X) diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index 206628d3799..c26ca4f67b7 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -4,6 +4,8 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +from contextlib import nullcontext + import numpy as np import pytest from numpy.testing import ( @@ -132,16 +134,23 @@ def inverse_transform(self, X): assert expected_n == len(_get_inverse_funcs(est)) bad_estimators = [ - Clf(), # no preprocessing - Inv(), # final estimator isn't classifier - make_pipeline(NoInv(), Clf()), # first step isn't invertible + Clf(), # 0: no preprocessing + Inv(), # 1: final estimator isn't classifier + make_pipeline(NoInv(), Clf()), # 2: first step isn't invertible make_pipeline( Inv(), make_pipeline(Inv(), NoInv()), Clf() - ), # nested step isn't invertible + ), # 3: nested step isn't invertible ] - for est in bad_estimators: + # It's the NoInv that triggers the warning, but too hard to context manage just + # the correct part of the bad_estimators loop + for ei, est in enumerate(bad_estimators): est.fit(X, y) - invs = _get_inverse_funcs(est) + if ei in (2, 3): # the NoInv indices + ctx = pytest.warns(RuntimeWarning, match="Cannot inverse transform") + else: + ctx = nullcontext() + with ctx: + invs = _get_inverse_funcs(est) assert_equal(invs, list()) # II. Test get coef for classification/regression estimators and pipelines diff --git a/mne/decoding/tests/test_csp.py b/mne/decoding/tests/test_csp.py index 1e8d138f83b..e05aca7226a 100644 --- a/mne/decoding/tests/test_csp.py +++ b/mne/decoding/tests/test_csp.py @@ -10,10 +10,15 @@ import numpy as np import pytest -from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal +from numpy.testing import ( + assert_allclose, + assert_array_almost_equal, + assert_array_equal, + assert_equal, +) from mne import Epochs, io, pick_types, read_events -from mne.decoding import CSP, Scaler, SPoC +from mne.decoding import CSP, LinearModel, Scaler, SPoC, get_coef from mne.decoding.csp import _ajd_pham from mne.utils import catch_logging @@ -299,6 +304,7 @@ def test_regularized_csp(ch_type, rank, reg): n_components = 3 sc = Scaler(epochs.info) + epochs_data_orig = epochs_data.copy() epochs_data = sc.fit_transform(epochs_data) csp = CSP(n_components=n_components, reg=reg, norm_trace=False, rank=rank) with catch_logging(verbose=True) as log: @@ -333,10 +339,27 @@ def test_regularized_csp(ch_type, rank, reg): assert sources.shape[1] == n_components cv = StratifiedKFold(5) - clf = make_pipeline(csp, LogisticRegression(solver="liblinear")) - score = cross_val_score(clf, epochs_data, y, cv=cv, scoring="roc_auc").mean() + clf = make_pipeline( + sc, + csp, + LinearModel(LogisticRegression(solver="liblinear")), + ) + score = cross_val_score(clf, epochs_data_orig, y, cv=cv, scoring="roc_auc").mean() assert 0.75 <= score <= 1.0 + # Test get_coef on CSP + clf.fit(epochs_data_orig, y) + coef = csp.patterns_[:n_components] + assert coef.shape == (n_components, n_channels), coef.shape + coef = sc.inverse_transform(coef.T[np.newaxis])[0] + assert coef.shape == (len(epochs.ch_names), n_components), coef.shape + coef_mne = get_coef(clf, "patterns_", inverse_transform=True, verbose="debug") + assert coef.shape == coef_mne.shape + coef_mne /= np.linalg.norm(coef_mne, axis=0) + coef /= np.linalg.norm(coef, axis=0) + coef *= np.sign(np.sum(coef_mne * coef, axis=0)) + assert_allclose(coef_mne, coef) + def test_csp_pipeline(): """Test if CSP works in a pipeline.""" diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 3ba47b99700..5e105bd399d 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -217,7 +217,7 @@ def inverse_transform(self, epochs_data): Parameters ---------- - epochs_data : array, shape (n_epochs, n_channels, n_times) + epochs_data : array, shape ([n_epochs, ]n_channels, n_times) The data. Returns @@ -230,8 +230,16 @@ def inverse_transform(self, epochs_data): This function makes a copy of the data before the operations and the memory usage may be large with big data. """ + squeeze = False + # Can happen with CSP + if epochs_data.ndim == 2: + squeeze = True + epochs_data = epochs_data[..., np.newaxis] assert epochs_data.ndim == 3, epochs_data.shape - return _sklearn_reshape_apply(self._scaler.inverse_transform, True, epochs_data) + out = _sklearn_reshape_apply(self._scaler.inverse_transform, True, epochs_data) + if squeeze: + out = out[..., 0] + return out class Vectorizer(TransformerMixin): From 6fd4674673dafb944fbade0297a8c609d090a59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Mon, 22 Apr 2024 23:16:40 +0200 Subject: [PATCH 007/153] Remove unused functions from `report.py` (#12563) --- mne/report/report.py | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/mne/report/report.py b/mne/report/report.py index b2fafe5b446..776f1bfee26 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -457,36 +457,6 @@ def _fig_to_img(fig, *, image_format="png", own_figure=True): ) -def _scale_mpl_figure(fig, scale): - """Magic scaling helper. - - Keeps font size and artist sizes constant - 0.5 : current font - 4pt - 2.0 : current font + 4pt - - This is a heuristic but it seems to work for most cases. - """ - scale = float(scale) - fig.set_size_inches(fig.get_size_inches() * scale) - fig.set_dpi(fig.get_dpi() * scale) - import matplotlib as mpl - - if scale >= 1: - sfactor = scale**2 - else: - sfactor = -((1.0 / scale) ** 2) - for text in fig.findobj(mpl.text.Text): - fs = text.get_fontsize() - new_size = fs + sfactor - if new_size <= 0: - raise ValueError( - "could not rescale matplotlib fonts, consider " 'increasing "scale"' - ) - text.set_fontsize(new_size) - - fig.canvas.draw() - - def _get_bem_contour_figs_as_arrays( *, sl, n_jobs, mri_fname, surfaces, orientation, src, show, show_orientation, width ): @@ -699,12 +669,6 @@ def _webp_supported(): return good -def _check_scale(scale): - """Ensure valid scale value is passed.""" - if np.isscalar(scale) and scale <= 0: - raise ValueError("scale must be positive, not %s" % scale) - - def _check_image_format(rep, image_format): """Ensure fmt is valid.""" if rep is None or image_format is not None: From 52d9a9b5a03503d00439c7ba999799d8dd4ac030 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 22:51:58 +0000 Subject: [PATCH 008/153] [pre-commit.ci] pre-commit autoupdate (#12564) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 744e28edcf7..730a3bafa83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.7 + rev: v0.4.1 hooks: - id: ruff name: ruff lint mne From 96ac7afc522cd4775113b2327b0be7dc3a483e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Tue, 23 Apr 2024 16:06:15 +0200 Subject: [PATCH 009/153] In `Report`, replace some uses of jQuery with vanilla JavaScript (#12557) --- mne/report/js_and_css/report.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/mne/report/js_and_css/report.js b/mne/report/js_and_css/report.js index 10c877c05ce..0263a938cf5 100644 --- a/mne/report/js_and_css/report.js +++ b/mne/report/js_and_css/report.js @@ -8,8 +8,8 @@ const refreshScrollSpy = () =>{ } const propagateScrollSpyURL = () => { - $(window).on('activate.bs.scrollspy', (event) => { - history.replaceState({}, "", event.relatedTarget); + window.addEventListener('activate.bs.scrollspy', (e) => { + history.replaceState({}, "", e.relatedTarget); }); } @@ -40,10 +40,10 @@ const toggleTagVisibility = (tagName) => { if (visibleTagNamesOfCurrentElement.size === 0) { // hide $(currentElement).slideToggle('fast', () => { - $(currentElement).addClass('d-none'); + currentElement.classList.add('d-none'); }); } else if ($(currentElement).hasClass('d-none')) { // show - $(currentElement).removeClass('d-none'); + currentElement.classList.remove('d-none'); $(currentElement).slideToggle('fast'); } }) @@ -52,12 +52,12 @@ const toggleTagVisibility = (tagName) => { tagBadgeElements.forEach((badgeElement) => { if (tag.visible) { badgeElement.removeAttribute('data-mne-tag-hidden'); - $(badgeElement).removeClass('bg-secondary'); - $(badgeElement).addClass('bg-primary'); + badgeElement.classList.remove('bg-secondary'); + badgeElement.classList.add('bg-primary'); } else { badgeElement.setAttribute('data-mne-tag-hidden', true); - $(badgeElement).removeClass('bg-primary'); - $(badgeElement).addClass('bg-secondary'); + badgeElement.classList.remove('bg-primary'); + badgeElement.classList.add('bg-secondary'); } }) @@ -164,8 +164,8 @@ const _handleTocLinkClick = (e) => { const tocLinkElement = e.target; const targetDomId = tocLinkElement.getAttribute('href'); const targetElement = document.querySelector(targetDomId); - const top = $(targetElement).offset().top; - + const top = targetElement.getBoundingClientRect().top + window.scrollY; + // Update URL to reflect the current scroll position. // We use history.pushState to change the URL without causing the browser to scroll. history.pushState(null, "", targetDomId); @@ -248,7 +248,8 @@ const disableGlobalKeysInSearchBox = () => { }) } -$(document).ready(() => { +/* Run once all content is fully loaded. */ +window.addEventListener('load', () => { gatherTags(); updateTagCountBadges(); addFilterByTagsCheckboxEventHandlers(); @@ -261,6 +262,7 @@ $(document).ready(() => { propagateScrollSpyURL(); }); +/* Resizing the window throws off the scroll spy and top-margin handling. */ window.onresize = () => { fixTopMargin(); refreshScrollSpy(); From 55cba826db9f5b7a9a28c6a12248108fddfadfa2 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 23 Apr 2024 10:32:11 -0400 Subject: [PATCH 010/153] DOC: Clearer error message and warning (#12567) --- doc/conf.py | 2 ++ mne/commands/mne_freeview_bem_surfaces.py | 14 ++++------ mne/utils/config.py | 32 ++++++++++++++++++----- mne/utils/tests/test_config.py | 6 +++++ 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index ae7ab9677fd..09e1a9685e5 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1362,6 +1362,8 @@ def reset_warnings(gallery_conf, fname): r"DataFrameGroupBy\.apply operated on the grouping columns.*", # pandas r"\nPyarrow will become a required dependency of pandas.*", + # latexcodec + r"open_text is deprecated\. Use files.*", ): warnings.filterwarnings( # deal with other modules having bad imports "ignore", message=".*%s.*" % key, category=DeprecationWarning diff --git a/mne/commands/mne_freeview_bem_surfaces.py b/mne/commands/mne_freeview_bem_surfaces.py index 504ca3378bf..502e4fe2d67 100644 --- a/mne/commands/mne_freeview_bem_surfaces.py +++ b/mne/commands/mne_freeview_bem_surfaces.py @@ -20,7 +20,7 @@ from mne.utils import get_subjects_dir, run_subprocess -def freeview_bem_surfaces(subject, subjects_dir, method): +def freeview_bem_surfaces(subject, subjects_dir, method=None): """View 3-Layers BEM model with Freeview. Parameters @@ -29,8 +29,9 @@ def freeview_bem_surfaces(subject, subjects_dir, method): Subject name subjects_dir : path-like Directory containing subjects data (Freesurfer SUBJECTS_DIR) - method : str - Can be ``'flash'`` or ``'watershed'``. + method : str | None + Can be ``'flash'`` or ``'watershed'``, or None to use the ``bem/`` directory + files. """ subjects_dir = str(get_subjects_dir(subjects_dir, raise_error=True)) @@ -85,10 +86,6 @@ def run(): parser = get_optparser(__file__) subject = os.environ.get("SUBJECT") - subjects_dir = get_subjects_dir() - if subjects_dir is not None: - subjects_dir = str(subjects_dir) - parser.add_option( "-s", "--subject", dest="subject", help="Subject name", default=subject ) @@ -97,13 +94,12 @@ def run(): "--subjects-dir", dest="subjects_dir", help="Subjects directory", - default=subjects_dir, ) parser.add_option( "-m", "--method", dest="method", - help=("Method used to generate the BEM model. " "Can be flash or watershed."), + help="Method used to generate the BEM model. Can be flash or watershed.", ) options, args = parser.parse_args() diff --git a/mne/utils/config.py b/mne/utils/config.py index 9fab1015040..e432e8c00f6 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -468,16 +468,34 @@ def get_subjects_dir(subjects_dir=None, raise_error=False): value : Path | None The SUBJECTS_DIR value. """ + from_config = False if subjects_dir is None: subjects_dir = get_config("SUBJECTS_DIR", raise_error=raise_error) + from_config = True if subjects_dir is not None: - subjects_dir = _check_fname( - fname=subjects_dir, - overwrite="read", - must_exist=True, - need_dir=True, - name="subjects_dir", - ) + # Emit a nice error or warning if their config is bad + try: + subjects_dir = _check_fname( + fname=subjects_dir, + overwrite="read", + must_exist=True, + need_dir=True, + name="subjects_dir", + ) + except FileNotFoundError: + if from_config: + msg = ( + "SUBJECTS_DIR in your MNE-Python configuration or environment " + "does not exist, consider using mne.set_config to fix it: " + f"{subjects_dir}" + ) + if raise_error: + raise FileNotFoundError(msg) from None + else: + warn(msg) + elif raise_error: + raise + return subjects_dir diff --git a/mne/utils/tests/test_config.py b/mne/utils/tests/test_config.py index e0155638b0d..0706e84996c 100644 --- a/mne/utils/tests/test_config.py +++ b/mne/utils/tests/test_config.py @@ -161,6 +161,12 @@ def test_get_subjects_dir(tmp_path, monkeypatch): monkeypatch.setenv("USERPROFILE", str(tmp_path)) # Windows assert str(get_subjects_dir("~/foo")) == str(subjects_dir) + monkeypatch.setenv("SUBJECTS_DIR", str(tmp_path / "doesntexist")) + with pytest.warns(RuntimeWarning, match="MNE-Python config"): + get_subjects_dir() + with pytest.raises(FileNotFoundError, match="MNE-Python config"): + get_subjects_dir(raise_error=True) + @pytest.mark.slowtest @requires_good_network From 9262ae4858b123b7dcd70d5fad99b3b4aa00d271 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 23 Apr 2024 17:48:29 +0200 Subject: [PATCH 011/153] Try fixed-width social icons on website (#12565) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/conf.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 09e1a9685e5..b175fa4cb03 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -828,30 +828,25 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): html_theme_options = { "icon_links": [ dict( - name="GitHub", - url="https://github.com/mne-tools/mne-python", - icon="fa-brands fa-square-github", + name="Discord", + url="https://discord.gg/rKfvxTuATa", + icon="fa-brands fa-discord fa-fw", ), dict( name="Mastodon", url="https://fosstodon.org/@mne", - icon="fa-brands fa-mastodon", + icon="fa-brands fa-mastodon fa-fw", attributes=dict(rel="me"), ), - dict( - name="Twitter", - url="https://twitter.com/mne_python", - icon="fa-brands fa-square-twitter", - ), dict( name="Forum", url="https://mne.discourse.group/", - icon="fa-brands fa-discourse", + icon="fa-brands fa-discourse fa-fw", ), dict( - name="Discord", - url="https://discord.gg/rKfvxTuATa", - icon="fa-brands fa-discord", + name="GitHub", + url="https://github.com/mne-tools/mne-python", + icon="fa-brands fa-square-github fa-fw", ), ], "icon_links_label": "External Links", # for screen reader @@ -859,7 +854,12 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): "navigation_with_keys": False, "show_toc_level": 1, "article_header_start": [], # disable breadcrumbs - "navbar_end": ["theme-switcher", "version-switcher", "navbar-icon-links"], + "navbar_end": [ + "theme-switcher", + "version-switcher", + "navbar-icon-links", + ], + "navbar_persistent": ["search-button"], "footer_start": ["copyright"], "secondary_sidebar_items": ["page-toc", "edit-this-page"], "analytics": dict(google_analytics_id="G-5TBCPCRB6X"), From 0d781c8329a524c7bd66b27d69348eabb468681d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 23 Apr 2024 16:48:13 -0400 Subject: [PATCH 012/153] MAINT: Post-release deprecations and version bumps (#12554) --- README.rst | 10 +-- doc/changes/devel/12554.dependency.rst | 6 ++ doc/conf.py | 2 - mne/_fiff/reference.py | 2 +- mne/conftest.py | 26 ------ mne/decoding/csp.py | 2 +- mne/decoding/receptive_field.py | 3 +- mne/dipole.py | 3 +- mne/epochs.py | 11 +-- mne/evoked.py | 2 +- mne/fixes.py | 59 +----------- mne/io/brainvision/tests/test_brainvision.py | 10 +-- mne/io/kit/coreg.py | 20 +---- mne/io/kit/tests/test_coreg.py | 14 --- mne/io/tests/test_raw.py | 3 +- mne/preprocessing/ica.py | 9 +- mne/preprocessing/nirs/_beer_lambert_law.py | 5 +- mne/preprocessing/tests/test_infomax.py | 5 +- mne/preprocessing/xdawn.py | 4 +- mne/report/report.py | 28 ++---- mne/report/tests/test_report.py | 6 -- mne/tests/test_epochs.py | 10 +-- mne/time_frequency/multitaper.py | 12 --- mne/time_frequency/spectrum.py | 25 ++---- mne/time_frequency/tests/test_spectrum.py | 3 +- mne/time_frequency/tests/test_tfr.py | 13 --- mne/time_frequency/tfr.py | 48 +--------- mne/utils/__init__.pyi | 4 + mne/utils/check.py | 8 +- mne/utils/docs.py | 7 +- mne/utils/linalg.py | 55 ++++++++++++ mne/utils/spectrum.py | 2 +- mne/viz/_brain/tests/test_brain.py | 3 +- mne/viz/_mpl_figure.py | 34 ++----- mne/viz/epochs.py | 5 +- mne/viz/evoked.py | 9 +- mne/viz/ica.py | 16 +++- mne/viz/raw.py | 2 +- mne/viz/tests/test_topomap.py | 17 +--- mne/viz/utils.py | 90 +++++-------------- pyproject.toml | 6 +- .../dev}/gen_css_for_mne.py | 3 +- tools/github_actions_env_vars.sh | 2 +- 43 files changed, 187 insertions(+), 417 deletions(-) create mode 100644 doc/changes/devel/12554.dependency.rst rename {mne/report/js_and_css/bootstrap-icons => tools/dev}/gen_css_for_mne.py (94%) diff --git a/README.rst b/README.rst index 153dcf0a5ef..11116169727 100644 --- a/README.rst +++ b/README.rst @@ -74,9 +74,9 @@ Dependencies The minimum required dependencies to run MNE-Python are: - `Python `__ ≥ 3.9 -- `NumPy `__ ≥ 1.21.2 -- `SciPy `__ ≥ 1.7.1 -- `Matplotlib `__ ≥ 3.5.0 +- `NumPy `__ ≥ 1.23 +- `SciPy `__ ≥ 1.9 +- `Matplotlib `__ ≥ 3.6 - `Pooch `__ ≥ 1.5 - `tqdm `__ - `Jinja2 `__ @@ -85,9 +85,9 @@ The minimum required dependencies to run MNE-Python are: For full functionality, some functions require: -- `scikit-learn `__ ≥ 1.0 +- `scikit-learn `__ ≥ 1.1 - `Joblib `__ ≥ 0.15 (for parallelization) -- `mne-qt-browser `__ ≥ 0.1 (for fast raw data visualization) +- `mne-qt-browser `__ ≥ 0.5 (for fast raw data visualization) - `Qt `__ ≥ 5.15 via one of the following bindings (for fast raw data visualization and interactive 3D visualization): - `PyQt6 `__ ≥ 6.0 diff --git a/doc/changes/devel/12554.dependency.rst b/doc/changes/devel/12554.dependency.rst new file mode 100644 index 00000000000..5c77efd325f --- /dev/null +++ b/doc/changes/devel/12554.dependency.rst @@ -0,0 +1,6 @@ +Minimum versions for dependencies were bumped to those ~2 years old at the time of release (by `Eric Larson`_), including: + +- NumPy ≥ 1.23 +- SciPy ≥ 1.9 +- Matplotlib ≥ 3.6 +- scikit-learn ≥ 1.1 \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index b175fa4cb03..9d6c08a4c83 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1382,8 +1382,6 @@ def reset_warnings(gallery_conf, fname): message="The figure layout has changed to tight", category=UserWarning, ) - # matplotlib 3.6 in nilearn and pyvista - warnings.filterwarnings("ignore", message=".*cmap function will be deprecated.*") # xarray/netcdf4 warnings.filterwarnings( "ignore", diff --git a/mne/_fiff/reference.py b/mne/_fiff/reference.py index 5822e87e17b..996a034e8a2 100644 --- a/mne/_fiff/reference.py +++ b/mne/_fiff/reference.py @@ -8,7 +8,6 @@ import numpy as np from ..defaults import DEFAULTS -from ..fixes import pinv from ..utils import ( _check_option, _check_preload, @@ -16,6 +15,7 @@ _validate_type, fill_doc, logger, + pinv, verbose, warn, ) diff --git a/mne/conftest.py b/mne/conftest.py index 62f1d3f4cf9..8e50be0bcde 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -137,17 +137,6 @@ def pytest_configure(config): ignore:joblib not installed.*:RuntimeWarning # qdarkstyle ignore:.*Setting theme=.*:RuntimeWarning - # scikit-learn using this arg - ignore:.*The 'sym_pos' keyword is deprecated.*:DeprecationWarning - # Should be removable by 2022/07/08, SciPy savemat issue - ignore:.*elementwise comparison failed; returning scalar in.*:FutureWarning - # numba with NumPy dev - ignore:`np.MachAr` is deprecated.*:DeprecationWarning - # matplotlib 3.6 and pyvista/nilearn - ignore:.*cmap function will be deprecated.*: - # joblib hasn't updated to avoid distutils - ignore:.*distutils package is deprecated.*:DeprecationWarning - ignore:.*distutils Version classes are deprecated.*:DeprecationWarning # nbclient ignore:Passing a schema to Validator\.iter_errors is deprecated.*: ignore:Unclosed context SciPy - ignore:numpy\.core\._multiarray_umath.*:DeprecationWarning - ignore:numpy\.core\.numeric is deprecated.*:DeprecationWarning - ignore:numpy\.core\.multiarray is deprecated.*:DeprecationWarning - ignore:The numpy\.fft\.helper has been made private.*:DeprecationWarning # tqdm (Fedora) ignore:.*'tqdm_asyncio' object has no attribute 'last_print_t':pytest.PytestUnraisableExceptionWarning - # Until mne-qt-browser > 0.5.2 is released - ignore:mne\.io\.pick.channel_indices_by_type is deprecated.*: # Windows CIs using MESA get this ignore:Mesa version 10\.2\.4 is too old for translucent.*:RuntimeWarning - # Matplotlib <-> NumPy 2.0 - ignore:`row_stack` alias is deprecated.*:DeprecationWarning # Matplotlib->tz ignore:datetime.datetime.utcfromtimestamp.*:DeprecationWarning # joblib diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index bdf66758290..b45453dc2dc 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -16,13 +16,13 @@ from ..cov import _compute_rank_raw_array, _regularized_covariance, _smart_eigh from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT from ..evoked import EvokedArray -from ..fixes import pinv from ..utils import ( _check_option, _validate_type, _verbose_safe_false, copy_doc, fill_doc, + pinv, ) from .base import BaseEstimator from .mixin import TransformerMixin diff --git a/mne/decoding/receptive_field.py b/mne/decoding/receptive_field.py index fdf7dea9211..3bb3d6da903 100644 --- a/mne/decoding/receptive_field.py +++ b/mne/decoding/receptive_field.py @@ -9,8 +9,7 @@ import numpy as np from scipy.stats import pearsonr -from ..fixes import pinv -from ..utils import _validate_type, fill_doc, verbose +from ..utils import _validate_type, fill_doc, pinv, verbose from .base import BaseEstimator, _check_estimator, get_coef from .time_delaying_ridge import TimeDelayingRidge diff --git a/mne/dipole.py b/mne/dipole.py index 9dcc88c2b01..008f394f0ea 100644 --- a/mne/dipole.py +++ b/mne/dipole.py @@ -22,7 +22,7 @@ from .bem import _bem_find_surface, _bem_surf_name, _fit_sphere from .cov import _ensure_cov, compute_whitener from .evoked import _aspect_rev, _read_evoked, _write_evokeds -from .fixes import _safe_svd, pinvh +from .fixes import _safe_svd from .forward._compute_forward import _compute_forwards_meeg, _prep_field_computation from .forward._make_forward import ( _get_trans, @@ -50,6 +50,7 @@ copy_function_doc_to_method_doc, fill_doc, logger, + pinvh, verbose, warn, ) diff --git a/mne/epochs.py b/mne/epochs.py index 9e48936f8bf..b733c73018c 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1850,13 +1850,6 @@ def _data_sel_copy_scale( data *= ch_factors[:, np.newaxis] if not data_is_self_data: return data - if copy is None: - warn( - "The current default of copy=False will change to copy=True in 1.7. " - "Set the value of copy explicitly to avoid this warning", - FutureWarning, - ) - copy = False if copy: logger.debug(" Copying, copy=True") data = data.copy() @@ -1880,7 +1873,7 @@ def get_data( tmin=None, tmax=None, *, - copy=None, + copy=True, verbose=None, ): """Get all epochs as a 3D array. @@ -2708,7 +2701,7 @@ def plot_psd( method="auto", average=False, dB=True, - estimate="auto", + estimate="power", xscale="linear", area_mode="std", area_alpha=0.33, diff --git a/mne/evoked.py b/mne/evoked.py index 2e36f47f81b..461fdb61ba6 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -1242,7 +1242,7 @@ def plot_psd( method="auto", average=False, dB=True, - estimate="auto", + estimate="power", xscale="linear", area_mode="std", area_alpha=0.33, diff --git a/mne/fixes.py b/mne/fixes.py index f7534377b5a..c5410476246 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -21,7 +21,6 @@ import operator as operator_module import os import warnings -from contextlib import contextmanager from io import StringIO from math import log from pprint import pprint @@ -814,7 +813,6 @@ def bincount(x, weights, minlength): # noqa: D103 # workaround: plt.close() doesn't spawn close_event on Agg backend # https://github.com/matplotlib/matplotlib/issues/18609 -# scheduled to be fixed by MPL 3.6 def _close_event(fig): """Force calling of the MPL figure close event.""" from matplotlib import backend_bases @@ -832,63 +830,8 @@ def _close_event(fig): pass # pragma: no cover -def _is_last_row(ax): - try: - return ax.get_subplotspec().is_last_row() # 3.4+ - except AttributeError: - return ax.is_last_row() - return ax.get_subplotspec().is_last_row() - - -def _sharex(ax1, ax2): - if hasattr(ax1.axes, "sharex"): - ax1.axes.sharex(ax2) - else: - ax1.get_shared_x_axes().join(ax1, ax2) - - ############################################################################### -# SciPy deprecation of pinv + pinvh rcond (never worked properly anyway) in 1.7 - - -def pinvh(a, rtol=None): - """Compute a pseudo-inverse of a Hermitian matrix.""" - s, u = np.linalg.eigh(a) - del a - if rtol is None: - rtol = s.size * np.finfo(s.dtype).eps - maxS = np.max(np.abs(s)) - above_cutoff = abs(s) > maxS * rtol - psigma_diag = 1.0 / s[above_cutoff] - u = u[:, above_cutoff] - return (u * psigma_diag) @ u.conj().T - - -def pinv(a, rtol=None): - """Compute a pseudo-inverse of a matrix.""" - u, s, vh = _safe_svd(a, full_matrices=False) - del a - maxS = np.max(s) - if rtol is None: - rtol = max(vh.shape + u.shape) * np.finfo(u.dtype).eps - rank = np.sum(s > maxS * rtol) - u = u[:, :rank] - u /= s[:rank] - return (u @ vh[:rank]).conj().T - - -############################################################################### -# h5py uses np.product which is deprecated in NumPy 1.25 - - -@contextmanager -def _numpy_h5py_dep(): - # h5io uses np.product - with warnings.catch_warnings(record=True): - warnings.filterwarnings( - "ignore", "`product` is deprecated.*", DeprecationWarning - ) - yield +# SciPy 1.14+ minimum_phase half=True option def minimum_phase(h, method="homomorphic", n_fft=None, *, half=True): diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 309e44e3cf8..9e31bb2d1d6 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -375,7 +375,7 @@ def test_brainvision_data_highpass_filters(): w = [str(ww.message) for ww in w] assert not any("different lowpass filters" in ww for ww in w), w - assert all("different highpass filters" in ww for ww in w), w + assert any("different highpass filters" in ww for ww in w), w assert raw.info["highpass"] == 1.0 / (2 * np.pi * 10) assert raw.info["lowpass"] == 250.0 @@ -397,7 +397,7 @@ def test_brainvision_data_highpass_filters(): w = [str(ww.message) for ww in w] assert not any("will be dropped" in ww for ww in w), w assert not any("different lowpass filters" in ww for ww in w), w - assert all("different highpass filters" in ww for ww in w), w + assert any("different highpass filters" in ww for ww in w), w assert raw.info["highpass"] == 5.0 assert raw.info["lowpass"] == 250.0 @@ -422,7 +422,7 @@ def test_brainvision_data_lowpass_filters(): expected_warnings = zip(lowpass_warning, highpass_warning) - assert all(any([lp, hp]) for lp, hp in expected_warnings) + assert any(any([lp, hp]) for lp, hp in expected_warnings) assert raw.info["highpass"] == 1.0 / (2 * np.pi * 10) assert raw.info["lowpass"] == 250.0 @@ -446,7 +446,7 @@ def test_brainvision_data_lowpass_filters(): expected_warnings = zip(lowpass_warning, highpass_warning) - assert all(any([lp, hp]) for lp, hp in expected_warnings) + assert any(any([lp, hp]) for lp, hp in expected_warnings) assert raw.info["highpass"] == 1.0 / (2 * np.pi * 10) assert raw.info["lowpass"] == 1.0 / (2 * np.pi * 0.004) @@ -467,7 +467,7 @@ def test_brainvision_data_partially_disabled_hw_filters(): expected_warnings = zip(trigger_warning, lowpass_warning, highpass_warning) - assert all(any([trg, lp, hp]) for trg, lp, hp in expected_warnings) + assert any(any([trg, lp, hp]) for trg, lp, hp in expected_warnings) assert raw.info["highpass"] == 0.0 assert raw.info["lowpass"] == 500.0 diff --git a/mne/io/kit/coreg.py b/mne/io/kit/coreg.py index 3e691249790..739a82a3137 100644 --- a/mne/io/kit/coreg.py +++ b/mne/io/kit/coreg.py @@ -5,7 +5,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -import pickle import re from collections import OrderedDict from os import SEEK_CUR, PathLike @@ -50,8 +49,7 @@ def read_mrk(fname): from .kit import _read_dirs fname = Path(_check_fname(fname, "read", must_exist=True, name="mrk file")) - if fname.suffix != ".pickled": - _check_option("file extension", fname.suffix, (".sqd", ".mrk", ".txt")) + _check_option("file extension", fname.suffix, (".sqd", ".mrk", ".txt")) if fname.suffix in (".sqd", ".mrk"): with open(fname, "rb", buffering=0) as fid: dirs = _read_dirs(fid) @@ -67,21 +65,9 @@ def read_mrk(fname): if meg_done: pts.append(meg_pts) mrk_points = np.array(pts) - elif fname.suffix == ".txt": + else: + assert fname.suffix == ".txt" mrk_points = _read_dig_kit(fname, unit="m") - elif fname.suffix == ".pickled": - warn( - "Reading pickled files is unsafe and not future compatible, save " - "to a standard format (text or FIF) instead, e.g. with:\n" - r"np.savetxt(fid, pts, delimiter=\"\\t\", newline=\"\\n\")", - FutureWarning, - ) - with open(fname, "rb") as fid: - food = pickle.load(fid) # nosec B301 - try: - mrk_points = food["mrk"] - except Exception: - raise ValueError(f"{fname} does not contain marker points.") from None # check output mrk_points = np.asarray(mrk_points) diff --git a/mne/io/kit/tests/test_coreg.py b/mne/io/kit/tests/test_coreg.py index 7907832ea6c..a1d508afbdd 100644 --- a/mne/io/kit/tests/test_coreg.py +++ b/mne/io/kit/tests/test_coreg.py @@ -3,7 +3,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -import pickle from pathlib import Path import numpy as np @@ -28,19 +27,6 @@ def test_io_mrk(tmp_path): pts_2 = read_mrk(path) assert_array_equal(pts, pts_2, "read/write mrk to text") - # pickle (deprecated) - fname = tmp_path / "mrk.pickled" - with open(fname, "wb") as fid: - pickle.dump(dict(mrk=pts), fid) - with pytest.warns(FutureWarning, match="unsafe"): - pts_2 = read_mrk(fname) - assert_array_equal(pts_2, pts, "pickle mrk") - with open(fname, "wb") as fid: - pickle.dump(dict(), fid) - with pytest.warns(FutureWarning, match="unsafe"): - with pytest.raises(ValueError, match="does not contain"): - read_mrk(fname) - # unsupported extension fname = tmp_path / "file.ext" with pytest.raises(FileNotFoundError, match="does not exist"): diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index 33384c1e0e4..b478ed59c46 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -30,7 +30,6 @@ from mne._fiff.pick import _ELECTRODE_CH_TYPES, _FNIRS_CH_TYPES_SPLIT from mne._fiff.proj import Projection from mne._fiff.utils import _mult_cal_one -from mne.fixes import _numpy_h5py_dep from mne.io import BaseRaw, RawArray, read_raw_fif from mne.io.base import _get_scaling from mne.transforms import Transform @@ -441,7 +440,7 @@ def _test_raw_reader( if check_version("h5io"): read_hdf5, write_hdf5 = _import_h5io_funcs() fname_h5 = op.join(tempdir, "info.h5") - with _writing_info_hdf5(raw.info), _numpy_h5py_dep(): + with _writing_info_hdf5(raw.info): write_hdf5(fname_h5, raw.info) new_info = Info(read_hdf5(fname_h5)) assert object_diff(new_info, raw.info) == "" diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 85bd312f3b2..78d35119f29 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -19,7 +19,7 @@ from typing import Literal, Optional, Union import numpy as np -from scipy import linalg, stats +from scipy import stats from scipy.spatial import distance from scipy.special import expit @@ -82,6 +82,7 @@ fill_doc, int_like, logger, + pinv, repr_html, verbose, warn, @@ -1006,7 +1007,7 @@ def _fit(self, data, fit_type): self.current_fit = fit_type def _update_mixing_matrix(self): - self.mixing_matrix_ = linalg.pinv(self.unmixing_matrix_) + self.mixing_matrix_ = pinv(self.unmixing_matrix_) def _update_ica_names(self): """Update ICA names when n_components_ is set.""" @@ -2519,6 +2520,7 @@ def plot_properties( reject="auto", reject_by_annotation=True, *, + estimate="power", verbose=None, ): return plot_ica_properties( @@ -2536,6 +2538,7 @@ def plot_properties( show=show, reject=reject, reject_by_annotation=reject_by_annotation, + estimate=estimate, verbose=verbose, ) @@ -3499,7 +3502,7 @@ def read_ica_eeglab(fname, *, montage_units="auto", verbose=None): # So in either case, we can use SVD to get our square whitened # weights matrix (u * s) and our PCA vectors (v) back: use = eeg.icaweights @ eeg.icasphere - use_check = linalg.pinv(eeg.icawinv) + use_check = pinv(eeg.icawinv) if not np.allclose(use, use_check, rtol=1e-6): warn( "Mismatch between icawinv and icaweights @ icasphere from EEGLAB " diff --git a/mne/preprocessing/nirs/_beer_lambert_law.py b/mne/preprocessing/nirs/_beer_lambert_law.py index 9a39a342e50..cb15409a59d 100644 --- a/mne/preprocessing/nirs/_beer_lambert_law.py +++ b/mne/preprocessing/nirs/_beer_lambert_law.py @@ -8,13 +8,12 @@ import os.path as op import numpy as np -from scipy import linalg from scipy.interpolate import interp1d from scipy.io import loadmat from ..._fiff.constants import FIFF from ...io import BaseRaw -from ...utils import _validate_type, warn +from ...utils import _validate_type, pinv, warn from ..nirs import _validate_nirs_info, source_detector_distances @@ -71,7 +70,7 @@ def beer_lambert_law(raw, ppf=6.0): rename = dict() for ii, jj in zip(picks[::2], picks[1::2]): EL = abs_coef * distances[ii] * ppf - iEL = linalg.pinv(EL) + iEL = pinv(EL) raw._data[[ii, jj]] = iEL @ raw._data[[ii, jj]] * 1e-3 diff --git a/mne/preprocessing/tests/test_infomax.py b/mne/preprocessing/tests/test_infomax.py index 94cb4713ddc..4c1c81cd552 100644 --- a/mne/preprocessing/tests/test_infomax.py +++ b/mne/preprocessing/tests/test_infomax.py @@ -8,9 +8,10 @@ import numpy as np import pytest from numpy.testing import assert_almost_equal -from scipy import linalg, stats +from scipy import stats from mne.preprocessing.infomax_ import infomax +from mne.utils import pinv pytest.importorskip("sklearn") @@ -159,7 +160,7 @@ def test_non_square_infomax(): unmixing_ = infomax(m, random_state=rng, extended=True) s_ = np.dot(unmixing_, m.T) # Check that the mixing model described in the docstring holds: - mixing_ = linalg.pinv(unmixing_.T) + mixing_ = pinv(unmixing_.T) assert_almost_equal(m, s_.T.dot(mixing_)) diff --git a/mne/preprocessing/xdawn.py b/mne/preprocessing/xdawn.py index c0a0bb88cb3..a332da6f3a8 100644 --- a/mne/preprocessing/xdawn.py +++ b/mne/preprocessing/xdawn.py @@ -14,7 +14,7 @@ from ..epochs import BaseEpochs from ..evoked import Evoked, EvokedArray from ..io import BaseRaw -from ..utils import _check_option, logger +from ..utils import _check_option, logger, pinv def _construct_signal_from_epochs(epochs, events, sfreq, tmin): @@ -92,7 +92,7 @@ def _least_square_evoked(epochs_data, events, tmin, sfreq): X = np.concatenate(toeplitz) # least square estimation - predictor = np.dot(linalg.pinv(np.dot(X, X.T)), X) + predictor = np.dot(pinv(np.dot(X, X.T)), X) evokeds = np.dot(predictor, raw.T) evokeds = np.transpose(np.vsplit(evokeds, len(classes)), (0, 2, 1)) return evokeds, toeplitz diff --git a/mne/report/report.py b/mne/report/report.py index 776f1bfee26..e5ee790c91f 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -60,7 +60,6 @@ _safe_input, _validate_type, _verbose_safe_false, - check_version, fill_doc, get_subjects_dir, logger, @@ -422,12 +421,10 @@ def _fig_to_img(fig, *, image_format="png", own_figure=True): # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html mpl_kwargs = dict() pil_kwargs = dict() - has_pillow = check_version("PIL") - if has_pillow: - if image_format == "webp": - pil_kwargs.update(lossless=True, method=6) - elif image_format == "png": - pil_kwargs.update(optimize=True, compress_level=9) + if image_format == "webp": + pil_kwargs.update(lossless=True, method=6) + elif image_format == "png": + pil_kwargs.update(optimize=True, compress_level=9) if pil_kwargs: # matplotlib modifies the passed dict, which is a bug mpl_kwargs["pil_kwargs"] = pil_kwargs.copy() @@ -438,7 +435,7 @@ def _fig_to_img(fig, *, image_format="png", own_figure=True): plt.close(fig) # Remove alpha - if image_format != "svg" and has_pillow: + if image_format != "svg": from PIL import Image output.seek(0) @@ -660,28 +657,16 @@ def open_report(fname, **params): _ALLOWED_IMAGE_FORMATS = ("png", "svg", "webp") -def _webp_supported(): - good = check_version("matplotlib", "3.6") and check_version("PIL") - if good: - from PIL import features - - good = features.check("webp") - return good - - def _check_image_format(rep, image_format): """Ensure fmt is valid.""" if rep is None or image_format is not None: allowed = list(_ALLOWED_IMAGE_FORMATS) + ["auto"] extra = "" - if not _webp_supported(): - allowed.pop(allowed.index("webp")) - extra = '("webp" supported on matplotlib 3.6+ with PIL installed)' _check_option("image_format", image_format, allowed_values=allowed, extra=extra) else: image_format = rep.image_format if image_format == "auto": - image_format = "webp" if _webp_supported() else "png" + image_format = "webp" return image_format @@ -707,7 +692,6 @@ class Report: ``'webp'`` if available and ``'png'`` otherwise). ``'svg'`` uses vector graphics, so fidelity is higher but can increase file size and browser image rendering time as well. - ``'webp'`` format requires matplotlib >= 3.6. .. versionadded:: 0.15 .. versionchanged:: 1.3 diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 3860e227318..8afb4fc9e80 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -35,7 +35,6 @@ from mne.report.report import ( _ALLOWED_IMAGE_FORMATS, CONTENT_ORDER, - _webp_supported, ) from mne.utils import Bunch, _record_warnings from mne.utils._testing import assert_object_equal @@ -1195,11 +1194,6 @@ def test_tags(tags, str_or_array, wrong_dtype, invalid_chars): @pytest.mark.parametrize("image_format", _ALLOWED_IMAGE_FORMATS) def test_image_format(image_format): """Test image format support.""" - if image_format == "webp": - if not _webp_supported(): - with pytest.raises(ValueError, match="matplotlib"): - Report(image_format="webp") - return r = Report(image_format=image_format) fig1, _ = _get_example_figures() r.add_figure(fig1, "fig1") diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 0bede8b53d4..a4e9319f3e2 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -87,13 +87,6 @@ rng = np.random.RandomState(42) -pytestmark = [ - pytest.mark.filterwarnings( - "ignore:The current default of copy=False will change to copy=.*:FutureWarning", - ), -] - - def _create_epochs_with_annotations(): """Create test dataset of Epochs with Annotations.""" # set up a test dataset @@ -334,8 +327,7 @@ def test_get_data_copy(): data = epochs.get_data(copy=True) assert not np.shares_memory(data, epochs._data) - with pytest.warns(FutureWarning, match="The current default of copy=False will"): - data = epochs.get_data(verbose="debug") + data = epochs.get_data(copy=False, verbose="debug") assert np.shares_memory(data, epochs._data) assert data is epochs._data data_orig = data.copy() diff --git a/mne/time_frequency/multitaper.py b/mne/time_frequency/multitaper.py index 4a9e66c4673..28ede346d20 100644 --- a/mne/time_frequency/multitaper.py +++ b/mne/time_frequency/multitaper.py @@ -474,7 +474,6 @@ def tfr_array_multitaper( n_jobs=None, *, verbose=None, - epoch_data=None, ): """Compute Time-Frequency Representation (TFR) using DPSS tapers. @@ -508,10 +507,6 @@ def tfr_array_multitaper( %(n_jobs)s The parallelization is implemented across channels. %(verbose)s - epoch_data : None - Deprecated parameter for providing epoched data as of 1.7, will be replaced with - the ``data`` parameter in 1.8. New code should use the ``data`` parameter. If - ``epoch_data`` is not ``None``, a warning will be raised. Returns ------- @@ -546,13 +541,6 @@ def tfr_array_multitaper( """ from .tfr import _compute_tfr - if epoch_data is not None: - warn( - "The parameter for providing data will be switched from `epoch_data` to " - "`data` in 1.8. Use the `data` parameter to avoid this warning.", - FutureWarning, - ) - return _compute_tfr( data, freqs, diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index a9006ac443f..f31c834490a 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -77,7 +77,7 @@ def plot_psd( method="auto", average=False, dB=True, - estimate="auto", + estimate="power", xscale="linear", area_mode="std", area_alpha=0.33, @@ -553,7 +553,7 @@ def plot( picks=None, average=False, dB=True, - amplitude=None, + amplitude=False, xscale="linear", ci="sd", ci_alpha=0.3, @@ -581,14 +581,12 @@ def plot( ``ci_alpha`` control the style of the confidence band around the mean. Default is ``False``. %(dB_spectrum_plot)s - amplitude : bool | 'auto' + amplitude : bool Whether to plot an amplitude spectrum (``True``) or power spectrum - (``False``). If ``'auto'``, will plot a power spectrum when ``dB=True`` and - an amplitude spectrum otherwise. Default is ``'auto'``. + (``False``). .. versionchanged:: 1.8 - In version 1.8, the value ``amplitude="auto"`` will be removed. The - default value will change to ``amplitude=False``. + In version 1.8, the default changed to ``amplitude=False``. %(xscale_plot_psd)s ci : float | 'sd' | 'range' | None Type of confidence band drawn around the mean when ``average=True``. If @@ -633,15 +631,8 @@ def plot( titles = _handle_default("titles", None) units = _handle_default("units", None) - depr_message = ( - "The value of `amplitude='auto'` will be removed in MNE 1.8.0, and the new " - "default will be `amplitude=False`." - ) - if amplitude is None or amplitude == "auto": - warn(depr_message, FutureWarning) - estimate = "power" if dB else "amplitude" - else: - estimate = "amplitude" if amplitude else "power" + _validate_type(amplitude, bool, "amplitude") + estimate = "amplitude" if amplitude else "power" logger.info(f"Plotting {estimate} spectral density ({dB=}).") @@ -1413,7 +1404,7 @@ def average(self, method="mean"): spectrum : instance of Spectrum The aggregated spectrum object. """ - _validate_type(method, ("str", "callable")) + _validate_type(method, ("str", "callable"), "method") method = _make_combine_callable( method, axis=0, valid=("mean", "median"), keepdims=False ) diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index a6ea0be9739..a44c6aeaa17 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -285,8 +285,7 @@ def test_spectrum_kwarg_triaging(raw): with _record_warnings(), pytest.warns(RuntimeWarning, match=regex): raw.plot_psd(axes=axes) # `ax` is the correct legacy param name - with pytest.warns(FutureWarning, match="amplitude='auto'"): - raw.plot_psd(ax=axes) + raw.plot_psd(ax=axes) def _check_spectrum_equivalent(spect1, spect2, tmp_path): diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index cedc13a479b..37a5fdc7724 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -733,19 +733,6 @@ def test_epochstfr_init_errors(epochs_tfr): EpochsTFR(inst=state | dict(freqs=epochs_tfr.freqs[:-1])) -@pytest.mark.parametrize("inst", ("epochs_tfr", "average_tfr")) -def test_tfr_init_deprecation(inst, average_tfr, request): - """Check for the deprecation warning message (not needed for RawTFR, it's new).""" - tfr = _get_inst(inst, request, average_tfr=average_tfr) - kwargs = dict(info=tfr.info, data=tfr.data, times=tfr.times, freqs=tfr.freqs) - Klass = tfr.__class__ - with pytest.warns(FutureWarning, match='"info", "data", "times" are deprecat'): - Klass(**kwargs) - with pytest.raises(ValueError, match="Do not pass `inst` alongside deprecated"): - with pytest.warns(FutureWarning, match='"info", "data", "times" are deprecat'): - Klass(**kwargs, inst="foo") - - @pytest.mark.parametrize( "method,freqs,match", ( diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 8f8599f757c..571f9683e75 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -930,13 +930,13 @@ def tfr_array_morlet( sfreq, freqs, n_cycles=7.0, - zero_mean=None, + zero_mean=True, use_fft=True, decim=1, output="complex", n_jobs=None, + *, verbose=None, - epoch_data=None, ): """Compute Time-Frequency Representation (TFR) using Morlet wavelets. @@ -956,7 +956,7 @@ def tfr_array_morlet( .. versionchanged:: 1.8 The default will change from ``zero_mean=False`` in 1.6 to ``True`` in - 1.8, and (if not set explicitly) will raise a ``FutureWarning`` in 1.7. + 1.8. use_fft : bool Use the FFT for convolutions or not. default True. @@ -974,10 +974,6 @@ def tfr_array_morlet( The number of epochs to process at the same time. The parallelization is implemented across channels. Default 1. %(verbose)s - epoch_data : None - Deprecated parameter for providing epoched data as of 1.7, will be replaced with - the ``data`` parameter in 1.8. New code should use the ``data`` parameter. If - ``epoch_data`` is not ``None``, a warning will be raised. Returns ------- @@ -1012,20 +1008,6 @@ def tfr_array_morlet( ---------- .. footbibliography:: """ - if zero_mean is None: - warn( - "The default value of `zero_mean` will change from `False` to `True` " - "in version 1.8. Set the value explicitly to avoid this warning.", - FutureWarning, - ) - zero_mean = False - if epoch_data is not None: - warn( - "The parameter for providing data will be switched from `epoch_data` to " - "`data` in 1.8. Use the `data` parameter to avoid this warning.", - FutureWarning, - ) - return _compute_tfr( epoch_data=data, freqs=freqs, @@ -2855,30 +2837,6 @@ def __init__( from ..evoked import Evoked from ._stockwell import _check_input_st, _compute_freqs_st - # deprecations. TODO remove after 1.7 release - depr_params = dict(info=info, data=data, times=times, nave=nave) - bad_params = list() - for name, param in depr_params.items(): - if param is not None: - bad_params.append(name) - if len(bad_params): - _s = _pl(bad_params) - is_are = _pl(bad_params, "is", "are") - bad_params_list = '", "'.join(bad_params) - warn( - f'Parameter{_s} "{bad_params_list}" {is_are} deprecated and will be ' - "removed in version 1.8. For a quick fix, use ``AverageTFRArray`` with " - "the same parameters. For a long-term fix, see the docstring notes.", - FutureWarning, - ) - if inst is not None: - raise ValueError( - "Do not pass `inst` alongside deprecated params " - f'"{bad_params_list}"; see docstring of AverageTFR for guidance.' - ) - inst = depr_params | dict(freqs=freqs, method=method, comment=comment) - # end TODO ↑↑↑↑↑↑ - # dict is allowed for __setstate__ compatibility, and Epochs.compute_tfr() can # return an AverageTFR depending on its parameters, so Epochs input is allowed _validate_type( diff --git a/mne/utils/__init__.pyi b/mne/utils/__init__.pyi index e22d8f6166c..54dc5272c37 100644 --- a/mne/utils/__init__.pyi +++ b/mne/utils/__init__.pyi @@ -159,6 +159,8 @@ __all__ = [ "open_docs", "path_like", "pformat", + "pinv", + "pinvh", "random_permutation", "repr_html", "requires_freesurfer", @@ -316,6 +318,8 @@ from .linalg import ( _svd_lwork, _sym_mat_pow, eigh, + pinv, + pinvh, sqrtm_sym, ) from .misc import ( diff --git a/mne/utils/check.py b/mne/utils/check.py index 80d87cafd2b..89d54b3386b 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -190,12 +190,8 @@ def check_random_state(seed): return np.random.mtrand.RandomState(seed) if isinstance(seed, np.random.mtrand.RandomState): return seed - try: - # Generator is only available in numpy >= 1.17 - if isinstance(seed, np.random.Generator): - return seed - except AttributeError: - pass + if isinstance(seed, np.random.Generator): + return seed raise ValueError( "%r cannot be used to seed a " "numpy.random.mtrand.RandomState instance" % seed ) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index f29ff9508a5..b1c15badcd3 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1286,10 +1286,9 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ docdict["estimate_plot_psd"] = """\ -estimate : str, {'auto', 'power', 'amplitude'} - Can be "power" for power spectral density (PSD), "amplitude" for - amplitude spectrum density (ASD), or "auto" (default), which uses - "power" when dB is True and "amplitude" otherwise. +estimate : str, {'power', 'amplitude'} + Can be "power" for power spectral density (PSD; default), "amplitude" for + amplitude spectrum density (ASD). """ docdict["event_color"] = """ diff --git a/mne/utils/linalg.py b/mne/utils/linalg.py index 9b36f0ae1ed..43574f317f0 100644 --- a/mne/utils/linalg.py +++ b/mne/utils/linalg.py @@ -28,6 +28,8 @@ from scipy import linalg from scipy._lib._util import _asarray_validated +from ..fixes import _safe_svd + # For efficiency, names should be str or tuple of str, dtype a builtin # NumPy dtype @@ -188,3 +190,56 @@ def _sym_mat_pow(A, power, rcond=1e-7, reduce_rank=False, return_s=False): if return_s: out = (out, s) return out + + +# SciPy deprecation of pinv + pinvh rcond (never worked properly anyway) +def pinvh(a, rtol=None): + """Compute a pseudo-inverse of a Hermitian matrix. + + Parameters + ---------- + a : ndarray, shape (n, n) + The Hermitian array to invert. + rtol : float | None + The relative tolerance. + + Returns + ------- + a_pinv : ndarray, shape (n, n) + The pseudo-inverse of a. + """ + s, u = np.linalg.eigh(a) + del a + if rtol is None: + rtol = s.size * np.finfo(s.dtype).eps + maxS = np.max(np.abs(s)) + above_cutoff = abs(s) > maxS * rtol + psigma_diag = 1.0 / s[above_cutoff] + u = u[:, above_cutoff] + return (u * psigma_diag) @ u.conj().T + + +def pinv(a, rtol=None): + """Compute a pseudo-inverse of a matrix. + + Parameters + ---------- + a : ndarray, shape (n, m) + The array to invert. + rtol : float | None + The relative tolerance. + + Returns + ------- + a_pinv : ndarray, shape (m, n) + The pseudo-inverse of a. + """ + u, s, vh = _safe_svd(a, full_matrices=False) + del a + maxS = np.max(s) + if rtol is None: + rtol = max(vh.shape + u.shape) * np.finfo(u.dtype).eps + rank = np.sum(s > maxS * rtol) + u = u[:, :rank] + u /= s[:rank] + return (u @ vh[:rank]).conj().T diff --git a/mne/utils/spectrum.py b/mne/utils/spectrum.py index 67a68b344a7..92ed4170c83 100644 --- a/mne/utils/spectrum.py +++ b/mne/utils/spectrum.py @@ -55,7 +55,7 @@ def _update_old_psd_kwargs(kwargs): "ci_alpha", _pop_with_fallback(kwargs, "area_alpha", fallback_fun) ) est = _pop_with_fallback(kwargs, "estimate", fallback_fun) - kwargs.setdefault("amplitude", "auto" if est == "auto" else (est == "amplitude")) + kwargs.setdefault("amplitude", est == "amplitude") area_mode = _pop_with_fallback(kwargs, "area_mode", fallback_fun) kwargs.setdefault("ci", "sd" if area_mode == "std" else area_mode) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index ea470937300..c8252070a32 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -1141,8 +1141,7 @@ def test_brain_scraper(renderer_interactive_pyvistaqt, brain_gc, tmp_path): img = image.imread(fname) w = img.shape[1] w0 = size[0] - # With matplotlib 3.6 on Linux+conda we get a width of 624, - # similar tweak in test_brain_init above + # On Linux+conda we get a width of 624, similar tweak in test_brain_init above assert np.isclose(w, w0, atol=30) or np.isclose( w, w0 * 2, atol=30 ), f"w ∉ {{{w0}, {2 * w0}}}" # HiDPI diff --git a/mne/viz/_mpl_figure.py b/mne/viz/_mpl_figure.py index da19372d8bc..702a38b2319 100644 --- a/mne/viz/_mpl_figure.py +++ b/mne/viz/_mpl_figure.py @@ -38,7 +38,6 @@ import datetime import platform -import warnings from collections import OrderedDict from contextlib import contextmanager from functools import partial @@ -67,7 +66,6 @@ _fake_keypress, _fake_scroll, _merge_annotations, - _prop_kw, _set_window_title, _validate_if_list_of_axes, plot_sensors, @@ -75,15 +73,7 @@ ) name = "matplotlib" -with plt.ion(): - BACKEND = get_backend() -# This ↑↑↑↑↑↑↑↑↑↑↑↑↑ does weird things: -# https://github.com/matplotlib/matplotlib/issues/23298 -# but wrapping it in ion() context makes it go away. -# Moving this bit to a separate function in ../../fixes.py doesn't work. -# -# TODO: Once we require matplotlib 3.6 we should be able to remove this. -# It also causes some problems... see mne/viz/utils.py:plt_show() for details. +BACKEND = get_backend() # CONSTANTS (inches) ANNOTATION_FIG_PAD = 0.1 @@ -240,13 +230,7 @@ def _radiopress(self, event, *, draw=True): selector = self.mne.parent_fig.mne.ax_main.selector # https://github.com/matplotlib/matplotlib/issues/20618 # https://github.com/matplotlib/matplotlib/pull/20693 - try: # > 3.4.2 - selector.set_props(color=color, facecolor=color) - except AttributeError: - with warnings.catch_warnings(record=True): - warnings.simplefilter("ignore", DeprecationWarning) - selector.rect.set_color(color) - selector.rectprops.update(dict(facecolor=color)) + selector.set_props(color=color, facecolor=color) if draw: self.canvas.draw() @@ -1141,7 +1125,6 @@ def _create_annotation_fig(self): else: col = self.mne.annotation_segment_colors[self._get_annotation_labels()[0]] - rect_kw = _prop_kw("rect", dict(alpha=0.5, facecolor=col)) selector = SpanSelector( self.mne.ax_main, self._select_annotation_span, @@ -1149,7 +1132,7 @@ def _create_annotation_fig(self): minspan=0.1, useblit=True, button=1, - **rect_kw, + props=dict(alpha=0.5, facecolor=col), ) self.mne.ax_main.selector = selector self.mne._callback_ids["motion_notify_event"] = self.canvas.mpl_connect( @@ -2247,8 +2230,8 @@ def _toggle_vline(self, visible): self.mne.vline_visible = visible self.canvas.draw_idle() - # workaround: plt.close() doesn't spawn close_event on Agg backend - # (check MPL github issue #18609; scheduled to be fixed by MPL 3.6) + # workaround: plt.close() doesn't spawn close_event on Agg backend, this method + # can be removed once the _close_event in fixes.py is removed def _close_event(self, fig=None): """Force calling of the MPL figure close event.""" fig = fig or self @@ -2370,10 +2353,7 @@ def _figure(toolbar=True, FigureClass=MNEFigure, **kwargs): # TODO: for some reason for topomaps->_prepare_trellis the layout=constrained does # not work the first time (maybe toolbar=False?) if kwargs.get("layout") == "constrained": - if hasattr(fig, "set_layout_engine"): # 3.6+ - fig.set_layout_engine("constrained") - else: - fig.set_constrained_layout(True) + fig.set_layout_engine("constrained") # add event callbacks fig._add_default_callbacks() @@ -2488,7 +2468,7 @@ def _init_browser(**kwargs): # (can't do in __init__ due to get_position() calls) fig.canvas.draw() fig._update_zen_mode_offsets() - fig._resize(None) # needed for MPL >=3.4 + fig._resize(None) # needed for MPL # if scrollbars are supposed to start hidden, # set to True and then toggle diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index 9871a0c2647..af6ef0f6786 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -25,7 +25,6 @@ _picks_to_idx, ) from ..defaults import _handle_default -from ..fixes import _sharex from ..utils import _check_option, fill_doc, legacy, logger, verbose, warn from ..utils.spectrum import _split_psd_kwargs from .raw import _setup_channel_selections @@ -631,7 +630,7 @@ def _plot_epochs_image( ax["evoked"].set_xlim(tmin, tmax) ax["evoked"].lines[0].set_clip_on(True) ax["evoked"].collections[0].set_clip_on(True) - _sharex(ax["evoked"], ax_im) + ax["evoked"].sharex(ax_im) # fix the axes for proper updating during interactivity loc = ax_im.xaxis.get_major_locator() ax["evoked"].xaxis.set_major_locator(loc) @@ -1103,7 +1102,7 @@ def plot_epochs_psd( area_mode="std", area_alpha=0.33, dB=True, - estimate="auto", + estimate="power", show=True, n_jobs=None, average=False, diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 5883dfaf5f5..3c851cfca5d 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -28,7 +28,6 @@ pick_info, ) from ..defaults import _handle_default -from ..fixes import _is_last_row from ..utils import ( _check_ch_locs, _check_if_nan, @@ -66,7 +65,6 @@ _plot_masked_image, _prepare_joint_axes, _process_times, - _prop_kw, _set_title_multiple_electrodes, _set_window_title, _setup_ax_spines, @@ -341,7 +339,7 @@ def _plot_evoked( "If `group_by` is a dict, `axes` must be " "a dict of axes or None." ) _validate_if_list_of_axes(list(axes.values())) - remove_xlabels = any([_is_last_row(ax) for ax in axes.values()]) + remove_xlabels = any(ax.get_subplotspec().is_last_row() for ax in axes.values()) for sel in group_by: # ... we loop over selections if sel not in axes: raise ValueError( @@ -385,7 +383,7 @@ def _plot_evoked( draw=False, spatial_colors=spatial_colors, ) - if remove_xlabels and not _is_last_row(ax): + if remove_xlabels and not ax.get_subplotspec().is_last_row(): ax.set_xticklabels([]) ax.set_xlabel("") ims = [ax.images[0] for ax in axes.values()] @@ -848,14 +846,13 @@ def _plot_lines( ) blit = False if plt.get_backend() == "MacOSX" else True minspan = 0 if len(times) < 2 else times[1] - times[0] - rect_kw = _prop_kw("rect", dict(alpha=0.5, facecolor="red")) ax._span_selector = SpanSelector( ax, callback_onselect, "horizontal", minspan=minspan, useblit=blit, - **rect_kw, + props=dict(alpha=0.5, facecolor="red"), ) diff --git a/mne/viz/ica.py b/mne/viz/ica.py index 1ec18fde1da..1f53d1f1d22 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -379,10 +379,10 @@ def _plot_ica_properties_on_press(event, ica, pick, topomap_args): return fig -def _get_psd_label_and_std(this_psd, dB, ica, num_std): +def _get_psd_label_and_std(this_psd, dB, ica, num_std, *, estimate): """Handle setting up PSD for one component, for plot_ica_properties.""" psd_ylabel = _convert_psds( - this_psd, dB, estimate="auto", scaling=1.0, unit="AU", first_dim="epoch" + this_psd, dB, estimate=estimate, scaling=1.0, unit="AU", first_dim="epoch" ) psds_mean = this_psd.mean(axis=0) diffs = this_psd - psds_mean @@ -417,6 +417,7 @@ def plot_ica_properties( reject="auto", reject_by_annotation=True, *, + estimate="power", verbose=None, ): """Display component properties. @@ -487,6 +488,9 @@ def plot_ica_properties( %(reject_by_annotation_raw)s .. versionadded:: 0.21.0 + %(estimate_plot_psd)s + + .. versionadded:: 1.8.0 %(verbose)s Returns @@ -514,6 +518,7 @@ def plot_ica_properties( reject=reject, reject_by_annotation=reject_by_annotation, verbose=verbose, + estimate=estimate, precomputed_data=None, ) @@ -535,6 +540,7 @@ def _fast_plot_ica_properties( precomputed_data=None, reject_by_annotation=True, *, + estimate="power", verbose=None, ): """Display component properties.""" @@ -626,7 +632,11 @@ def set_title_and_labels(ax, title, xlab, ylab): for idx, pick in enumerate(picks): # calculate component-specific spectrum stuff psd_ylabel, psds_mean, spectrum_std = _get_psd_label_and_std( - psds[:, idx, :].copy(), dB, ica, num_std + psds[:, idx, :].copy(), + dB, + ica, + num_std, + estimate=estimate, ) # if more than one component, spawn additional figures and axes diff --git a/mne/viz/raw.py b/mne/viz/raw.py index dd90352d0cc..5366f8feec4 100644 --- a/mne/viz/raw.py +++ b/mne/viz/raw.py @@ -430,7 +430,7 @@ def plot_raw_psd( area_mode="std", area_alpha=0.33, dB=True, - estimate="auto", + estimate="power", show=True, n_jobs=None, average=False, diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index 3ac6bb108a2..eefe178516d 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -257,19 +257,10 @@ def test_plot_evoked_topomap_units(evoked, units, scalings, expected_unit): fig = evoked.plot_topomap( times=0.1, res=8, contours=0, sensors=False, units=units, scalings=scalings ) - # ideally we'd do this: - # cbar = [ax for ax in fig.axes if hasattr(ax, '_colorbar')] - # assert len(cbar) == 1 - # cbar = cbar[0] - # assert cbar.get_title() == expected_unit - # ...but not all matplotlib versions support it, and we can't use - # check_version because it's hard figure out exactly which MPL version - # is the cutoff since it relies on a private attribute. Based on some - # basic testing it's at least matplotlib version >= 3.5. - # So for now we just do this: - for ax in fig.axes: - if hasattr(ax, "_colorbar"): - assert ax.get_title() == expected_unit + cbar = [ax for ax in fig.axes if hasattr(ax, "_colorbar")] + assert len(cbar) == 1 + cbar = cbar[0] + assert cbar.get_title() == expected_unit @pytest.mark.parametrize("extrapolate", ("box", "local", "head")) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 5d2f2d95617..cb4c6e85249 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -22,7 +22,6 @@ from contextlib import contextmanager from datetime import datetime from functools import partial -from inspect import signature import numpy as np from decorator import decorator @@ -59,7 +58,6 @@ _pl, _to_rgb, _validate_type, - check_version, fill_doc, get_config, logger, @@ -798,10 +796,6 @@ def to_layout(self, **kwargs): return lt -def _old_mpl_events(): - return not check_version("matplotlib", "3.6") - - def _fake_click(fig, ax, point, xform="ax", button=1, kind="press", key=None): """Fake a click at a relative point within axes.""" from matplotlib import backend_bases @@ -813,40 +807,28 @@ def _fake_click(fig, ax, point, xform="ax", button=1, kind="press", key=None): else: assert xform == "pix" x, y = point - # This works on 3.6+, but not on <= 3.5.1 (lasso events not propagated) - if _old_mpl_events(): - if kind == "press": - fig.canvas.button_press_event(x=x, y=y, button=button) - elif kind == "release": - fig.canvas.button_release_event(x=x, y=y, button=button) - elif kind == "motion": - fig.canvas.motion_notify_event(x=x, y=y) + if kind in ("press", "release"): + kind = f"button_{kind}_event" else: - if kind in ("press", "release"): - kind = f"button_{kind}_event" - else: - assert kind == "motion" - kind = "motion_notify_event" - button = None - logger.debug(f"Faking {kind} @ ({x}, {y}) with button={button} and key={key}") - fig.canvas.callbacks.process( - kind, - backend_bases.MouseEvent( - name=kind, canvas=fig.canvas, x=x, y=y, button=button, key=key - ), - ) + assert kind == "motion" + kind = "motion_notify_event" + button = None + logger.debug(f"Faking {kind} @ ({x}, {y}) with button={button} and key={key}") + fig.canvas.callbacks.process( + kind, + backend_bases.MouseEvent( + name=kind, canvas=fig.canvas, x=x, y=y, button=button, key=key + ), + ) def _fake_keypress(fig, key): - if _old_mpl_events(): - fig.canvas.key_press_event(key) - else: - from matplotlib import backend_bases + from matplotlib import backend_bases - fig.canvas.callbacks.process( - "key_press_event", - backend_bases.KeyEvent(name="key_press_event", canvas=fig.canvas, key=key), - ) + fig.canvas.callbacks.process( + "key_press_event", + backend_bases.KeyEvent(name="key_press_event", canvas=fig.canvas, key=key), + ) def _fake_scroll(fig, x, y, step): @@ -1558,7 +1540,7 @@ def key_press(self, event): self.index = 0 cmap = self.cycle[self.index] self.cbar.mappable.set_cmap(cmap) - _draw_without_rendering(self.cbar) + self.cbar.ax.figure.draw_without_rendering() self.mappable.set_cmap(cmap) self._publish() @@ -1622,20 +1604,11 @@ def _update(self): self.cbar.set_ticks(AutoLocator()) self.cbar.update_ticks() - _draw_without_rendering(self.cbar) + self.cbar.ax.figure.draw_without_rendering() self.mappable.set_norm(self.cbar.norm) self.cbar.ax.figure.canvas.draw() -def _draw_without_rendering(cbar): - # draw_all deprecated in Matplotlib 3.6 - try: - meth = cbar.ax.figure.draw_without_rendering - except AttributeError: - meth = cbar.draw_all - return meth() - - class SelectFromCollection: """Select channels from a matplotlib collection using ``LassoSelector``. @@ -1699,8 +1672,9 @@ def __init__( self.ec[:, -1] = self.alpha_other self.lw = np.full(self.Npts, self.linewidth_other) - line_kw = _prop_kw("line", dict(color="red", linewidth=0.5)) - self.lasso = LassoSelector(ax, onselect=self.on_select, **line_kw) + self.lasso = LassoSelector( + ax, onselect=self.on_select, props=dict(color="red", linewidth=0.5) + ) self.selection = list() self.callbacks = list() @@ -2426,9 +2400,7 @@ def _convert_psds( msg += "\nThese channels might be dead." warn(msg, UserWarning) - if estimate == "auto": - estimate = "power" if dB else "amplitude" - + _check_option("estimate", estimate, ("power", "amplitude")) if estimate == "amplitude": np.sqrt(psds, out=psds) psds *= scaling @@ -2772,15 +2744,6 @@ def _generate_default_filename(ext=".png"): return "MNE" + dt_string + ext -def _prop_kw(kind, val): - # Can be removed in when we depend on matplotlib 3.5+ - # https://github.com/matplotlib/matplotlib/pull/20585 - from matplotlib.widgets import SpanSelector - - pre = "" if "props" in signature(SpanSelector).parameters else kind - return {pre + "props": val} - - def _handle_precompute(precompute): _validate_type(precompute, (bool, str, None), "precompute") if precompute is None: @@ -2839,12 +2802,7 @@ def get_cmap(cmap): elif not isinstance(colormap, colors.Colormap): colormap = get_cmap(colormap) if lut is not None: - # triage method for MPL 3.6 ('resampled') or older ('_resample') - if hasattr(colormap, "resampled"): - resampled = colormap.resampled - else: - resampled = colormap._resample - colormap = resampled(lut) + colormap = colormap.resampled(lut) return colormap diff --git a/pyproject.toml b/pyproject.toml index 5a2dbce91a6..270ae2bf9a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,9 @@ classifiers = [ ] scripts = { mne = "mne.commands.utils:main" } dependencies = [ - "numpy>=1.21.2", - "scipy>=1.7.1", - "matplotlib>=3.5.0", + "numpy>=1.23", + "scipy>=1.9", + "matplotlib>=3.6", "tqdm", "pooch>=1.5", "decorator", diff --git a/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py b/tools/dev/gen_css_for_mne.py similarity index 94% rename from mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py rename to tools/dev/gen_css_for_mne.py index 7eac8ecdaa0..ca7210c8918 100644 --- a/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py +++ b/tools/dev/gen_css_for_mne.py @@ -16,11 +16,12 @@ # Copyright the MNE-Python contributors. import base64 +import mne from pathlib import Path import rcssmin -base_dir = Path(".") +base_dir = Path(mne.__file__).parent / "report" / "js_and_css" / "bootstrap-icons" css_path_in = base_dir / "bootstrap-icons.css" css_path_out = base_dir / "bootstrap-icons.mne.css" css_minified_path_out = base_dir / "bootstrap-icons.mne.min.css" diff --git a/tools/github_actions_env_vars.sh b/tools/github_actions_env_vars.sh index b291468a58a..a2776063688 100755 --- a/tools/github_actions_env_vars.sh +++ b/tools/github_actions_env_vars.sh @@ -4,7 +4,7 @@ set -eo pipefail -x # old and minimal use conda if [[ "$MNE_CI_KIND" == "old" ]]; then echo "Setting conda env vars for old" - echo "CONDA_DEPENDENCIES=numpy=1.21.2 scipy=1.7.1 matplotlib=3.5.0 pandas=1.3.2 scikit-learn=1.0" >> $GITHUB_ENV + echo "CONDA_DEPENDENCIES=numpy=1.23 scipy=1.9 matplotlib=3.6 pandas=1.3.2 scikit-learn=1.1" >> $GITHUB_ENV echo "MNE_IGNORE_WARNINGS_IN_TESTS=true" >> $GITHUB_ENV echo "MNE_SKIP_NETWORK_TESTS=1" >> $GITHUB_ENV echo "MNE_QT_BACKEND=PyQt5" >> $GITHUB_ENV From 7d36247e14f1f3ba8a10d4d4e73511e643355cda Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 23 Apr 2024 18:07:20 -0400 Subject: [PATCH 013/153] MAINT: Enable vulture (#12569) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 3 ++ .pre-commit-config.yaml | 19 ++++--- azure-pipelines.yml | 7 +++ doc/changes/devel/12569.other.rst | 1 + mne/commands/mne_flash_bem.py | 1 + mne/inverse_sparse/mxne_optim.py | 58 +-------------------- mne/io/fieldtrip/tests/helpers.py | 2 +- mne/minimum_norm/inverse.py | 2 +- mne/preprocessing/_csd.py | 2 +- mne/surface.py | 6 +-- mne/time_frequency/tests/test_stft.py | 74 +++++++++++++-------------- mne/viz/_3d.py | 6 +-- mne/viz/_brain/_brain.py | 13 +++-- mne/viz/backends/_pyvista.py | 8 +-- mne/viz/evoked.py | 4 +- mne/viz/tests/test_utils.py | 1 - mne/viz/topo.py | 3 +- pyproject.toml | 14 +++++ tools/vulture_allowlist.py | 22 ++++++++ 19 files changed, 121 insertions(+), 125 deletions(-) create mode 100644 doc/changes/devel/12569.other.rst create mode 100644 tools/vulture_allowlist.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 68979e20033..75873658bb3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,9 @@ jobs: with: python-version: '3.12' - uses: pre-commit/action@v3.0.1 + - run: pip install mypy numpy scipy vulture + - run: mypy + - run: vulture bandit: name: Bandit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 730a3bafa83..dd1b0f0e873 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,11 +46,14 @@ repos: - tomli files: ^doc/.*\.(rst|inc)$ - # mypy - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 - hooks: - - id: mypy - # Avoid the conflict between mne/__init__.py and mne/__init__.pyi by ignoring the former - exclude: ^mne/(beamformer|channels|commands|datasets|decoding|export|forward|gui|html_templates|inverse_sparse|io|minimum_norm|preprocessing|report|simulation|source_space|stats|time_frequency|utils|viz)?/?__init__\.py$ - additional_dependencies: ["numpy==1.26.2"] +# The following are too slow to run on local commits, so let's only run on CIs: +# +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v1.9.0 +# hooks: +# - id: mypy +# +# - repo: https://github.com/jendrikseipp/vulture +# rev: 'v2.11' # or any later Vulture version +# hooks: +# - id: vulture diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7e2fa2bd397..e357dc5cf47 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -68,6 +68,13 @@ stages: make check-readme displayName: make check-readme condition: always() + - bash: mypy + displayName: mypy + condition: always() + - bash: vulture + displayName: vulture + condition: always() + - stage: Test condition: and(succeeded(), eq(dependencies.Check.outputs['Skip.result.start_main'], 'true')) diff --git a/doc/changes/devel/12569.other.rst b/doc/changes/devel/12569.other.rst new file mode 100644 index 00000000000..acbd7d79663 --- /dev/null +++ b/doc/changes/devel/12569.other.rst @@ -0,0 +1 @@ +Added `vulture `__ as a pre-commit hook and removed related dead code, by `Eric Larson`_. diff --git a/mne/commands/mne_flash_bem.py b/mne/commands/mne_flash_bem.py index 24923bd78a1..9dde09d2208 100644 --- a/mne/commands/mne_flash_bem.py +++ b/mne/commands/mne_flash_bem.py @@ -33,6 +33,7 @@ def _vararg_callback(option, opt_str, value, parser): assert value is None + del opt_str # required for input but not used value = [] for arg in parser.rargs: diff --git a/mne/inverse_sparse/mxne_optim.py b/mne/inverse_sparse/mxne_optim.py index dbac66a96f9..03a5fa20df7 100644 --- a/mne/inverse_sparse/mxne_optim.py +++ b/mne/inverse_sparse/mxne_optim.py @@ -135,7 +135,6 @@ def dgap_l21(M, G, X, active_set, alpha, n_orient): return gap, p_obj, d_obj, R -@verbose def _mixed_norm_solver_cd( M, G, @@ -143,7 +142,6 @@ def _mixed_norm_solver_cd( lipschitz_constant, maxit=10000, tol=1e-8, - verbose=None, init=None, n_orient=1, dgap_freq=10, @@ -173,7 +171,6 @@ def _mixed_norm_solver_cd( return X, active_set, p_obj -@verbose def _mixed_norm_solver_bcd( M, G, @@ -181,7 +178,6 @@ def _mixed_norm_solver_bcd( lipschitz_constant, maxit=200, tol=1e-8, - verbose=None, init=None, n_orient=1, dgap_freq=10, @@ -667,7 +663,6 @@ def gprime(w): active_set_size=active_set_size, dgap_freq=dgap_freq, solver=solver, - verbose=verbose, ) else: X, _active_set, _ = mixed_norm_solver( @@ -681,7 +676,6 @@ def gprime(w): active_set_size=None, dgap_freq=dgap_freq, solver=solver, - verbose=verbose, ) else: X, _active_set, _ = mixed_norm_solver( @@ -695,7 +689,6 @@ def gprime(w): active_set_size=None, dgap_freq=dgap_freq, solver=solver, - verbose=verbose, ) logger.info("active set size %d" % (_active_set.sum() / n_orient)) @@ -735,46 +728,6 @@ def gprime(w): # TF-MxNE -@verbose -def tf_lipschitz_constant(M, G, phi, phiT, tol=1e-3, verbose=None): - """Compute lipschitz constant for FISTA. - - It uses a power iteration method. - """ - n_times = M.shape[1] - n_points = G.shape[1] - iv = np.ones((n_points, n_times), dtype=np.float64) - v = phi(iv) - L = 1e100 - for it in range(100): - L_old = L - logger.info("Lipschitz estimation: iteration = %d" % it) - iv = np.real(phiT(v)) - Gv = np.dot(G, iv) - GtGv = np.dot(G.T, Gv) - w = phi(GtGv) - L = np.max(np.abs(w)) # l_inf norm - v = w / L - if abs((L - L_old) / L_old) < tol: - break - return L - - -def safe_max_abs(A, ia): - """Compute np.max(np.abs(A[ia])) possible with empty A.""" - if np.sum(ia): # ia is not empty - return np.max(np.abs(A[ia])) - else: - return 0.0 - - -def safe_max_abs_diff(A, ia, B, ib): - """Compute np.max(np.abs(A)) possible with empty A.""" - A = A[ia] if np.sum(ia) else 0.0 - B = B[ib] if np.sum(ia) else 0.0 - return np.max(np.abs(A - B)) - - class _Phi: """Have phi stft as callable w/o using a lambda that does not pickle.""" @@ -1146,6 +1099,7 @@ def _tf_mixed_norm_solver_bcd_( lipschitz_constant, phi, phiT, + *, w_space=None, w_time=None, n_orient=1, @@ -1153,8 +1107,6 @@ def _tf_mixed_norm_solver_bcd_( tol=1e-8, dgap_freq=10, perc=None, - timeit=True, - verbose=None, ): n_sources = G.shape[1] n_positions = n_sources // n_orient @@ -1282,7 +1234,6 @@ def _tf_mixed_norm_solver_bcd_( return Z, active_set, E, converged -@verbose def _tf_mixed_norm_solver_bcd_active_set( M, G, @@ -1291,6 +1242,7 @@ def _tf_mixed_norm_solver_bcd_active_set( lipschitz_constant, phi, phiT, + *, Z_init=None, w_space=None, w_time=None, @@ -1298,7 +1250,6 @@ def _tf_mixed_norm_solver_bcd_active_set( maxit=200, tol=1e-8, dgap_freq=10, - verbose=None, ): n_sensors, n_times = M.shape n_sources = G.shape[1] @@ -1344,7 +1295,6 @@ def _tf_mixed_norm_solver_bcd_active_set( maxit=1, tol=tol, perc=None, - verbose=verbose, ) E += E_tmp @@ -1380,7 +1330,6 @@ def _tf_mixed_norm_solver_bcd_active_set( tol=tol, dgap_freq=dgap_freq, perc=0.5, - verbose=verbose, ) active = np.where(active_set[::n_orient])[0] active_set[active_set] = as_.copy() @@ -1535,7 +1484,6 @@ def tf_mixed_norm_solver( maxit=maxit, tol=tol, dgap_freq=dgap_freq, - verbose=None, ) if np.any(active_set) and debias: @@ -1548,7 +1496,6 @@ def tf_mixed_norm_solver( return X, active_set, E -@verbose def iterative_tf_mixed_norm_solver( M, G, @@ -1692,7 +1639,6 @@ def g_time_prime_inv(Z): maxit=maxit, tol=tol, dgap_freq=dgap_freq, - verbose=None, ) active_set[active_set] = active_set_ diff --git a/mne/io/fieldtrip/tests/helpers.py b/mne/io/fieldtrip/tests/helpers.py index 66cb582dde9..4a4202253ca 100644 --- a/mne/io/fieldtrip/tests/helpers.py +++ b/mne/io/fieldtrip/tests/helpers.py @@ -205,7 +205,7 @@ def get_evoked(system): return epochs.average(picks=np.arange(len(epochs.ch_names))) -def check_info_fields(expected, actual, has_raw_info, ignore_long=True): +def check_info_fields(expected, actual, has_raw_info): """ Check if info fields are equal. diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py index 440ed3735f2..387e341370b 100644 --- a/mne/minimum_norm/inverse.py +++ b/mne/minimum_norm/inverse.py @@ -889,7 +889,7 @@ def _assemble_kernel(inv, label, method, pick_ori, use_cps=True, verbose=None): return K, noise_norm, vertno, source_nn -def _check_ori(pick_ori, source_ori, src, allow_vector=True): +def _check_ori(pick_ori, source_ori, src): """Check pick_ori.""" _check_option("pick_ori", pick_ori, [None, "normal", "vector"]) _check_src_normal(pick_ori, src) diff --git a/mne/preprocessing/_csd.py b/mne/preprocessing/_csd.py index a5f81cd3208..544ac0364d2 100644 --- a/mne/preprocessing/_csd.py +++ b/mne/preprocessing/_csd.py @@ -302,7 +302,7 @@ def compute_bridged_electrodes( return bridged_idx, ed_matrix # kernel density estimation - kde = gaussian_kde(ed_flat[ed_flat < lm_cutoff]) + kde = gaussian_kde(ed_flat[ed_flat < lm_cutoff], bw_method=bw_method) with np.errstate(invalid="ignore"): local_minimum = float( minimize_scalar( diff --git a/mne/surface.py b/mne/surface.py index 0334ee12ab0..d0e2a53b303 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -644,7 +644,7 @@ def _safe_query(rr, func, reduce=False, **kwargs): class _DistanceQuery: """Wrapper for fast distance queries.""" - def __init__(self, xhs, method="BallTree", allow_kdtree=False): + def __init__(self, xhs, method="BallTree"): assert method in ("BallTree", "KDTree", "cdist") # Fastest for our problems: balltree @@ -1660,7 +1660,7 @@ def _find_nearest_tri_pts( else: use_pt_tris = s.astype(np.int64) pp, qq, ptt, distt = _nearest_tri_edge( - use_pt_tris, rr[0], pqs[s], dists[s], a, b, c + use_pt_tris, pqs[s], dists[s], a, b, c ) if np.abs(distt) < np.abs(dist): p, q, pt, dist = pp, qq, ptt, distt @@ -1676,7 +1676,7 @@ def _find_nearest_tri_pts( @jit() -def _nearest_tri_edge(pt_tris, to_pt, pqs, dist, a, b, c): # pragma: no cover +def _nearest_tri_edge(pt_tris, pqs, dist, a, b, c): # pragma: no cover """Get nearest location from a point to the edge of a set of triangles.""" # We might do something intelligent here. However, for now # it is ok to do it in the hard way diff --git a/mne/time_frequency/tests/test_stft.py b/mne/time_frequency/tests/test_stft.py index 4e9fb0ece34..9432ba92d8c 100644 --- a/mne/time_frequency/tests/test_stft.py +++ b/mne/time_frequency/tests/test_stft.py @@ -21,50 +21,50 @@ def test_stft(T, wsize, tstep, f): """Test stft and istft tight frame property.""" sfreq = 1000.0 # Hz - if True: # just to minimize diff - # Test with low frequency signal - t = np.arange(T).astype(np.float64) - x = np.sin(2 * np.pi * f * t / sfreq) - x = np.array([x, x + 1.0]) - X = stft(x, wsize, tstep) - xp = istft(X, tstep, Tx=T) - freqs = stftfreq(wsize, sfreq=sfreq) + # Test with low frequency signal + t = np.arange(T).astype(np.float64) + x = np.sin(2 * np.pi * f * t / sfreq) + x = np.array([x, x + 1.0]) + X = stft(x, wsize, tstep) + xp = istft(X, tstep, Tx=T) - max_freq = freqs[np.argmax(np.sum(np.abs(X[0]) ** 2, axis=1))] + freqs = stftfreq(wsize, sfreq=sfreq) - assert X.shape[1] == len(freqs) - assert np.all(freqs >= 0.0) - assert np.abs(max_freq - f) < 1.0 - assert_array_almost_equal(x, xp, decimal=6) + max_freq = freqs[np.argmax(np.sum(np.abs(X[0]) ** 2, axis=1))] - # norm conservation thanks to tight frame property - assert_almost_equal( - np.sqrt(stft_norm2(X)), [linalg.norm(xx) for xx in x], decimal=6 - ) + assert X.shape[1] == len(freqs) + assert np.all(freqs >= 0.0) + assert np.abs(max_freq - f) < 1.0 + assert_array_almost_equal(x, xp, decimal=6) - # Test with random signal - x = np.random.randn(2, T) - wsize = 16 - tstep = 8 - X = stft(x, wsize, tstep) - xp = istft(X, tstep, Tx=T) + # norm conservation thanks to tight frame property + assert_almost_equal( + np.sqrt(stft_norm2(X)), [linalg.norm(xx) for xx in x], decimal=6 + ) - freqs = stftfreq(wsize, sfreq=1000) + # Test with random signal + x = np.random.randn(2, T) + wsize = 16 + tstep = 8 + X = stft(x, wsize, tstep) + xp = istft(X, tstep, Tx=T) - max_freq = freqs[np.argmax(np.sum(np.abs(X[0]) ** 2, axis=1))] + freqs = stftfreq(wsize, sfreq=1000) - assert X.shape[1] == len(freqs) - assert np.all(freqs >= 0.0) - assert_array_almost_equal(x, xp, decimal=6) + max_freq = freqs[np.argmax(np.sum(np.abs(X[0]) ** 2, axis=1))] - # norm conservation thanks to tight frame property - assert_almost_equal( - np.sqrt(stft_norm2(X)), [linalg.norm(xx) for xx in x], decimal=6 - ) + assert X.shape[1] == len(freqs) + assert np.all(freqs >= 0.0) + assert_array_almost_equal(x, xp, decimal=6) - # Try with empty array - x = np.zeros((0, T)) - X = stft(x, wsize, tstep) - xp = istft(X, tstep, T) - assert xp.shape == x.shape + # norm conservation thanks to tight frame property + assert_almost_equal( + np.sqrt(stft_norm2(X)), [linalg.norm(xx) for xx in x], decimal=6 + ) + + # Try with empty array + x = np.zeros((0, T)) + X = stft(x, wsize, tstep) + xp = istft(X, tstep, T) + assert xp.shape == x.shape diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 3bee0c4fd29..eb23035eb63 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -2786,7 +2786,7 @@ def plot_volume_source_estimates( del kind # XXX this assumes zooms are uniform, should probably mult by zooms... - dist_to_verts = _DistanceQuery(stc_ijk, allow_kdtree=True) + dist_to_verts = _DistanceQuery(stc_ijk) def _cut_coords_to_idx(cut_coords, img): """Convert voxel coordinates to index in stc.data.""" @@ -3489,8 +3489,8 @@ def plot_sparse_source_estimates( linestyle=linestyle, ) - ax.set_xlabel("Time (ms)", fontsize=18) - ax.set_ylabel("Source amplitude (nAm)", fontsize=18) + ax.set_xlabel("Time (ms)", fontsize=fontsize) + ax.set_ylabel("Source amplitude (nAm)", fontsize=fontsize) if fig_name is not None: ax.set_title(fig_name) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index da5ca5c3cd1..675a3bcc4f8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -163,9 +163,7 @@ class Brain: .. versionchanged:: 0.23 Default changed to "auto". offscreen : bool - If True, rendering will be done offscreen (not shown). Useful - mostly for generating images or screenshots, but can be buggy. - Use at your own risk. + Deprecated and will be removed in 1.9, do not use. interaction : str Can be "trackball" (default) or "terrain", i.e. a turntable-style camera. @@ -298,7 +296,7 @@ def __init__( views="auto", *, offset="auto", - offscreen=False, + offscreen=None, interaction="trackball", units="mm", view_layout="vertical", @@ -311,6 +309,12 @@ def __init__( _validate_type(subject, str, "subject") self._surf = surf + if offscreen is not None: + warn( + "The 'offscreen' parameter is deprecated and will be removed in 1.9. " + "as it has no effect", + FutureWarning, + ) if hemi is None: hemi = "vol" hemi = self._check_hemi(hemi, extras=("both", "split", "vol")) @@ -3657,7 +3661,6 @@ def _update_glyphs(self, hemi, vectors): scale_mode="vector", scale=scale_factor, opacity=vector_alpha, - name=str(hemi) + "_glyph", ) hemi_data["glyph_dataset"] = glyph_dataset hemi_data["glyph_mapper"] = glyph_mapper diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index b94163b2ec8..21526587707 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -619,6 +619,7 @@ def quiver3d( scale, mode, resolution=8, + *, glyph_height=None, glyph_center=None, glyph_resolution=None, @@ -627,13 +628,8 @@ def quiver3d( scalars=None, colormap=None, backface_culling=False, - line_width=2.0, - name=None, - glyph_width=None, - glyph_depth=None, glyph_radius=0.15, solid_transform=None, - *, clim=None, ): _check_option("mode", mode, ALLOWED_QUIVER_MODES) @@ -1274,12 +1270,12 @@ def _arrow_glyph(grid, factor): def _glyph( dataset, + *, scale_mode="scalar", orient=True, scalars=True, factor=1.0, geom=None, - tolerance=0.0, absolute=False, clamping=False, rng=None, diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 3c851cfca5d..7a1be5eb586 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2528,7 +2528,7 @@ def _get_ci_function_pce(ci, do_topo=False): def _plot_compare_evokeds( - ax, data_dict, conditions, times, ci_dict, styles, title, all_positive, topo + ax, data_dict, conditions, times, ci_dict, styles, title, topo ): """Plot evokeds (to compare them; with CIs) based on a data_dict.""" for condition in conditions: @@ -3181,7 +3181,7 @@ def click_func( # plot the data _times = [] if idx == -1 else times _plot_compare_evokeds( - ax, data, conditions, _times, cis, _styles, title, norm, do_topo + ax, data, conditions, _times, cis, _styles, title, do_topo ) # draw axes & vlines skip_axlabel = do_topo and (idx != -1) diff --git a/mne/viz/tests/test_utils.py b/mne/viz/tests/test_utils.py index cb9e40b583c..816f984ca77 100644 --- a/mne/viz/tests/test_utils.py +++ b/mne/viz/tests/test_utils.py @@ -101,7 +101,6 @@ def test_add_background_image(): # Background without changing aspect if ii == 0: ax_im = add_background_image(f, im) - return assert ax_im.get_aspect() == "auto" for ax in axs: assert ax.get_aspect() == 1 diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 11f6695e834..52a5193f2e0 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -412,6 +412,7 @@ def _imshow_tfr( vmin, vmax, onselect, + *, ylim=None, tfr=None, freq=None, @@ -424,7 +425,6 @@ def _imshow_tfr( mask_style="both", mask_cmap="Greys", mask_alpha=0.1, - is_jointplot=False, cnorm=None, ): """Show time-frequency map as two-dimensional image.""" @@ -475,6 +475,7 @@ def _imshow_tfr_unified( vmin, vmax, onselect, + *, ylim=None, tfr=None, freq=None, diff --git a/pyproject.toml b/pyproject.toml index 270ae2bf9a7..eb49c992bae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,7 @@ test = [ "wheel", "pre-commit", "mypy", + "vulture", ] # Dependencies for being able to run additional tests (rare/CIs/advanced devs) @@ -309,6 +310,13 @@ ignore_messages = "^.*(Unknown target name|Undefined substitution referenced)[^` ignore_errors = true scripts_are_modules = true strict = false +modules = ["mne"] +# Avoid the conflict between mne/__init__.py and mne/__init__.pyi by ignoring the former +exclude = '^mne/(beamformer|channels|commands|datasets|decoding|export|forward|gui|html_templates|inverse_sparse|io|minimum_norm|preprocessing|report|simulation|source_space|stats|time_frequency|utils|viz)?/?__init__\.py$' + +[[tool.mypy.overrides]] +module = ['scipy.*'] +ignore_missing_imports = true [[tool.mypy.overrides]] module = ['mne.annotations', 'mne.epochs', 'mne.evoked', 'mne.io'] @@ -377,3 +385,9 @@ showcontent = true enabled = true verify_pr_number = true changelog_skip_label = "no-changelog-entry-needed" + +[tool.vulture] +min_confidence = 70 +paths = ["mne", "tools/vulture_allowlist.py"] +sort_by_size = true +verbose = false diff --git a/tools/vulture_allowlist.py b/tools/vulture_allowlist.py new file mode 100644 index 00000000000..5c3d41c356e --- /dev/null +++ b/tools/vulture_allowlist.py @@ -0,0 +1,22 @@ +# Testing stuff +numba_conditional +options_3d +invisible_fig +brain_gc +windows_like_datetime +garbage_collect +renderer_notebook +qt_windows_closed +download_is_error +exitstatus +startdir +pg_backend +recwarn +verbose_debug +few_surfaces +disabled_event_channels + +# Others +exc_value +exc_type +estimate_head_mri_t # imported for backward compat From 7c0c07a1dfb8229a9fafd81b993b6cc75ce10a27 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 24 Apr 2024 12:57:15 -0400 Subject: [PATCH 014/153] MAINT: Fixes for sphinx 7.3 (#12574) Co-authored-by: Daniel McCloy --- doc/conf.py | 272 ++------------------------------- doc/sphinxext/mne_doc_utils.py | 245 +++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 261 insertions(+), 258 deletions(-) create mode 100644 doc/sphinxext/mne_doc_utils.py diff --git a/doc/conf.py b/doc/conf.py index 9d6c08a4c83..3433a666782 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -7,33 +7,28 @@ # Copyright the MNE-Python contributors. import faulthandler -import gc import os import subprocess import sys -import time -import warnings from datetime import datetime, timezone from importlib.metadata import metadata from pathlib import Path import matplotlib -import numpy as np +import pyvista import sphinx from numpydoc import docscrape +from sphinx.config import is_serializable from sphinx.domains.changeset import versionlabels -from sphinx_gallery.sorting import ExplicitOrder, FileNameSortKey +from sphinx_gallery.sorting import ExplicitOrder import mne import mne.html_templates._templates from mne.tests.test_docstring_parameters import error_ignores from mne.utils import ( - _assert_no_instances, linkcode_resolve, run_subprocess, - sizeof_fmt, ) -from mne.viz import Brain # noqa assert linkcode_resolve is not None # avoid flake warnings, used by numpydoc matplotlib.use("agg") @@ -55,6 +50,7 @@ curpath = Path(__file__).parent.resolve(strict=True) sys.path.append(str(curpath / "sphinxext")) +from mne_doc_utils import report_scraper, reset_warnings # noqa: E402 # -- Project information ----------------------------------------------------- @@ -83,7 +79,7 @@ # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "2.0" +needs_sphinx = "6.0" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -450,122 +446,21 @@ # -- Sphinx-gallery configuration -------------------------------------------- - -class Resetter: - """Simple class to make the str(obj) static for Sphinx build env hash.""" - - def __init__(self): - self.t0 = time.time() - - def __repr__(self): - """Make a stable repr.""" - return f"<{self.__class__.__name__}>" - - def __call__(self, gallery_conf, fname, when): - """Do the reset.""" - import matplotlib.pyplot as plt - - try: - from pyvista import Plotter # noqa - except ImportError: - Plotter = None # noqa - try: - from pyvistaqt import BackgroundPlotter # noqa - except ImportError: - BackgroundPlotter = None # noqa - try: - from vtkmodules.vtkCommonDataModel import vtkPolyData # noqa - except ImportError: - vtkPolyData = None # noqa - try: - from mne_qt_browser._pg_figure import MNEQtBrowser - except ImportError: - MNEQtBrowser = None - from mne.viz.backends.renderer import backend - - _Renderer = backend._Renderer if backend is not None else None - reset_warnings(gallery_conf, fname) - # in case users have interactive mode turned on in matplotlibrc, - # turn it off here (otherwise the build can be very slow) - plt.ioff() - plt.rcParams["animation.embed_limit"] = 40.0 - plt.rcParams["figure.raise_window"] = False - # https://github.com/sphinx-gallery/sphinx-gallery/pull/1243#issue-2043332860 - plt.rcParams["animation.html"] = "html5" - # neo holds on to an exception, which in turn holds a stack frame, - # which will keep alive the global vars during SG execution - try: - import neo - - neo.io.stimfitio.STFIO_ERR = None - except Exception: - pass - gc.collect() - - # Agg does not call close_event so let's clean up on our own :( - # https://github.com/matplotlib/matplotlib/issues/18609 - mne.viz.ui_events._cleanup_agg() - assert len(mne.viz.ui_events._event_channels) == 0, list( - mne.viz.ui_events._event_channels - ) - - when = f"mne/conf.py:Resetter.__call__:{when}:{fname}" - # Support stuff like - # MNE_SKIP_INSTANCE_ASSERTIONS="Brain,Plotter,BackgroundPlotter,vtkPolyData,_Renderer" make html-memory # noqa: E501 - # to just test MNEQtBrowser - skips = os.getenv("MNE_SKIP_INSTANCE_ASSERTIONS", "").lower() - prefix = "" - if skips not in ("true", "1", "all"): - prefix = "Clean " - skips = skips.split(",") - if "brain" not in skips: - _assert_no_instances(Brain, when) # calls gc.collect() - if Plotter is not None and "plotter" not in skips: - _assert_no_instances(Plotter, when) - if BackgroundPlotter is not None and "backgroundplotter" not in skips: - _assert_no_instances(BackgroundPlotter, when) - if vtkPolyData is not None and "vtkpolydata" not in skips: - _assert_no_instances(vtkPolyData, when) - if "_renderer" not in skips: - _assert_no_instances(_Renderer, when) - if MNEQtBrowser is not None and "mneqtbrowser" not in skips: - # Ensure any manual fig.close() events get properly handled - from mne_qt_browser._pg_figure import QApplication - - inst = QApplication.instance() - if inst is not None: - for _ in range(2): - inst.processEvents() - _assert_no_instances(MNEQtBrowser, when) - # This will overwrite some Sphinx printing but it's useful - # for memory timestamps - if os.getenv("SG_STAMP_STARTS", "").lower() == "true": - import psutil - - process = psutil.Process(os.getpid()) - mem = sizeof_fmt(process.memory_info().rss) - print(f"{prefix}{time.time() - self.t0:6.1f} s : {mem}".ljust(22)) - - examples_dirs = ["../tutorials", "../examples"] gallery_dirs = ["auto_tutorials", "auto_examples"] os.environ["_MNE_BUILDING_DOC"] = "true" scrapers = ("matplotlib",) mne.viz.set_3d_backend("pyvistaqt") -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - import pyvista pyvista.OFF_SCREEN = False pyvista.BUILDING_GALLERY = True -report_scraper = mne.report._ReportScraper() scrapers = ( "matplotlib", - mne.gui._GUIScraper(), - mne.viz._brain._BrainScraper(), + "mne_doc_utils.gui_scraper", + "mne_doc_utils.brain_scraper", "pyvista", - report_scraper, - mne.viz._scraper._MNEQtBrowserScraper(), + "mne_doc_utils.report_scraper", + "mne_doc_utils.mne_qt_browser_scraper", ) compress_images = ("images", "thumbnails") @@ -622,12 +517,15 @@ def __call__(self, gallery_conf, fname, when): "remove_config_comments": True, "min_reported_time": 1.0, "abort_on_example_error": False, - "reset_modules": ("matplotlib", Resetter()), # called w/each script + "reset_modules": ( + "matplotlib", + "mne_doc_utils.reset_modules", + ), # called w/each script "reset_modules_order": "both", "image_scrapers": scrapers, "show_memory": not sys.platform.startswith(("win", "darwin")), "line_numbers": False, # messes with style - "within_subsection_order": FileNameSortKey, + "within_subsection_order": "FileNameSortKey", "capture_repr": ("_repr_html_",), "junit": os.path.join("..", "test-results", "sphinx-gallery", "junit.xml"), "matplotlib_animations": True, @@ -674,6 +572,7 @@ def __call__(self, gallery_conf, fname, when): ), "copyfile_regex": r".*index\.rst", # allow custom index.rst files } +assert is_serializable(sphinx_gallery_conf) # Files were renamed from plot_* with: # find . -type f -name 'plot_*.py' -exec sh -c 'x="{}"; xn=`basename "${x}"`; git mv "$x" `dirname "${x}"`/${xn:5}' \; # noqa @@ -806,7 +705,6 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): ] suppress_warnings = [ "image.nonlocal_uri", # we intentionally link outside - "config.cache", # our rebuild is okay ] @@ -1291,150 +1189,10 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): # not chapters. latex_toplevel_sectioning = "part" -_np_print_defaults = np.get_printoptions() - - # -- Warnings management ----------------------------------------------------- - - -def reset_warnings(gallery_conf, fname): - """Ensure we are future compatible and ignore silly warnings.""" - # In principle, our examples should produce no warnings. - # Here we cause warnings to become errors, with a few exceptions. - # This list should be considered alongside - # setup.cfg -> [tool:pytest] -> filterwarnings - - # remove tweaks from other module imports or example runs - warnings.resetwarnings() - # restrict - warnings.filterwarnings("error") - # allow these, but show them - warnings.filterwarnings("always", '.*non-standard config type: "foo".*') - warnings.filterwarnings("always", '.*config type: "MNEE_USE_CUUDAA".*') - warnings.filterwarnings("always", ".*cannot make axes width small.*") - warnings.filterwarnings("always", ".*Axes that are not compatible.*") - warnings.filterwarnings("always", ".*FastICA did not converge.*") - # ECoG BIDS spec violations: - warnings.filterwarnings("always", ".*Fiducial point nasion not found.*") - warnings.filterwarnings("always", ".*DigMontage is only a subset of.*") - warnings.filterwarnings( # xhemi morph (should probably update sample) - "always", ".*does not exist, creating it and saving it.*" - ) - # internal warnings - warnings.filterwarnings("default", module="sphinx") - # allow these warnings, but don't show them - for key in ( - "invalid version and will not be supported", # pyxdf - "distutils Version classes are deprecated", # seaborn and neo - "is_categorical_dtype is deprecated", # seaborn - "`np.object` is a deprecated alias for the builtin `object`", # pyxdf - # nilearn, should be fixed in > 0.9.1 - "In future, it will be an error for 'np.bool_' scalars to", - # sklearn hasn't updated to SciPy's sym_pos dep - "The 'sym_pos' keyword is deprecated", - # numba - "`np.MachAr` is deprecated", - # joblib hasn't updated to avoid distutils - "distutils package is deprecated", - # jupyter - "Jupyter is migrating its paths to use standard", - r"Widget\..* is deprecated\.", - # PyQt6 - "Enum value .* is marked as deprecated", - # matplotlib PDF output - "The py23 module has been deprecated", - # pkg_resources - "Implementing implicit namespace packages", - "Deprecated call to `pkg_resources", - # nilearn - "pkg_resources is deprecated as an API", - r"The .* was deprecated in Matplotlib 3\.7", - # Matplotlib->tz - r"datetime\.datetime\.utcfromtimestamp", - # joblib - r"ast\.Num is deprecated", - r"Attribute n is deprecated and will be removed in Python 3\.14", - # numpydoc - r"ast\.NameConstant is deprecated and will be removed in Python 3\.14", - # pooch - r"Python 3\.14 will, by default, filter extracted tar archives.*", - # seaborn - r"DataFrameGroupBy\.apply operated on the grouping columns.*", - # pandas - r"\nPyarrow will become a required dependency of pandas.*", - # latexcodec - r"open_text is deprecated\. Use files.*", - ): - warnings.filterwarnings( # deal with other modules having bad imports - "ignore", message=".*%s.*" % key, category=DeprecationWarning - ) - warnings.filterwarnings( - "ignore", - message="Matplotlib is currently using agg, which is a non-GUI backend.*", - ) - warnings.filterwarnings( - "ignore", - message=".*is non-interactive, and thus cannot.*", - ) - # seaborn - warnings.filterwarnings( - "ignore", - message="The figure layout has changed to tight", - category=UserWarning, - ) - # xarray/netcdf4 - warnings.filterwarnings( - "ignore", - message=r"numpy\.ndarray size changed, may indicate.*", - category=RuntimeWarning, - ) - # qdarkstyle - warnings.filterwarnings( - "ignore", - message=r".*Setting theme=.*6 in qdarkstyle.*", - category=RuntimeWarning, - ) - # pandas, via seaborn (examples/time_frequency/time_frequency_erds.py) - for message in ( - "use_inf_as_na option is deprecated.*", - r"iteritems is deprecated.*Use \.items instead\.", - "is_categorical_dtype is deprecated.*", - "The default of observed=False.*", - "When grouping with a length-1 list-like.*", - ): - warnings.filterwarnings( - "ignore", - message=message, - category=FutureWarning, - ) - # pandas in 50_epochs_to_data_frame.py - warnings.filterwarnings( - "ignore", message=r"invalid value encountered in cast", category=RuntimeWarning - ) - # xarray _SixMetaPathImporter (?) - warnings.filterwarnings( - "ignore", message=r"falling back to find_module", category=ImportWarning - ) - # Sphinx deps - warnings.filterwarnings( - "ignore", message="The str interface for _CascadingStyleSheet.*" - ) - # mne-qt-browser until > 0.5.2 released - warnings.filterwarnings( - "ignore", - r"mne\.io\.pick.channel_indices_by_type is deprecated.*", - ) - - # In case we use np.set_printoptions in any tutorials, we only - # want it to affect those: - np.set_printoptions(**_np_print_defaults) - - reset_warnings(None, None) - # -- Fontawesome support ----------------------------------------------------- - brand_icons = ("apple", "linux", "windows", "discourse", "python") fixed_width_icons = ( # homepage: diff --git a/doc/sphinxext/mne_doc_utils.py b/doc/sphinxext/mne_doc_utils.py new file mode 100644 index 00000000000..334c544cda7 --- /dev/null +++ b/doc/sphinxext/mne_doc_utils.py @@ -0,0 +1,245 @@ +"""Doc building utils.""" + +import gc +import os +import time +import warnings + +import numpy as np + +import mne +from mne.utils import ( + _assert_no_instances, + sizeof_fmt, +) +from mne.viz import Brain + +_np_print_defaults = np.get_printoptions() + + +def reset_warnings(gallery_conf, fname): + """Ensure we are future compatible and ignore silly warnings.""" + # In principle, our examples should produce no warnings. + # Here we cause warnings to become errors, with a few exceptions. + # This list should be considered alongside + # setup.cfg -> [tool:pytest] -> filterwarnings + + # remove tweaks from other module imports or example runs + warnings.resetwarnings() + # restrict + warnings.filterwarnings("error") + # allow these, but show them + warnings.filterwarnings("always", '.*non-standard config type: "foo".*') + warnings.filterwarnings("always", '.*config type: "MNEE_USE_CUUDAA".*') + warnings.filterwarnings("always", ".*cannot make axes width small.*") + warnings.filterwarnings("always", ".*Axes that are not compatible.*") + warnings.filterwarnings("always", ".*FastICA did not converge.*") + # ECoG BIDS spec violations: + warnings.filterwarnings("always", ".*Fiducial point nasion not found.*") + warnings.filterwarnings("always", ".*DigMontage is only a subset of.*") + warnings.filterwarnings( # xhemi morph (should probably update sample) + "always", ".*does not exist, creating it and saving it.*" + ) + # internal warnings + warnings.filterwarnings("default", module="sphinx") + # allow these warnings, but don't show them + for key in ( + "invalid version and will not be supported", # pyxdf + "distutils Version classes are deprecated", # seaborn and neo + "is_categorical_dtype is deprecated", # seaborn + "`np.object` is a deprecated alias for the builtin `object`", # pyxdf + # nilearn, should be fixed in > 0.9.1 + "In future, it will be an error for 'np.bool_' scalars to", + # sklearn hasn't updated to SciPy's sym_pos dep + "The 'sym_pos' keyword is deprecated", + # numba + "`np.MachAr` is deprecated", + # joblib hasn't updated to avoid distutils + "distutils package is deprecated", + # jupyter + "Jupyter is migrating its paths to use standard", + r"Widget\..* is deprecated\.", + # PyQt6 + "Enum value .* is marked as deprecated", + # matplotlib PDF output + "The py23 module has been deprecated", + # pkg_resources + "Implementing implicit namespace packages", + "Deprecated call to `pkg_resources", + # nilearn + "pkg_resources is deprecated as an API", + r"The .* was deprecated in Matplotlib 3\.7", + # Matplotlib->tz + r"datetime\.datetime\.utcfromtimestamp", + # joblib + r"ast\.Num is deprecated", + r"Attribute n is deprecated and will be removed in Python 3\.14", + # numpydoc + r"ast\.NameConstant is deprecated and will be removed in Python 3\.14", + # pooch + r"Python 3\.14 will, by default, filter extracted tar archives.*", + # seaborn + r"DataFrameGroupBy\.apply operated on the grouping columns.*", + # pandas + r"\nPyarrow will become a required dependency of pandas.*", + # latexcodec + r"open_text is deprecated\. Use files.*", + ): + warnings.filterwarnings( # deal with other modules having bad imports + "ignore", message=".*%s.*" % key, category=DeprecationWarning + ) + warnings.filterwarnings( + "ignore", + message="Matplotlib is currently using agg, which is a non-GUI backend.*", + ) + warnings.filterwarnings( + "ignore", + message=".*is non-interactive, and thus cannot.*", + ) + # seaborn + warnings.filterwarnings( + "ignore", + message="The figure layout has changed to tight", + category=UserWarning, + ) + # xarray/netcdf4 + warnings.filterwarnings( + "ignore", + message=r"numpy\.ndarray size changed, may indicate.*", + category=RuntimeWarning, + ) + # qdarkstyle + warnings.filterwarnings( + "ignore", + message=r".*Setting theme=.*6 in qdarkstyle.*", + category=RuntimeWarning, + ) + # pandas, via seaborn (examples/time_frequency/time_frequency_erds.py) + for message in ( + "use_inf_as_na option is deprecated.*", + r"iteritems is deprecated.*Use \.items instead\.", + "is_categorical_dtype is deprecated.*", + "The default of observed=False.*", + "When grouping with a length-1 list-like.*", + ): + warnings.filterwarnings( + "ignore", + message=message, + category=FutureWarning, + ) + # pandas in 50_epochs_to_data_frame.py + warnings.filterwarnings( + "ignore", message=r"invalid value encountered in cast", category=RuntimeWarning + ) + # xarray _SixMetaPathImporter (?) + warnings.filterwarnings( + "ignore", message=r"falling back to find_module", category=ImportWarning + ) + # Sphinx deps + warnings.filterwarnings( + "ignore", message="The str interface for _CascadingStyleSheet.*" + ) + # mne-qt-browser until > 0.5.2 released + warnings.filterwarnings( + "ignore", + r"mne\.io\.pick.channel_indices_by_type is deprecated.*", + ) + + # In case we use np.set_printoptions in any tutorials, we only + # want it to affect those: + np.set_printoptions(**_np_print_defaults) + + +t0 = time.time() + + +def reset_modules(gallery_conf, fname, when): + """Do the reset.""" + import matplotlib.pyplot as plt + + try: + from pyvista import Plotter # noqa + except ImportError: + Plotter = None # noqa + try: + from pyvistaqt import BackgroundPlotter # noqa + except ImportError: + BackgroundPlotter = None # noqa + try: + from vtkmodules.vtkCommonDataModel import vtkPolyData # noqa + except ImportError: + vtkPolyData = None # noqa + try: + from mne_qt_browser._pg_figure import MNEQtBrowser + except ImportError: + MNEQtBrowser = None + from mne.viz.backends.renderer import backend + + _Renderer = backend._Renderer if backend is not None else None + reset_warnings(gallery_conf, fname) + # in case users have interactive mode turned on in matplotlibrc, + # turn it off here (otherwise the build can be very slow) + plt.ioff() + plt.rcParams["animation.embed_limit"] = 40.0 + plt.rcParams["figure.raise_window"] = False + # https://github.com/sphinx-gallery/sphinx-gallery/pull/1243#issue-2043332860 + plt.rcParams["animation.html"] = "html5" + # neo holds on to an exception, which in turn holds a stack frame, + # which will keep alive the global vars during SG execution + try: + import neo + + neo.io.stimfitio.STFIO_ERR = None + except Exception: + pass + gc.collect() + + # Agg does not call close_event so let's clean up on our own :( + # https://github.com/matplotlib/matplotlib/issues/18609 + mne.viz.ui_events._cleanup_agg() + assert len(mne.viz.ui_events._event_channels) == 0, list( + mne.viz.ui_events._event_channels + ) + + when = f"mne/conf.py:Resetter.__call__:{when}:{fname}" + # Support stuff like + # MNE_SKIP_INSTANCE_ASSERTIONS="Brain,Plotter,BackgroundPlotter,vtkPolyData,_Renderer" make html-memory # noqa: E501 + # to just test MNEQtBrowser + skips = os.getenv("MNE_SKIP_INSTANCE_ASSERTIONS", "").lower() + prefix = "" + if skips not in ("true", "1", "all"): + prefix = "Clean " + skips = skips.split(",") + if "brain" not in skips: + _assert_no_instances(Brain, when) # calls gc.collect() + if Plotter is not None and "plotter" not in skips: + _assert_no_instances(Plotter, when) + if BackgroundPlotter is not None and "backgroundplotter" not in skips: + _assert_no_instances(BackgroundPlotter, when) + if vtkPolyData is not None and "vtkpolydata" not in skips: + _assert_no_instances(vtkPolyData, when) + if "_renderer" not in skips: + _assert_no_instances(_Renderer, when) + if MNEQtBrowser is not None and "mneqtbrowser" not in skips: + # Ensure any manual fig.close() events get properly handled + from mne_qt_browser._pg_figure import QApplication + + inst = QApplication.instance() + if inst is not None: + for _ in range(2): + inst.processEvents() + _assert_no_instances(MNEQtBrowser, when) + # This will overwrite some Sphinx printing but it's useful + # for memory timestamps + if os.getenv("SG_STAMP_STARTS", "").lower() == "true": + import psutil + + process = psutil.Process(os.getpid()) + mem = sizeof_fmt(process.memory_info().rss) + print(f"{prefix}{time.time() - t0:6.1f} s : {mem}".ljust(22)) + + +report_scraper = mne.report._ReportScraper() +mne_qt_browser_scraper = mne.viz._scraper._MNEQtBrowserScraper() +brain_scraper = mne.viz._brain._BrainScraper() +gui_scraper = mne.gui._GUIScraper() diff --git a/pyproject.toml b/pyproject.toml index eb49c992bae..a3b58098482 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -143,7 +143,7 @@ test_extra = [ # Dependencies for building the documentation doc = [ - "sphinx>=6,<7.3", + "sphinx>=6", "numpydoc", "pydata_sphinx_theme==0.15.2", "sphinx-gallery", From cfb232a41eb316e2173fb4dba3ab3b23ab326995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Wed, 24 Apr 2024 23:15:05 +0200 Subject: [PATCH 015/153] Display EOG and ECG channel names in titles of ICA artifact score subplots generated by `Report.add_ica()` (#12573) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12573.newfeature.rst | 3 +++ mne/report/report.py | 5 ++++- mne/report/tests/test_report.py | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12573.newfeature.rst diff --git a/doc/changes/devel/12573.newfeature.rst b/doc/changes/devel/12573.newfeature.rst new file mode 100644 index 00000000000..147db983edc --- /dev/null +++ b/doc/changes/devel/12573.newfeature.rst @@ -0,0 +1,3 @@ +When plotting EOG and ECG artifact scores for ICA in :meth:`mne.Report.add_ica`, +the channel names used for artifact detection are now displayed in the titles of +each respective subplot, by `Richard Höchenberger`_. \ No newline at end of file diff --git a/mne/report/report.py b/mne/report/report.py index e5ee790c91f..f53292458eb 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -1739,7 +1739,10 @@ def _add_ica_artifact_sources( def _add_ica_artifact_scores( self, *, ica, scores, artifact_type, image_format, section, tags, replace ): - fig = ica.plot_scores(scores=scores, title=None, show=False) + assert artifact_type in ("EOG", "ECG") + fig = ica.plot_scores( + scores=scores, title=None, labels=artifact_type.lower(), show=False + ) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) self._add_figure( fig=fig, diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 8afb4fc9e80..eaf7025e9db 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -925,6 +925,13 @@ def test_manual_report_2d(tmp_path, invisible_fig): ica_ecg_scores = ica_eog_scores = np.array([3, 0, 0]) ica_ecg_evoked = ica_eog_evoked = epochs_without_metadata.average() + # Normally, ICA.find_bads_*() assembles the labels_ dict; since we didn't run any + # of these methods, fill in some fake values manually. + ica.labels_ = { + "ecg/0/fake ECG channel": [0], + "eog/0/fake EOG channel": [1], + } + r.add_raw(raw=raw, title="my raw data", tags=("raw",), psd=True, projs=False) r.add_raw(raw=raw, title="my raw data 2", psd=False, projs=False, butterfly=1) r.add_events(events=events_fname, title="my events", sfreq=raw.info["sfreq"]) From d99d498257eb06c479500a88cf6abf28e9d943bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Fri, 26 Apr 2024 16:20:51 +0200 Subject: [PATCH 016/153] Improve `Report` slider description to better explain how the interaction mode works (#12579) --- mne/html_templates/report/slider.html.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/html_templates/report/slider.html.jinja b/mne/html_templates/report/slider.html.jinja index 8012918e280..58ee8a9f9fc 100644 --- a/mne/html_templates/report/slider.html.jinja +++ b/mne/html_templates/report/slider.html.jinja @@ -19,7 +19,7 @@
From 59606aa0e3d43d7313a644d7c6be3e4ec863a1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Fri, 26 Apr 2024 19:02:27 +0200 Subject: [PATCH 017/153] Fix color scaling of evoked topomaps in `Report` (#12578) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12578.bugfix.rst | 3 +++ mne/report/report.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12578.bugfix.rst diff --git a/doc/changes/devel/12578.bugfix.rst b/doc/changes/devel/12578.bugfix.rst new file mode 100644 index 00000000000..e22dffdd020 --- /dev/null +++ b/doc/changes/devel/12578.bugfix.rst @@ -0,0 +1,3 @@ +The color scaling of Evoked topomaps added to reports via :meth:`mne.Report.add_evokeds` +was sometimes sub-optimal if bad channels were present in the data. This has now been fixed +and should be more consistent with the topomaps shown in the joint plots, by `Richard Höchenberger`_. diff --git a/mne/report/report.py b/mne/report/report.py index f53292458eb..f5082a1ab57 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -3549,7 +3549,9 @@ def _add_evoked_topomap_slider( continue vmax[ch_type] = ( - np.abs(evoked.copy().pick(ch_type, verbose=False).data).max() + np.abs( + evoked.copy().pick(ch_type, exclude="bads", verbose=False).data + ).max() ) * scalings[ch_type] if ch_type == "grad": vmin[ch_type] = 0 From 5a5b4f10467664b1444873ab879f4ae5724eedbb Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 26 Apr 2024 13:33:59 -0400 Subject: [PATCH 018/153] DOC: Work around PyQt6 linking bug (#12580) --- pyproject.toml | 4 ++-- tools/pyqt6_requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a3b58098482..ee89321df6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,8 +62,8 @@ hdf5 = ["h5io", "pymatreader"] full = [ "mne[hdf5]", "qtpy", - "PyQt6!=6.6.1", - "PyQt6-Qt6!=6.6.1,!=6.6.2,!=6.6.3", + "PyQt6!=6.6.0", + "PyQt6-Qt6!=6.6.0,!=6.7.0", "pyobjc-framework-Cocoa>=5.2.0; platform_system=='Darwin'", "sip", "scikit-learn", diff --git a/tools/pyqt6_requirements.txt b/tools/pyqt6_requirements.txt index 26ec8315141..329265c91cf 100644 --- a/tools/pyqt6_requirements.txt +++ b/tools/pyqt6_requirements.txt @@ -1,2 +1,2 @@ -PyQt6!=6.6.1 -PyQt6-Qt6!=6.6.1,!=6.6.2,!=6.6.3 +PyQt6!=6.6.0 +PyQt6-Qt6!=6.6.0,!=6.7.0 From 2448974c87828a4f4c2aac0e7c51376556e257b7 Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Sat, 27 Apr 2024 02:40:39 -0700 Subject: [PATCH 019/153] BUG: Use wmparc in add_volume_labels (#12576) Co-authored-by: Eric Larson Co-authored-by: Marijn van Vliet --- doc/changes/devel/12576.newfeature.rst | 1 + mne/_freesurfer.py | 18 ++++++++++------- mne/surface.py | 4 +--- mne/tests/test_surface.py | 2 -- mne/utils/docs.py | 10 +++++++--- mne/viz/_brain/_brain.py | 27 +++++--------------------- 6 files changed, 25 insertions(+), 37 deletions(-) create mode 100644 doc/changes/devel/12576.newfeature.rst diff --git a/doc/changes/devel/12576.newfeature.rst b/doc/changes/devel/12576.newfeature.rst new file mode 100644 index 00000000000..06ea4bb85b0 --- /dev/null +++ b/doc/changes/devel/12576.newfeature.rst @@ -0,0 +1 @@ +Use ``aseg='auto'`` for :meth:`mne.viz.Brain.add_volume_labels` and :func:`mne.get_montage_volume_labels` to use ``aparc+aseg`` by default or if not present use ``wmparc`` because freesurfer uses ``wmparc`` in the latest version, by `Alex Rockhill`_. diff --git a/mne/_freesurfer.py b/mne/_freesurfer.py index dd868c1ee0d..d0aef0b5225 100644 --- a/mne/_freesurfer.py +++ b/mne/_freesurfer.py @@ -50,13 +50,17 @@ def _get_aseg(aseg, subject, subjects_dir): """Check that the anatomical segmentation file exists and load it.""" nib = _import_nibabel("load aseg") subjects_dir = Path(get_subjects_dir(subjects_dir, raise_error=True)) - if not aseg.endswith("aseg"): - raise RuntimeError(f'`aseg` file path must end with "aseg", got {aseg}') - aseg = _check_fname( - subjects_dir / subject / "mri" / (aseg + ".mgz"), - overwrite="read", - must_exist=True, - ) + if aseg == "auto": # use aparc+aseg if auto + aseg = _check_fname( + subjects_dir / subject / "mri" / "aparc+aseg.mgz", + overwrite="read", + must_exist=False, + ) + if not aseg: # if doesn't exist use wmparc + aseg = subjects_dir / subject / "mri" / "wmparc.mgz" + else: + aseg = subjects_dir / subject / "mri" / f"{aseg}.mgz" + _check_fname(aseg, overwrite="read", must_exist=True) aseg = nib.load(aseg) aseg_data = np.array(aseg.dataobj) return aseg, aseg_data diff --git a/mne/surface.py b/mne/surface.py index d0e2a53b303..62e689b6dc6 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -2081,9 +2081,7 @@ def _vtk_smooth(pd, smooth): @fill_doc -def get_montage_volume_labels( - montage, subject, subjects_dir=None, aseg="aparc+aseg", dist=2 -): +def get_montage_volume_labels(montage, subject, subjects_dir=None, aseg="auto", dist=2): """Get regions of interest near channels from a Freesurfer parcellation. .. note:: This is applicable for channels inside the brain diff --git a/mne/tests/test_surface.py b/mne/tests/test_surface.py index 6199bdfbe41..5fa5aa4fd49 100644 --- a/mne/tests/test_surface.py +++ b/mne/tests/test_surface.py @@ -312,8 +312,6 @@ def test_get_montage_volume_labels(): np.testing.assert_almost_equal(colors["Unknown"], (0.0, 0.0, 0.0, 1.0)) # test inputs - with pytest.raises(RuntimeError, match='`aseg` file path must end with "aseg"'): - get_montage_volume_labels(montage, "sample", subjects_dir, aseg="foo") fail_montage = make_dig_montage(ch_pos, coord_frame="head") with pytest.raises(RuntimeError, match="Coordinate frame not supported"): get_montage_volume_labels(fail_montage, "sample", subjects_dir, aseg="aseg") diff --git a/mne/utils/docs.py b/mne/utils/docs.py index b1c15badcd3..399ebe6fbef 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -308,9 +308,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["aseg"] = """ aseg : str - The anatomical segmentation file. Default ``aparc+aseg``. This may - be any anatomical segmentation file in the mri subdirectory of the - Freesurfer subject directory. + The anatomical segmentation file. Default ``auto`` uses ``aparc+aseg`` + if available and ``wmparc`` if not. This may be any anatomical + segmentation file in the mri subdirectory of the Freesurfer subject + directory. + + .. versionchanged:: 1.8 + Added support for the new default ``'auto'``. """ docdict["average_plot_evoked_topomap"] = """ diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 675a3bcc4f8..f4bccfc5447 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -26,6 +26,7 @@ from ..._fiff.pick import pick_types from ..._freesurfer import ( _estimate_talxfm_rigid, + _get_aseg, _get_head_surface, _get_skull_surface, read_freesurfer_lut, @@ -2537,7 +2538,7 @@ def remove_skull(self): @fill_doc def add_volume_labels( self, - aseg="aparc+aseg", + aseg="auto", labels=None, colors=None, alpha=0.5, @@ -2575,26 +2576,8 @@ def add_volume_labels( ----- .. versionadded:: 0.24 """ - import nibabel as nib - - # load anatomical segmentation image - if not aseg.endswith(("aseg", "parc")): - raise RuntimeError(f"Expected `aseg` file path, {aseg} suffix") - aseg = str( - _check_fname( - op.join( - self._subjects_dir, - self._subject, - "mri", - aseg + ".mgz", - ), - overwrite="read", - must_exist=True, - ) - ) - aseg_fname = aseg - aseg = nib.load(aseg_fname) - aseg_data = np.asarray(aseg.dataobj) + aseg, aseg_data = _get_aseg(aseg, self._subject, self._subjects_dir) + vox_mri_t = aseg.header.get_vox2ras_tkr() mult = 1e-3 if self._units == "m" else 1 vox_mri_t[:3] *= mult @@ -2628,7 +2611,7 @@ def add_volume_labels( if len(verts) == 0: # not in aseg vals warn( f"Value {lut[label]} not found for label " - f"{repr(label)} in: {aseg_fname}" + f"{repr(label)} in anatomical segmentation file " ) continue verts = apply_trans(vox_mri_t, verts) From 0b190849bac458e3dcbedb3b49f8e9891ded77ba Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Mon, 29 Apr 2024 12:03:54 +0200 Subject: [PATCH 020/153] Fix NumPy 2 related reshape in Persyst (#12585) --- mne/io/persyst/persyst.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/persyst/persyst.py b/mne/io/persyst/persyst.py index 11f8a3a35ea..c260f413205 100644 --- a/mne/io/persyst/persyst.py +++ b/mne/io/persyst/persyst.py @@ -282,7 +282,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): # chs * rows # cast as float32; more than enough precision - record = np.reshape(record, (n_chs, -1), "F").astype(np.float32) + record = np.reshape(record, (n_chs, -1), order="F").astype(np.float32) # calibrate to convert to V and handle mult _mult_cal_one(data, record, idx, cals, mult) From 47d26ec99ac4b65deeac5c561860bb9ed7473878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Mon, 29 Apr 2024 15:08:08 +0200 Subject: [PATCH 021/153] When rendering `Evoked` in a `Report`, now also include an "Info" section containing the HTML repr (#12584) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12584.newfeature.rst | 4 ++++ mne/report/report.py | 16 ++++++++++++++++ tutorials/intro/70_report.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12584.newfeature.rst diff --git a/doc/changes/devel/12584.newfeature.rst b/doc/changes/devel/12584.newfeature.rst new file mode 100644 index 00000000000..88f286afbbe --- /dev/null +++ b/doc/changes/devel/12584.newfeature.rst @@ -0,0 +1,4 @@ +When adding :class:`~mne.Evoked` data to a :class:`~mne.Report` via +:meth:`~mne.Report.add_evokeds`, we now also include an "Info" section +with some basic summary info, as has already been the case for raw and +epochs data, by `Richard Höchenberger`_. \ No newline at end of file diff --git a/mne/report/report.py b/mne/report/report.py index f5082a1ab57..e9ed4379e8f 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -3672,6 +3672,17 @@ def _add_evoked( n_jobs, replace, ): + # Summary table + self._add_html_repr( + inst=evoked, + title="Info", + tags=tags, + section=section, + replace=replace, + div_klass="evoked", + ) + + # Joint plot ch_types = _get_data_ch_types(evoked) self._add_evoked_joint( evoked=evoked, @@ -3682,6 +3693,8 @@ def _add_evoked( topomap_kwargs=topomap_kwargs, replace=replace, ) + + # Topomaps self._add_evoked_topomap_slider( evoked=evoked, ch_types=ch_types, @@ -3693,6 +3706,8 @@ def _add_evoked( n_jobs=n_jobs, replace=replace, ) + + # GFP self._add_evoked_gfp( evoked=evoked, ch_types=ch_types, @@ -3702,6 +3717,7 @@ def _add_evoked( replace=replace, ) + # Whitened evoked if noise_cov is not None: self._add_evoked_whitened( evoked=evoked, diff --git a/tutorials/intro/70_report.py b/tutorials/intro/70_report.py index 926e278838d..757e5a7c5ce 100644 --- a/tutorials/intro/70_report.py +++ b/tutorials/intro/70_report.py @@ -156,7 +156,7 @@ # noise covariance, we can add plots evokeds that were "whitened" using this # covariance matrix. # -# By default, this method will produce snapshots at 21 equally-spaced time +# By default, this method will produce topographic plots at 21 equally-spaced time # points (or fewer, if the data contains fewer time points). We can adjust this # via the ``n_time_points`` parameter. From 43414ba878ee7746464a8fd5aa7f4a555ea0bd5e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 29 Apr 2024 13:38:19 -0400 Subject: [PATCH 022/153] MAINT: Remove ref-names (#12586) --- .git_archival.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.git_archival.txt b/.git_archival.txt index 8fb235d7045..7c5100942aa 100644 --- a/.git_archival.txt +++ b/.git_archival.txt @@ -1,4 +1,3 @@ node: $Format:%H$ node-date: $Format:%cI$ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ -ref-names: $Format:%D$ From 8903b4a1a2a3f2a6c73e2e5793f8e793151de633 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Mon, 29 Apr 2024 15:23:57 -0500 Subject: [PATCH 023/153] add docstring note about legacy n_fft default (#12587) --- mne/time_frequency/spectrum.py | 2 +- mne/utils/docs.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index f31c834490a..1441e339d41 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -279,7 +279,7 @@ def _set_legacy_nfft_default(self, tmin, tmax, method, method_kw): This method returns ``None`` and has a side effect of (maybe) updating the ``method_kw`` dict. """ - if method == "welch" and method_kw.get("n_fft", None) is None: + if method == "welch" and method_kw.get("n_fft") is None: tm = _time_mask(self.times, tmin, tmax, sfreq=self.info["sfreq"]) method_kw["n_fft"] = min(np.sum(tm), 2048) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 399ebe6fbef..78e6ae7d3b5 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1704,7 +1704,8 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): _fmin_fmax = """\ fmin, fmax : float - The lower- and upper-bound on frequencies of interest. Default is {}""" + The lower- and upper-bound on frequencies of interest. Default is + {}""" docdict["fmin_fmax_psd"] = _fmin_fmax.format( "``fmin=0, fmax=np.inf`` (spans all frequencies present in the data)." @@ -2578,10 +2579,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): **method_kw Additional keyword arguments passed to the spectral estimation function (e.g., ``n_fft, n_overlap, n_per_seg, average, window`` - for Welch method, or - ``bandwidth, adaptive, low_bias, normalization`` for multitaper - method). See :func:`~mne.time_frequency.psd_array_welch` and - :func:`~mne.time_frequency.psd_array_multitaper` for details. + for Welch method, or ``bandwidth, adaptive, low_bias, normalization`` + for multitaper method). See :func:`~mne.time_frequency.psd_array_welch` + and :func:`~mne.time_frequency.psd_array_multitaper` for details. Note + that for Welch method if ``n_fft`` is unspecified its default will be + the smaller of ``2048`` or the number of available time samples (taking into + account ``tmin`` and ``tmax``), not ``256`` as in + :func:`~mne.time_frequency.psd_array_welch`. """ docdict["method_kw_tfr"] = _method_kw_tfr_template.format( From dddbe78f0c1a4140e9fd54904593a456b117f56a Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 30 Apr 2024 13:12:24 +0200 Subject: [PATCH 024/153] Slightly improve Epochs repr (#12550) --- mne/epochs.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index b733c73018c..d49c5792fa3 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2027,14 +2027,17 @@ def filename(self): def __repr__(self): """Build string representation.""" - s = f" {len(self.events)} events " + s = f"{len(self.events)} events " s += "(all good)" if self._bad_dropped else "(good & bad)" - s += f", {self.tmin:g} – {self.tmax:g} s" - s += ", baseline " + s += f", {self.tmin:.3f}".rstrip("0").rstrip(".") + s += f" – {self.tmax:.3f}".rstrip("0").rstrip(".") + s += " s (baseline " if self.baseline is None: s += "off" else: - s += f"{self.baseline[0]:g} – {self.baseline[1]:g} s" + s += f"{self.baseline[0]:.3f}".rstrip("0").rstrip(".") + s += f" – {self.baseline[1]:.3f}".rstrip("0").rstrip(".") + s += " s" if self.baseline != _check_baseline( self.baseline, times=self.times, @@ -2043,7 +2046,7 @@ def __repr__(self): ): s += " (baseline period was cropped after baseline correction)" - s += f", ~{sizeof_fmt(self._size)}" + s += f"), ~{sizeof_fmt(self._size)}" s += f", data{'' if self.preload else ' not'} loaded" s += ", with metadata" if self.metadata is not None else "" max_events = 10 From e3a40fc1b173cb4b49356864aa3551237c5d0d2d Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 30 Apr 2024 13:23:33 +0200 Subject: [PATCH 025/153] Fix navbar alignment and sidebar scrollbar (#12571) --- doc/_static/style.css | 8 ++++++++ doc/conf.py | 1 + 2 files changed, 9 insertions(+) diff --git a/doc/_static/style.css b/doc/_static/style.css index 11a27b72c92..25446d35659 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -353,3 +353,11 @@ div.sphx-glr-animation video { max-width: 100%; height: auto; } + +/* fix sidebar scrollbars */ +.sidebar-primary-items__end { + margin-bottom: 0 !important; + margin-top: 0 !important; + margin-left: 0 !important; + margin-right: 0 !important; +} diff --git a/doc/conf.py b/doc/conf.py index 3433a666782..ad4a158c1f9 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -757,6 +757,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): "version-switcher", "navbar-icon-links", ], + "navbar_align": "left", "navbar_persistent": ["search-button"], "footer_start": ["copyright"], "secondary_sidebar_items": ["page-toc", "edit-this-page"], From e39995d9be6fc831c7a4a59f09b7a7c0a41ae315 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 01:45:35 +0000 Subject: [PATCH 026/153] [pre-commit.ci] pre-commit autoupdate (#12588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Richard Höchenberger Co-authored-by: Eric Larson --- .pre-commit-config.yaml | 2 +- doc/sphinxext/flow_diagram.py | 10 +- doc/sphinxext/gen_commands.py | 2 +- doc/sphinxext/mne_doc_utils.py | 2 +- examples/decoding/receptive_field_mtrf.py | 2 +- examples/forward/forward_sensitivity_maps.py | 4 +- .../compute_mne_inverse_raw_in_label.py | 2 +- examples/inverse/label_activation_from_stc.py | 4 +- examples/inverse/label_from_stc.py | 4 +- examples/inverse/label_source_activations.py | 4 +- .../inverse/mixed_source_space_inverse.py | 3 +- examples/inverse/psf_ctf_vertices_lcmv.py | 4 +- examples/inverse/read_inverse.py | 16 +- .../time_frequency_mixed_norm_inverse.py | 2 +- .../preprocessing/eog_artifact_histogram.py | 2 +- examples/stats/sensor_permutation_test.py | 4 +- .../source_power_spectrum_opm.py | 11 +- .../source_space_time_frequency.py | 2 +- .../time_frequency_global_field_power.py | 2 +- mne/_fiff/_digitization.py | 4 +- mne/_fiff/compensator.py | 16 +- mne/_fiff/ctf_comp.py | 6 +- mne/_fiff/matrix.py | 8 +- mne/_fiff/meas_info.py | 65 ++--- mne/_fiff/open.py | 7 +- mne/_fiff/pick.py | 18 +- mne/_fiff/proc_history.py | 4 +- mne/_fiff/proj.py | 41 ++- mne/_fiff/reference.py | 24 +- mne/_fiff/tests/test_compensator.py | 2 +- mne/_fiff/tests/test_constants.py | 10 +- mne/_fiff/tests/test_meas_info.py | 4 +- mne/_fiff/tests/test_reference.py | 2 +- mne/_fiff/tree.py | 7 +- mne/_fiff/write.py | 2 +- mne/_freesurfer.py | 2 +- mne/_ola.py | 26 +- mne/annotations.py | 13 +- mne/baseline.py | 2 +- mne/beamformer/_compute_beamformer.py | 4 +- mne/beamformer/_dics.py | 2 +- mne/beamformer/_lcmv.py | 2 +- mne/beamformer/resolution_matrix.py | 4 +- mne/beamformer/tests/test_dics.py | 6 +- mne/beamformer/tests/test_lcmv.py | 8 +- mne/bem.py | 130 ++++----- mne/channels/_dig_montage_utils.py | 6 +- mne/channels/_standard_montage_utils.py | 2 +- mne/channels/channels.py | 12 +- mne/channels/layout.py | 4 +- mne/channels/montage.py | 14 +- mne/channels/tests/test_montage.py | 20 +- mne/chpi.py | 40 +-- mne/commands/mne_anonymize.py | 2 +- mne/commands/mne_browse_raw.py | 2 +- mne/commands/mne_clean_eog_ecg.py | 6 +- mne/commands/mne_compute_proj_ecg.py | 28 +- mne/commands/mne_compute_proj_eog.py | 26 +- mne/commands/mne_coreg.py | 4 +- mne/commands/mne_flash_bem.py | 4 +- mne/commands/mne_freeview_bem_surfaces.py | 12 +- mne/commands/mne_kit2fiff.py | 2 +- mne/commands/mne_make_scalp_surfaces.py | 2 +- mne/commands/mne_maxfilter.py | 257 ------------------ mne/commands/mne_report.py | 12 +- mne/commands/mne_setup_source_space.py | 2 +- mne/commands/mne_show_info.py | 4 +- mne/commands/mne_surf2bem.py | 2 +- mne/commands/mne_watershed_bem.py | 2 +- mne/commands/utils.py | 6 +- mne/conftest.py | 2 +- mne/coreg.py | 52 ++-- mne/cov.py | 6 +- mne/cuda.py | 11 +- mne/datasets/_fetch.py | 6 +- mne/datasets/brainstorm/bst_phantom_elekta.py | 2 +- mne/datasets/config.py | 4 +- mne/datasets/tests/test_datasets.py | 14 +- mne/datasets/utils.py | 12 +- mne/decoding/csp.py | 6 +- mne/decoding/receptive_field.py | 2 +- mne/decoding/search_light.py | 8 +- mne/decoding/tests/test_receptive_field.py | 2 +- mne/decoding/transformer.py | 24 +- mne/dipole.py | 34 ++- mne/epochs.py | 25 +- mne/event.py | 4 +- mne/evoked.py | 10 +- mne/export/_export.py | 2 +- mne/filter.py | 18 +- mne/fixes.py | 6 +- mne/forward/_compute_forward.py | 2 +- mne/forward/_lead_dots.py | 4 +- mne/forward/_make_forward.py | 28 +- mne/forward/forward.py | 42 ++- mne/gui/_coreg.py | 6 +- mne/inverse_sparse/_gamma_map.py | 2 +- mne/inverse_sparse/mxne_inverse.py | 33 +-- mne/inverse_sparse/mxne_optim.py | 6 +- mne/inverse_sparse/tests/test_mxne_inverse.py | 2 +- mne/io/array/tests/test_array.py | 2 +- mne/io/artemis123/artemis123.py | 44 ++- mne/io/artemis123/tests/test_artemis123.py | 6 +- mne/io/artemis123/utils.py | 4 +- mne/io/base.py | 22 +- mne/io/besa/besa.py | 4 +- mne/io/boxy/boxy.py | 5 +- mne/io/brainvision/brainvision.py | 14 +- mne/io/brainvision/tests/test_brainvision.py | 5 +- mne/io/bti/bti.py | 28 +- mne/io/bti/read.py | 2 +- mne/io/cnt/_utils.py | 2 +- mne/io/ctf/ctf.py | 4 +- mne/io/ctf/eeg.py | 4 +- mne/io/ctf/hc.py | 2 +- mne/io/ctf/info.py | 20 +- mne/io/ctf/res4.py | 4 +- mne/io/ctf/tests/test_ctf.py | 2 +- mne/io/ctf/trans.py | 8 +- mne/io/curry/curry.py | 6 +- mne/io/edf/edf.py | 2 +- mne/io/edf/tests/test_edf.py | 5 +- mne/io/eeglab/eeglab.py | 14 +- mne/io/egi/egi.py | 18 +- mne/io/egi/egimff.py | 14 +- mne/io/egi/general.py | 4 +- mne/io/eximia/eximia.py | 2 +- mne/io/eyelink/tests/test_eyelink.py | 4 +- mne/io/fieldtrip/fieldtrip.py | 2 +- mne/io/fieldtrip/tests/test_fieldtrip.py | 4 +- mne/io/fieldtrip/utils.py | 6 +- mne/io/fiff/raw.py | 4 +- mne/io/hitachi/hitachi.py | 4 +- mne/io/kit/kit.py | 18 +- mne/io/nicolet/nicolet.py | 2 +- mne/io/nihon/nihon.py | 8 +- mne/io/nirx/nirx.py | 6 +- mne/io/nirx/tests/test_nirx.py | 4 +- mne/io/persyst/persyst.py | 12 +- mne/io/persyst/tests/test_persyst.py | 2 +- mne/io/snirf/_snirf.py | 2 +- mne/label.py | 32 +-- mne/minimum_norm/_eloreta.py | 4 +- mne/minimum_norm/inverse.py | 26 +- mne/minimum_norm/resolution_matrix.py | 4 +- mne/minimum_norm/spatial_resolution.py | 4 +- mne/minimum_norm/tests/test_inverse.py | 2 +- mne/morph.py | 16 +- mne/morph_map.py | 6 +- mne/parallel.py | 2 +- mne/preprocessing/_csd.py | 12 +- mne/preprocessing/artifact_detection.py | 4 +- mne/preprocessing/bads.py | 2 +- mne/preprocessing/ecg.py | 5 +- mne/preprocessing/eog.py | 2 +- mne/preprocessing/eyetracking/eyetracking.py | 4 +- mne/preprocessing/ica.py | 32 +-- mne/preprocessing/infomax_.py | 4 +- mne/preprocessing/maxwell.py | 52 ++-- mne/preprocessing/nirs/_tddr.py | 4 +- mne/preprocessing/otp.py | 2 +- mne/preprocessing/ssp.py | 2 +- mne/preprocessing/stim.py | 4 +- .../tests/test_annotate_amplitude.py | 6 +- mne/preprocessing/tests/test_csd.py | 8 +- mne/preprocessing/tests/test_ica.py | 2 +- mne/preprocessing/xdawn.py | 6 +- mne/proj.py | 4 +- mne/rank.py | 2 +- mne/report/report.py | 16 +- mne/simulation/metrics/metrics.py | 2 +- mne/simulation/raw.py | 10 +- mne/simulation/source.py | 10 +- mne/simulation/tests/test_source.py | 6 +- mne/source_estimate.py | 34 ++- mne/source_space/_source_space.py | 68 ++--- mne/stats/cluster_level.py | 4 +- mne/stats/regression.py | 6 +- mne/surface.py | 37 +-- mne/tests/test_annotations.py | 6 +- mne/tests/test_bem.py | 6 +- mne/tests/test_coreg.py | 8 +- mne/tests/test_cov.py | 5 +- mne/tests/test_dipole.py | 10 +- mne/tests/test_docstring_parameters.py | 6 +- mne/tests/test_epochs.py | 4 +- mne/tests/test_event.py | 4 +- mne/tests/test_line_endings.py | 2 +- mne/tests/test_source_estimate.py | 2 +- mne/time_frequency/_stft.py | 4 +- mne/time_frequency/csd.py | 14 +- mne/time_frequency/psd.py | 2 +- mne/time_frequency/tfr.py | 30 +- mne/transforms.py | 10 +- mne/utils/_bunch.py | 2 +- mne/utils/_logging.py | 2 +- mne/utils/_testing.py | 4 +- mne/utils/check.py | 18 +- mne/utils/config.py | 20 +- mne/utils/docs.py | 8 +- mne/utils/misc.py | 6 +- mne/utils/mixin.py | 2 +- mne/utils/numerics.py | 22 +- mne/viz/_3d.py | 17 +- mne/viz/_brain/_brain.py | 14 +- mne/viz/_brain/surface.py | 2 +- mne/viz/_brain/tests/test_brain.py | 4 +- mne/viz/backends/renderer.py | 2 +- mne/viz/circle.py | 2 +- mne/viz/epochs.py | 2 +- mne/viz/evoked.py | 31 +-- mne/viz/evoked_field.py | 6 +- mne/viz/ica.py | 10 +- mne/viz/misc.py | 44 ++- mne/viz/tests/test_3d_mpl.py | 2 +- mne/viz/tests/test_circle.py | 2 +- mne/viz/tests/test_epochs.py | 2 +- mne/viz/topo.py | 16 +- mne/viz/topomap.py | 25 +- mne/viz/utils.py | 4 +- pyproject.toml | 2 +- tutorials/epochs/40_autogenerate_metadata.py | 2 +- tutorials/inverse/30_mne_dspm_loreta.py | 2 +- tutorials/inverse/50_beamformer_lcmv.py | 2 +- .../inverse/85_brainstorm_phantom_ctf.py | 8 +- tutorials/inverse/90_phantom_4DBTi.py | 2 +- tutorials/machine-learning/30_strf.py | 4 +- .../preprocessing/25_background_filtering.py | 6 +- .../stats-sensor-space/10_background_stats.py | 4 +- 229 files changed, 1046 insertions(+), 1486 deletions(-) delete mode 100644 mne/commands/mne_maxfilter.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd1b0f0e873..a120e2b7326 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.1 + rev: v0.4.2 hooks: - id: ruff name: ruff lint mne diff --git a/doc/sphinxext/flow_diagram.py b/doc/sphinxext/flow_diagram.py index cefe6713a7d..e8194d2063f 100644 --- a/doc/sphinxext/flow_diagram.py +++ b/doc/sphinxext/flow_diagram.py @@ -73,8 +73,8 @@ [ ("T1", "flashes", "recon", "bem", "src"), ( - '' - "Freesurfer / MNE-C>" % node_small_size + f'' + "Freesurfer / MNE-C>" ), ], ) @@ -107,10 +107,10 @@ def generate_flow_diagram(app): for key, label in nodes.items(): label = label.split("\n") if len(label) > 1: - label[0] = '<' % node_size + label[0] + "" + label[0] = f'<' + label[0] + "" for li in range(1, len(label)): label[li] = ( - '' % node_small_size + f'' + label[li] + "" ) @@ -142,7 +142,7 @@ def generate_flow_diagram(app): # Create subgraphs for si, subgraph in enumerate(subgraphs): - g.add_subgraph(subgraph[0], "cluster%s" % si, label=subgraph[1], color="black") + g.add_subgraph(subgraph[0], f"cluster{si}", label=subgraph[1], color="black") # Format (sub)graphs for gr in g.subgraphs() + [g]: diff --git a/doc/sphinxext/gen_commands.py b/doc/sphinxext/gen_commands.py index e50e243eb48..c369bba6db0 100644 --- a/doc/sphinxext/gen_commands.py +++ b/doc/sphinxext/gen_commands.py @@ -87,7 +87,7 @@ def generate_commands_rst(app=None): # Add code styling for the "Usage: " line for li, line in enumerate(output): if line.startswith("Usage: mne "): - output[li] = "Usage: ``%s``" % line[7:] + output[li] = f"Usage: ``{line[7:]}``" break # Turn "Options:" into field list diff --git a/doc/sphinxext/mne_doc_utils.py b/doc/sphinxext/mne_doc_utils.py index 334c544cda7..5e9e7621eb4 100644 --- a/doc/sphinxext/mne_doc_utils.py +++ b/doc/sphinxext/mne_doc_utils.py @@ -86,7 +86,7 @@ def reset_warnings(gallery_conf, fname): r"open_text is deprecated\. Use files.*", ): warnings.filterwarnings( # deal with other modules having bad imports - "ignore", message=".*%s.*" % key, category=DeprecationWarning + "ignore", message=f".*{key}.*", category=DeprecationWarning ) warnings.filterwarnings( "ignore", diff --git a/examples/decoding/receptive_field_mtrf.py b/examples/decoding/receptive_field_mtrf.py index 6d20b9ac582..89a97956559 100644 --- a/examples/decoding/receptive_field_mtrf.py +++ b/examples/decoding/receptive_field_mtrf.py @@ -160,7 +160,7 @@ mne.viz.plot_topomap( mean_coefs[:, ix_plot], pos=info, axes=ax, show=False, vlim=(-max_coef, max_coef) ) -ax.set(title="Topomap of model coefficients\nfor delay %s" % time_plot) +ax.set(title=f"Topomap of model coefficients\nfor delay {time_plot}") # %% # Create and fit a stimulus reconstruction model diff --git a/examples/forward/forward_sensitivity_maps.py b/examples/forward/forward_sensitivity_maps.py index abda7be4b16..c9163b5b792 100644 --- a/examples/forward/forward_sensitivity_maps.py +++ b/examples/forward/forward_sensitivity_maps.py @@ -38,7 +38,7 @@ fwd = mne.read_forward_solution(fwd_fname) mne.convert_forward_solution(fwd, surf_ori=True, copy=False) leadfield = fwd["sol"]["data"] -print("Leadfield size : %d x %d" % leadfield.shape) +print("Leadfield shape : {leadfield.shape}") # %% # Compute sensitivity maps @@ -107,7 +107,7 @@ # sensors. To determine the strength of this relationship, we can compute the # correlation between source depth and sensitivity values. corr = np.corrcoef(depths, grad_map.data[:, 0])[0, 1] -print("Correlation between source depth and gradiomter sensitivity values: %f." % corr) +print(f"Correlation between source depth and gradiomter sensitivity values: {corr:f}.") # %% # Gradiometer sensitiviy is highest close to the sensors, and decreases rapidly diff --git a/examples/inverse/compute_mne_inverse_raw_in_label.py b/examples/inverse/compute_mne_inverse_raw_in_label.py index ac97df8ff4b..b462c09e180 100644 --- a/examples/inverse/compute_mne_inverse_raw_in_label.py +++ b/examples/inverse/compute_mne_inverse_raw_in_label.py @@ -55,5 +55,5 @@ # View activation time-series plt.plot(1e3 * stc.times, stc.data[::100, :].T) plt.xlabel("time (ms)") -plt.ylabel("%s value" % method) +plt.ylabel(f"{method} value") plt.show() diff --git a/examples/inverse/label_activation_from_stc.py b/examples/inverse/label_activation_from_stc.py index ae0e528924a..daaf4c4ae12 100644 --- a/examples/inverse/label_activation_from_stc.py +++ b/examples/inverse/label_activation_from_stc.py @@ -59,8 +59,8 @@ # add a legend including center-of-mass mni coordinates to the plot labels = [ - "LH: center of mass = %s" % mni_lh.round(2), - "RH: center of mass = %s" % mni_rh.round(2), + f"LH: center of mass = {mni_lh.round(2)}", + f"RH: center of mass = {mni_rh.round(2)}", "Combined LH & RH", ] plt.figlegend([hl, hr, hb], labels, loc="lower center") diff --git a/examples/inverse/label_from_stc.py b/examples/inverse/label_from_stc.py index d73d7a20b45..76545d4895f 100644 --- a/examples/inverse/label_from_stc.py +++ b/examples/inverse/label_from_stc.py @@ -98,10 +98,10 @@ # plot the time courses.... plt.figure() plt.plot( - 1e3 * stc_anat_label.times, pca_anat, "k", label="Anatomical %s" % aparc_label_name + 1e3 * stc_anat_label.times, pca_anat, "k", label=f"Anatomical {aparc_label_name}" ) plt.plot( - 1e3 * stc_func_label.times, pca_func, "b", label="Functional %s" % aparc_label_name + 1e3 * stc_func_label.times, pca_func, "b", label=f"Functional {aparc_label_name}" ) plt.legend() plt.show() diff --git a/examples/inverse/label_source_activations.py b/examples/inverse/label_source_activations.py index 7640a468ebd..74d338486b0 100644 --- a/examples/inverse/label_source_activations.py +++ b/examples/inverse/label_source_activations.py @@ -57,7 +57,7 @@ tcs = dict() for mode in modes: tcs[mode] = stc.extract_label_time_course(label, src, mode=mode) -print("Number of vertices : %d" % len(stc_label.data)) +print(f"Number of vertices : {len(stc_label.data)}") # %% # View source activations @@ -78,7 +78,7 @@ ax.set( xlabel="Time (ms)", ylabel="Source amplitude", - title="Activations in Label %r" % (label.name), + title=f"Activations in Label {label.name!r}", xlim=xlim, ylim=ylim, ) diff --git a/examples/inverse/mixed_source_space_inverse.py b/examples/inverse/mixed_source_space_inverse.py index bec6fc6177d..a339c3ac667 100644 --- a/examples/inverse/mixed_source_space_inverse.py +++ b/examples/inverse/mixed_source_space_inverse.py @@ -120,7 +120,8 @@ del src # save memory leadfield = fwd["sol"]["data"] -print("Leadfield size : %d sensors x %d dipoles" % leadfield.shape) +ns, nd = leadfield.shape +print(f"Leadfield size : {ns} sensors x {nd} dipoles") print( f"The fwd source space contains {len(fwd['src'])} spaces and " f"{sum(s['nuse'] for s in fwd['src'])} vertices" diff --git a/examples/inverse/psf_ctf_vertices_lcmv.py b/examples/inverse/psf_ctf_vertices_lcmv.py index 569f77ab237..bf7009374e0 100644 --- a/examples/inverse/psf_ctf_vertices_lcmv.py +++ b/examples/inverse/psf_ctf_vertices_lcmv.py @@ -143,7 +143,7 @@ brain_pre.add_text( 0.1, 0.9, - "LCMV beamformer with pre-stimulus\ndata " "covariance matrix", + "LCMV beamformer with pre-stimulus\ndata covariance matrix", "title", font_size=16, ) @@ -168,7 +168,7 @@ brain_post.add_text( 0.1, 0.9, - "LCMV beamformer with post-stimulus\ndata " "covariance matrix", + "LCMV beamformer with post-stimulus\ndata covariance matrix", "title", font_size=16, ) diff --git a/examples/inverse/read_inverse.py b/examples/inverse/read_inverse.py index 95db394012e..148d09d84af 100644 --- a/examples/inverse/read_inverse.py +++ b/examples/inverse/read_inverse.py @@ -29,19 +29,19 @@ inv = read_inverse_operator(inv_fname) -print("Method: %s" % inv["methods"]) -print("fMRI prior: %s" % inv["fmri_prior"]) -print("Number of sources: %s" % inv["nsource"]) -print("Number of channels: %s" % inv["nchan"]) +print(f"Method: {inv['methods']}") +print(f"fMRI prior: {inv['fmri_prior']}") +print(f"Number of sources: {inv['nsource']}") +print(f"Number of channels: {inv['nchan']}") src = inv["src"] # get the source space # Get access to the triangulation of the cortex -print("Number of vertices on the left hemisphere: %d" % len(src[0]["rr"])) -print("Number of triangles on left hemisphere: %d" % len(src[0]["use_tris"])) -print("Number of vertices on the right hemisphere: %d" % len(src[1]["rr"])) -print("Number of triangles on right hemisphere: %d" % len(src[1]["use_tris"])) +print(f"Number of vertices on the left hemisphere: {len(src[0]['rr'])}") +print(f"Number of triangles on left hemisphere: {len(src[0]['use_tris'])}") +print(f"Number of vertices on the right hemisphere: {len(src[1]['rr'])}") +print(f"Number of triangles on right hemisphere: {len(src[1]['use_tris'])}") # %% # Show the 3D source space diff --git a/examples/inverse/time_frequency_mixed_norm_inverse.py b/examples/inverse/time_frequency_mixed_norm_inverse.py index 693c4ec88d5..bdd1134f39a 100644 --- a/examples/inverse/time_frequency_mixed_norm_inverse.py +++ b/examples/inverse/time_frequency_mixed_norm_inverse.py @@ -154,7 +154,7 @@ stc, bgcolor=(1, 1, 1), opacity=0.1, - fig_name="TF-MxNE (cond %s)" % condition, + fig_name=f"TF-MxNE (cond {condition})", modes=["sphere"], scale_factors=[1.0], ) diff --git a/examples/preprocessing/eog_artifact_histogram.py b/examples/preprocessing/eog_artifact_histogram.py index 8a89f9d8a44..ac51d8b1f39 100644 --- a/examples/preprocessing/eog_artifact_histogram.py +++ b/examples/preprocessing/eog_artifact_histogram.py @@ -52,4 +52,4 @@ # Plot EOG artifact distribution fig, ax = plt.subplots(layout="constrained") ax.stem(1e3 * epochs.times, data) -ax.set(xlabel="Times (ms)", ylabel="Blink counts (from %s trials)" % len(epochs)) +ax.set(xlabel="Times (ms)", ylabel=f"Blink counts (from {len(epochs)} trials)") diff --git a/examples/stats/sensor_permutation_test.py b/examples/stats/sensor_permutation_test.py index 7aaa75ba023..ded8cb9c314 100644 --- a/examples/stats/sensor_permutation_test.py +++ b/examples/stats/sensor_permutation_test.py @@ -66,8 +66,8 @@ significant_sensors = picks[p_values <= 0.05] significant_sensors_names = [raw.ch_names[k] for k in significant_sensors] -print("Number of significant sensors : %d" % len(significant_sensors)) -print("Sensors names : %s" % significant_sensors_names) +print(f"Number of significant sensors : {len(significant_sensors)}") +print(f"Sensors names : {significant_sensors_names}") # %% # View location of significantly active sensors diff --git a/examples/time_frequency/source_power_spectrum_opm.py b/examples/time_frequency/source_power_spectrum_opm.py index 8a12b78a9d3..ae8152670f2 100644 --- a/examples/time_frequency/source_power_spectrum_opm.py +++ b/examples/time_frequency/source_power_spectrum_opm.py @@ -77,7 +77,7 @@ titles = dict(vv="VectorView", opm="OPM") kinds = ("vv", "opm") n_fft = next_fast_len(int(round(4 * new_sfreq))) -print("Using n_fft=%d (%0.1f s)" % (n_fft, n_fft / raws["vv"].info["sfreq"])) +print(f"Using n_fft={n_fft} ({n_fft / raws['vv'].info['sfreq']:0.1f} s)") for kind in kinds: fig = ( raws[kind] @@ -184,13 +184,8 @@ def plot_band(kind, band): """Plot activity within a frequency band on the subject's brain.""" - title = "%s %s\n(%d-%d Hz)" % ( - ( - titles[kind], - band, - ) - + freq_bands[band] - ) + lf, hf = freq_bands[band] + title = f"{titles[kind]} {band}\n({lf:d}-{hf:d} Hz)" topos[kind][band].plot_topomap( times=0.0, scalings=1.0, diff --git a/examples/time_frequency/source_space_time_frequency.py b/examples/time_frequency/source_space_time_frequency.py index 119c06e9230..c5ca425dd4c 100644 --- a/examples/time_frequency/source_space_time_frequency.py +++ b/examples/time_frequency/source_space_time_frequency.py @@ -72,7 +72,7 @@ ) for b, stc in stcs.items(): - stc.save("induced_power_%s" % b, overwrite=True) + stc.save(f"induced_power_{b}", overwrite=True) # %% # plot mean power diff --git a/examples/time_frequency/time_frequency_global_field_power.py b/examples/time_frequency/time_frequency_global_field_power.py index 0bf72082442..cc4ff14ce2a 100644 --- a/examples/time_frequency/time_frequency_global_field_power.py +++ b/examples/time_frequency/time_frequency_global_field_power.py @@ -144,7 +144,7 @@ def stat_fun(x): ax.grid(True) ax.set_ylabel("GFP") ax.annotate( - "%s (%d-%dHz)" % (freq_name, fmin, fmax), + f"{freq_name} ({fmin:d}-{fmax:d}Hz)", xy=(0.95, 0.8), horizontalalignment="right", xycoords="axes fraction", diff --git a/mne/_fiff/_digitization.py b/mne/_fiff/_digitization.py index dcbf9e8d24d..27185a98b27 100644 --- a/mne/_fiff/_digitization.py +++ b/mne/_fiff/_digitization.py @@ -344,13 +344,13 @@ def _get_fid_coords(dig, raise_error=True): if len(fid_coord_frames) > 0 and raise_error: if set(fid_coord_frames.keys()) != set(["nasion", "lpa", "rpa"]): raise ValueError( - "Some fiducial points are missing (got %s)." % fid_coord_frames.keys() + f"Some fiducial points are missing (got {fid_coord_frames.keys()})." ) if len(set(fid_coord_frames.values())) > 1: raise ValueError( "All fiducial points must be in the same coordinate system " - "(got %s)" % len(fid_coord_frames) + f"(got {len(fid_coord_frames)})" ) coord_frame = fid_coord_frames.popitem()[1] if fid_coord_frames else None diff --git a/mne/_fiff/compensator.py b/mne/_fiff/compensator.py index 84a60f39614..e068a236c6d 100644 --- a/mne/_fiff/compensator.py +++ b/mne/_fiff/compensator.py @@ -16,9 +16,7 @@ def get_current_comp(info): if first_comp < 0: first_comp = comp elif comp != first_comp: - raise ValueError( - "Compensation is not set equally on " "all MEG channels" - ) + raise ValueError("Compensation is not set equally on all MEG channels") return comp @@ -42,11 +40,9 @@ def _make_compensator(info, grade): for col, col_name in enumerate(this_data["col_names"]): ind = [k for k, ch in enumerate(info["ch_names"]) if ch == col_name] if len(ind) == 0: - raise ValueError( - "Channel %s is not available in " "data" % col_name - ) + raise ValueError(f"Channel {col_name} is not available in data") elif len(ind) > 1: - raise ValueError("Ambiguous channel %s" % col_name) + raise ValueError(f"Ambiguous channel {col_name}") presel[col, ind[0]] = 1.0 # Create the postselector (zero entries for channels not found) @@ -56,14 +52,14 @@ def _make_compensator(info, grade): k for k, ch in enumerate(this_data["row_names"]) if ch == ch_name ] if len(ind) > 1: - raise ValueError("Ambiguous channel %s" % ch_name) + raise ValueError(f"Ambiguous channel {ch_name}") elif len(ind) == 1: postsel[c, ind[0]] = 1.0 # else, don't use it at all (postsel[c, ?] = 0.0) by allocation this_comp = np.dot(postsel, np.dot(this_data["data"], presel)) return this_comp - raise ValueError("Desired compensation matrix (grade = %d) not" " found" % grade) + raise ValueError(f"Desired compensation matrix (grade = {grade:d}) not found") @fill_doc @@ -120,7 +116,7 @@ def make_compensator(info, from_, to, exclude_comp_chs=False): if len(pick) == 0: raise ValueError( - "Nothing remains after excluding the " "compensation channels" + "Nothing remains after excluding the compensation channels" ) comp = comp[pick, :] diff --git a/mne/_fiff/ctf_comp.py b/mne/_fiff/ctf_comp.py index 940ef02e848..4d896039e84 100644 --- a/mne/_fiff/ctf_comp.py +++ b/mne/_fiff/ctf_comp.py @@ -43,8 +43,8 @@ def _calibrate_comp( p = ch_names.count(names[ii]) if p != 1: raise RuntimeError( - "Channel %s does not appear exactly once " - "in data, found %d instance%s" % (names[ii], p, _pl(p)) + f"Channel {names[ii]} does not appear exactly once " + f"in data, found {p:d} instance{_pl(p)}" ) idx = ch_names.index(names[ii]) val = chs[idx][mult_keys[0]] * chs[idx][mult_keys[1]] @@ -145,7 +145,7 @@ def _read_ctf_comp(fid, node, chs, ch_names_mapping): compdata.append(one) if len(compdata) > 0: - logger.info(" Read %d compensation matrices" % len(compdata)) + logger.info(f" Read {len(compdata)} compensation matrices") return compdata diff --git a/mne/_fiff/matrix.py b/mne/_fiff/matrix.py index db5cafbfc11..422c13ce490 100644 --- a/mne/_fiff/matrix.py +++ b/mne/_fiff/matrix.py @@ -52,13 +52,13 @@ def _read_named_matrix(fid, node, matkind, indent=" ", transpose=False): break else: logger.info( - indent + "Desired named matrix (kind = %d) not " "available" % matkind + f"{indent}Desired named matrix (kind = {matkind}) not available" ) return None else: if not has_tag(node, matkind): logger.info( - indent + "Desired named matrix (kind = %d) not " "available" % matkind + f"{indent}Desired named matrix (kind = {matkind}) not available" ) return None @@ -73,13 +73,13 @@ def _read_named_matrix(fid, node, matkind, indent=" ", transpose=False): tag = find_tag(fid, node, FIFF.FIFF_MNE_NROW) if tag is not None and tag.data != nrow: raise ValueError( - "Number of rows in matrix data and FIFF_MNE_NROW " "tag do not match" + "Number of rows in matrix data and FIFF_MNE_NROW tag do not match" ) tag = find_tag(fid, node, FIFF.FIFF_MNE_NCOL) if tag is not None and tag.data != ncol: raise ValueError( - "Number of columns in matrix data and " "FIFF_MNE_NCOL tag do not match" + "Number of columns in matrix data and FIFF_MNE_NCOL tag do not match" ) tag = find_tag(fid, node, FIFF.FIFF_MNE_ROW_NAMES) diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index a2928a9f2a6..631c8149b1c 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -25,7 +25,6 @@ _check_on_missing, _check_option, _dt_to_stamp, - _is_numeric, _on_missing, _pl, _stamp_to_dt, @@ -281,7 +280,7 @@ def _unique_channel_names(ch_names, max_length=None, verbose=None): dups = {ch_names[x] for x in np.setdiff1d(range(len(ch_names)), unique_ids)} warn( "Channel names are not unique, found duplicates for: " - "%s. Applying running numbers for duplicates." % dups + f"{dups}. Applying running numbers for duplicates." ) for ch_stem in dups: overlaps = np.where(np.array(ch_names) == ch_stem)[0] @@ -296,7 +295,7 @@ def _unique_channel_names(ch_names, max_length=None, verbose=None): for idx, ch_idx in enumerate(overlaps): # try idx first, then loop through lower case chars for suffix in (idx,) + suffixes: - ch_name = ch_stem + "-%s" % suffix + ch_name = ch_stem + f"-{suffix}" if ch_name not in ch_names: break if ch_name not in ch_names: @@ -305,7 +304,7 @@ def _unique_channel_names(ch_names, max_length=None, verbose=None): raise ValueError( "Adding a single alphanumeric for a " "duplicate resulted in another " - "duplicate name %s" % ch_name + f"duplicate name {ch_name}" ) return ch_names @@ -503,7 +502,7 @@ def _set_channel_positions(self, pos, names): info = self if isinstance(self, Info) else self.info if len(pos) != len(names): raise ValueError( - "Number of channel positions not equal to " "the number of names given." + "Number of channel positions not equal to the number of names given." ) pos = np.asarray(pos, dtype=np.float64) if pos.shape[-1] != 3 or pos.ndim != 2: @@ -516,7 +515,7 @@ def _set_channel_positions(self, pos, names): idx = self.ch_names.index(name) info["chs"][idx]["loc"][:3] = p else: - msg = "%s was not found in the info. Cannot be updated." % name + msg = f"{name} was not found in the info. Cannot be updated." raise ValueError(msg) @verbose @@ -562,7 +561,7 @@ def set_channel_types(self, mapping, *, on_unit_change="warn", verbose=None): for ch_name, ch_type in mapping.items(): if ch_name not in ch_names: raise ValueError( - "This channel name (%s) doesn't exist in " "info." % ch_name + f"This channel name ({ch_name}) doesn't exist in info." ) c_ind = ch_names.index(ch_name) @@ -1668,7 +1667,7 @@ def __repr__(self): elif k == "projs": if v: entr = ", ".join( - p["desc"] + ": o%s" % {0: "ff", 1: "n"}[p["active"]] for p in v + p["desc"] + ": o" + ("n" if p["active"] else "ff") for p in v ) entr = shorten(entr, MAX_WIDTH, placeholder=" ...") else: @@ -1684,12 +1683,12 @@ def __repr__(self): elif k == "dig" and v is not None: counts = Counter(d["kind"] for d in v) counts = [ - "%d %s" % (counts[ii], _dig_kind_proper[_dig_kind_rev[ii]]) + f"{counts[ii]} {_dig_kind_proper[_dig_kind_rev[ii]]}" for ii in _dig_kind_ints if ii in counts ] - counts = (" (%s)" % (", ".join(counts))) if len(counts) else "" - entr = "%d item%s%s" % (len(v), _pl(len(v)), counts) + counts = f" ({', '.join(counts)})" if len(counts) else "" + entr = f"{len(v)} item{_pl(v)}{counts}" elif isinstance(v, Transform): # show entry only for non-identity transform if not np.allclose(v["trans"], np.eye(v["trans"].shape[0])): @@ -1722,11 +1721,7 @@ def __repr__(self): entr = f"{v}" if v is not None else "" else: if this_len > 0: - entr = "%d item%s (%s)" % ( - this_len, - _pl(this_len), - type(v).__name__, - ) + entr = f"{this_len} item{_pl(this_len)} ({type(v).__name__})" else: entr = "" if entr != "": @@ -1815,23 +1810,15 @@ def _check_consistency(self, prepend_error=""): for ci, ch in enumerate(self["chs"]): _check_ch_keys(ch, ci) ch_name = ch["ch_name"] - if not isinstance(ch_name, str): - raise TypeError( - 'Bad info: info["chs"][%d]["ch_name"] is not a string, ' - "got type %s" % (ci, type(ch_name)) - ) + _validate_type(ch_name, str, f'info["chs"][{ci}]["ch_name"]') for key in _SCALAR_CH_KEYS: val = ch.get(key, 1) - if not _is_numeric(val): - raise TypeError( - 'Bad info: info["chs"][%d][%r] = %s is type %s, must ' - "be float or int" % (ci, key, val, type(val)) - ) + _validate_type(val, "numeric", f'info["chs"][{ci}][{key}]') loc = ch["loc"] if not (isinstance(loc, np.ndarray) and loc.shape == (12,)): raise TypeError( - 'Bad info: info["chs"][%d]["loc"] must be ndarray with ' - "12 elements, got %r" % (ci, loc) + f'Bad info: info["chs"][{ci}]["loc"] must be ndarray with ' + f"12 elements, got {repr(loc)}" ) # make sure channel names are unique @@ -2989,9 +2976,7 @@ def _where_isinstance(values, kind): if is_qual: return values[0] elif key == "meas_date": - logger.info( - "Found multiple entries for %s. " "Setting value to `None`" % key - ) + logger.info(f"Found multiple entries for {key}. Setting value to `None`") return None else: raise RuntimeError(msg) @@ -3007,10 +2992,10 @@ def _where_isinstance(values, kind): if len(unique_values) == 1: return list(values)[0] elif isinstance(list(unique_values)[0], BytesIO): - logger.info("Found multiple StringIO instances. " "Setting value to `None`") + logger.info("Found multiple StringIO instances. Setting value to `None`") return None elif isinstance(list(unique_values)[0], str): - logger.info("Found multiple filenames. " "Setting value to `None`") + logger.info("Found multiple filenames. Setting value to `None`") return None else: raise RuntimeError(msg) @@ -3059,7 +3044,7 @@ def _merge_info(infos, force_update_to_first=False, verbose=None): if len(duplicates) > 0: msg = ( "The following channels are present in more than one input " - "measurement info objects: %s" % list(duplicates) + f"measurement info objects: {list(duplicates)}" ) raise ValueError(msg) @@ -3078,7 +3063,7 @@ def _merge_info(infos, force_update_to_first=False, verbose=None): ): info[trans_name] = trans[0] else: - msg = "Measurement infos provide mutually inconsistent %s" % trans_name + msg = f"Measurement infos provide mutually inconsistent {trans_name}" raise ValueError(msg) # KIT system-IDs @@ -3101,7 +3086,7 @@ def _merge_info(infos, force_update_to_first=False, verbose=None): elif all(object_diff(values[0], v) == "" for v in values[1:]): info[k] = values[0] else: - msg = "Measurement infos are inconsistent for %s" % k + msg = f"Measurement infos are inconsistent for {k}" raise ValueError(msg) # other fields @@ -3367,7 +3352,7 @@ def _force_update_info(info_base, info_target): all_infos = np.hstack([info_base, info_target]) for ii in all_infos: if not isinstance(ii, Info): - raise ValueError("Inputs must be of type Info. " "Found type %s" % type(ii)) + raise ValueError("Inputs must be of type Info. " f"Found type {type(ii)}") for key, val in info_base.items(): if key in exclude_keys: continue @@ -3418,7 +3403,7 @@ def anonymize_info(info, daysback=None, keep_his=False, verbose=None): default_str = "mne_anonymize" default_subject_id = 0 default_sex = 0 - default_desc = "Anonymized using a time shift" " to preserve age at acquisition" + default_desc = "Anonymized using a time shift to preserve age at acquisition" none_meas_date = info["meas_date"] is None @@ -3464,7 +3449,7 @@ def anonymize_info(info, daysback=None, keep_his=False, verbose=None): subject_info["id"] = default_subject_id if keep_his: logger.info( - "Not fully anonymizing info - keeping " "his_id, sex, and hand info" + "Not fully anonymizing info - keeping his_id, sex, and hand info" ) else: if subject_info.get("his_id") is not None: @@ -3536,7 +3521,7 @@ def anonymize_info(info, daysback=None, keep_his=False, verbose=None): di[k] = default_str err_mesg = ( - "anonymize_info generated an inconsistent info object. " "Underlying Error:\n" + "anonymize_info generated an inconsistent info object. Underlying Error:\n" ) info._check_consistency(prepend_error=err_mesg) err_mesg = ( diff --git a/mne/_fiff/open.py b/mne/_fiff/open.py index 5bfcb83a951..abc32aab687 100644 --- a/mne/_fiff/open.py +++ b/mne/_fiff/open.py @@ -93,12 +93,13 @@ def _get_next_fname(fid, fname, tree): if idx2 < 0 and next_num == 1: # this is the first file, which may not be numbered next_fname = op.join( - path, "%s-%d.%s" % (base[:idx], next_num, base[idx + 1 :]) + path, + f"{base[:idx]}-{next_num:d}.{base[idx + 1 :]}", ) continue next_fname = op.join( - path, "%s-%d.%s" % (base[:idx2], next_num, base[idx + 1 :]) + path, f"{base[:idx2]}-{next_num:d}.{base[idx + 1 :]}" ) if next_fname is not None: break @@ -164,7 +165,7 @@ def _fiff_open(fname, fid, preload): raise ValueError(f"{prefix} have a directory pointer") # Read or create the directory tree - logger.debug(" Creating tag directory for %s..." % fname) + logger.debug(f" Creating tag directory for {fname}...") dirpos = int(tag.data.item()) read_slow = True diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index 2af49c7b921..88d9e112b42 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -629,7 +629,7 @@ def pick_info(info, sel=(), copy=True, verbose=None): n_unique = len(ch_set) if n_unique != len(sel): raise ValueError( - "Found %d / %d unique names, sel is not unique" % (n_unique, len(sel)) + f"Found {n_unique} / {len(sel)} unique names, sel is not unique" ) # make sure required the compensation channels are present @@ -638,8 +638,8 @@ def pick_info(info, sel=(), copy=True, verbose=None): _, comps_missing = _bad_chans_comp(info, ch_names) if len(comps_missing) > 0: logger.info( - "Removing %d compensators from info because " - "not all compensation channels were picked." % (len(info["comps"]),) + f"Removing {len(info['comps'])} compensators from info because " + "not all compensation channels were picked." ) with info._unlock(): info["comps"] = [] @@ -747,7 +747,7 @@ def pick_channels_forward( if nuse == 0: raise ValueError("Nothing remains after picking") - logger.info(" %d out of %d channels remain after picking" % (nuse, fwd["nchan"])) + logger.info(f" {nuse:d} out of {fwd['nchan']} channels remain after picking") # Pick the correct rows of the forward operator using sel_sol fwd["sol"]["data"] = fwd["sol"]["data"][sel_sol, :] @@ -1233,7 +1233,7 @@ def _picks_to_idx( if picks is None: if isinstance(info, int): # special wrapper for no real info picks = np.arange(n_chan) - extra_repr = ", treated as range(%d)" % (n_chan,) + extra_repr = ", treated as range({n_chan})" else: picks = none # let _picks_str_to_idx handle it extra_repr = f'None, treated as "{none}"' @@ -1283,10 +1283,10 @@ def _picks_to_idx( f"No appropriate {picks_on} found for the given picks ({orig_picks!r})" ) if (picks < -n_chan).any(): - raise IndexError("All picks must be >= %d, got %r" % (-n_chan, orig_picks)) + raise IndexError(f"All picks must be >= {-n_chan}, got {repr(orig_picks)}") if (picks >= n_chan).any(): raise IndexError( - "All picks must be < n_%s (%d), got %r" % (picks_on, n_chan, orig_picks) + f"All picks must be < n_{picks_on} ({n_chan}), got {repr(orig_picks)}" ) picks %= n_chan # ensure positive if return_kind: @@ -1301,7 +1301,7 @@ def _picks_str_to_idx( # special case for _picks_to_idx w/no info: shouldn't really happen if isinstance(info, int): raise ValueError( - "picks as str can only be used when measurement " "info is available" + "picks as str can only be used when measurement info is available" ) # @@ -1391,7 +1391,7 @@ def _picks_str_to_idx( if not allow_empty: raise ValueError( f"picks ({repr(orig_picks) + extra_repr}) could not be interpreted as " - f'channel names (no channel "{str(bad_names)}"), channel types (no type' + f'channel names (no channel "{bad_names}"), channel types (no type' f' "{bad_type}" present), or a generic type (just "all" or "data")' ) picks = np.array([], int) diff --git a/mne/_fiff/proc_history.py b/mne/_fiff/proc_history.py index 34203bfbb61..5cea6ab9a0e 100644 --- a/mne/_fiff/proc_history.py +++ b/mne/_fiff/proc_history.py @@ -105,7 +105,7 @@ def _read_proc_history(fid, tree): record[key] = cast(tag.data) break else: - warn("Unknown processing history item %s" % kind) + warn(f"Unknown processing history item {kind}") record["max_info"] = _read_maxfilter_record(fid, proc_record) iass = dir_tree_find(proc_record, FIFF.FIFFB_IAS) if len(iass) > 0: @@ -212,7 +212,7 @@ def _read_ctc(fname): f, tree, _ = fiff_open(fname) with f as fid: sss_ctc = _read_maxfilter_record(fid, tree)["sss_ctc"] - bad_str = "Invalid cross-talk FIF: %s" % fname + bad_str = f"Invalid cross-talk FIF: {fname}" if len(sss_ctc) == 0: raise ValueError(bad_str) node = dir_tree_find(tree, FIFF.FIFFB_DATA_CORRECTION)[0] diff --git a/mne/_fiff/proj.py b/mne/_fiff/proj.py index 0036257d00c..fd5887a4d20 100644 --- a/mne/_fiff/proj.py +++ b/mne/_fiff/proj.py @@ -76,12 +76,12 @@ def __init__( ) def __repr__(self): # noqa: D105 - s = "%s" % self["desc"] - s += ", active : %s" % self["active"] + s = str(self["desc"]) + s += f", active : {self['active']}" s += f", n_channels : {len(self['data']['col_names'])}" if self["explained_var"] is not None: s += f', exp. var : {self["explained_var"] * 100:0.2f}%' - return "" % s + return f"" # speed up info copy by taking advantage of mutability def __deepcopy__(self, memodict): @@ -256,7 +256,7 @@ def add_proj(self, projs, remove_existing=False, verbose=None): if not isinstance(projs, list) and not all( isinstance(p, Projection) for p in projs ): - raise ValueError("Only projs can be added. You supplied " "something else.") + raise ValueError("Only projs can be added. You supplied something else.") # mark proj as inactive, as they have not been applied projs = deactivate_proj(projs, copy=True) @@ -264,7 +264,7 @@ def add_proj(self, projs, remove_existing=False, verbose=None): # we cannot remove the proj if they are active if any(p["active"] for p in self.info["projs"]): raise ValueError( - "Cannot remove projectors that have " "already been applied" + "Cannot remove projectors that have already been applied" ) with self.info._unlock(): self.info["projs"] = projs @@ -338,7 +338,7 @@ def apply_proj(self, verbose=None): ) # let's not raise a RuntimeError here, otherwise interactive plotting if _projector is None: # won't be fun. - logger.info("The projections don't apply to these data." " Doing nothing.") + logger.info("The projections don't apply to these data. Doing nothing.") return self self._projector, self.info = _projector, info if isinstance(self, (BaseRaw, Evoked)): @@ -642,7 +642,7 @@ def _read_proj(fid, node, *, ch_names_mapping=None, verbose=None): if data.shape[1] != len(names): raise ValueError( - "Number of channel names does not match the " "size of data matrix" + "Number of channel names does not match the size of data matrix" ) # just always use this, we used to have bugs with writing the @@ -663,7 +663,7 @@ def _read_proj(fid, node, *, ch_names_mapping=None, verbose=None): projs.append(one) if len(projs) > 0: - logger.info(" Read a total of %d projection items:" % len(projs)) + logger.info(f" Read a total of {len(projs)} projection items:") for proj in projs: misc = "active" if proj["active"] else " idle" logger.info( @@ -728,14 +728,9 @@ def _write_proj(fid, projs, *, ch_names_mapping=None): def _check_projs(projs, copy=True): """Check that projs is a list of Projection.""" - if not isinstance(projs, (list, tuple)): - raise TypeError(f"projs must be a list or tuple, got {type(projs)}") + _validate_type(projs, (list, tuple), "projs") for pi, p in enumerate(projs): - if not isinstance(p, Projection): - raise TypeError( - "All entries in projs list must be Projection " - "instances, but projs[%d] is type %s" % (pi, type(p)) - ) + _validate_type(p, Projection, f"projs[{pi}]") return deepcopy(projs) if copy else projs @@ -804,8 +799,8 @@ def _make_projector(projs, ch_names, bads=(), include_active=True, inplace=False if not p["active"] or include_active: if len(p["data"]["col_names"]) != len(np.unique(p["data"]["col_names"])): raise ValueError( - "Channel name list in projection item %d" - " contains duplicate items" % k + f"Channel name list in projection item {k}" + " contains duplicate items" ) # Get the two selection vectors to pick correct elements from @@ -882,8 +877,8 @@ def _make_projector(projs, ch_names, bads=(), include_active=True, inplace=False proj = np.eye(nchan, nchan) - np.dot(U, U.T) if nproj >= nchan: # e.g., 3 channels and 3 projectors raise RuntimeError( - "Application of %d projectors for %d channels " - "will yield no components." % (nproj, nchan) + f"Application of {nproj} projectors for {nchan} channels " + "will yield no components." ) return proj, nproj, U @@ -957,7 +952,7 @@ def activate_proj(projs, copy=True, verbose=None): for proj in projs: proj["active"] = True - logger.info("%d projection items activated" % len(projs)) + logger.info(f"{len(projs)} projection items activated") return projs @@ -988,7 +983,7 @@ def deactivate_proj(projs, copy=True, verbose=None): for proj in projs: proj["active"] = False - logger.info("%d projection items deactivated" % len(projs)) + logger.info(f"{len(projs)} projection items deactivated") return projs @@ -1164,10 +1159,10 @@ def setup_proj( projector, nproj = make_projector_info(info) if nproj == 0: if verbose: - logger.info("The projection vectors do not apply to these " "channels") + logger.info("The projection vectors do not apply to these channels") projector = None else: - logger.info("Created an SSP operator (subspace dimension = %d)" % nproj) + logger.info(f"Created an SSP operator (subspace dimension = {nproj})") # The projection items have been activated if activate: diff --git a/mne/_fiff/reference.py b/mne/_fiff/reference.py index 996a034e8a2..d0f4b08f76b 100644 --- a/mne/_fiff/reference.py +++ b/mne/_fiff/reference.py @@ -77,7 +77,7 @@ def _check_before_reference(inst, ref_from, ref_to, ch_type): proj["desc"] == "Average EEG reference" or proj["kind"] == FIFF.FIFFV_PROJ_ITEM_EEG_AVREF ): - logger.info("Removing existing average EEG reference " "projection.") + logger.info("Removing existing average EEG reference projection.") # Don't remove the projection right away, but do this at the end of # this loop. projs_to_remove.append(i) @@ -196,7 +196,7 @@ def add_reference_channels(inst, ref_channels, copy=True): ref_channels = [ref_channels] for ch in ref_channels: if ch in inst.info["ch_names"]: - raise ValueError("Channel %s already specified in inst." % ch) + raise ValueError(f"Channel {ch} already specified in inst.") # Once CAR is applied (active), don't allow adding channels if _has_eeg_average_ref_proj(inst.info, check_active=True): @@ -219,7 +219,7 @@ def add_reference_channels(inst, ref_channels, copy=True): inst._data = data else: raise TypeError( - "inst should be Raw, Epochs, or Evoked instead of %s." % type(inst) + f"inst should be Raw, Epochs, or Evoked instead of {type(inst)}." ) nchan = len(inst.info["ch_names"]) @@ -453,15 +453,13 @@ def _get_ch_type(inst, ch_type): if type_ in inst: ch_type = [type_] logger.info( - "%s channel type selected for " - "re-referencing" % DEFAULTS["titles"][type_] + f"{DEFAULTS['titles'][type_]} channel type selected for " + "re-referencing" ) break # if auto comes up empty, or the user specifies a bad ch_type. else: - raise ValueError( - "No EEG, ECoG, sEEG or DBS channels found " "to rereference." - ) + raise ValueError("No EEG, ECoG, sEEG or DBS channels found to rereference.") return ch_type @@ -554,8 +552,8 @@ def set_bipolar_reference( if len(anode) != len(cathode): raise ValueError( - "Number of anodes (got %d) must equal the number " - "of cathodes (got %d)." % (len(anode), len(cathode)) + f"Number of anodes (got {len(anode)}) must equal the number " + f"of cathodes (got {len(cathode)})." ) if ch_name is None: @@ -565,7 +563,7 @@ def set_bipolar_reference( if len(ch_name) != len(anode): raise ValueError( "Number of channel names must equal the number of " - "anodes/cathodes (got %d)." % len(ch_name) + f"anodes/cathodes (got {len(ch_name)})." ) # Check for duplicate channel names (it is allowed to give the name of the @@ -573,9 +571,9 @@ def set_bipolar_reference( for ch, a, c in zip(ch_name, anode, cathode): if ch not in [a, c] and ch in inst.ch_names: raise ValueError( - 'There is already a channel named "%s", please ' + f'There is already a channel named "{ch}", please ' "specify a different name for the bipolar " - "channel using the ch_name parameter." % ch + "channel using the ch_name parameter." ) if ch_info is None: diff --git a/mne/_fiff/tests/test_compensator.py b/mne/_fiff/tests/test_compensator.py index 350fb212032..d743c7ad7f2 100644 --- a/mne/_fiff/tests/test_compensator.py +++ b/mne/_fiff/tests/test_compensator.py @@ -88,7 +88,7 @@ def make_evoked(fname, comp): def compensate_mne(fname, comp): """Compensate using MNE-C.""" - tmp_fname = "%s-%d-ave.fif" % (fname.stem, comp) + tmp_fname = f"{fname.stem}-{comp}-ave.fif" cmd = [ "mne_compensate_data", "--in", diff --git a/mne/_fiff/tests/test_constants.py b/mne/_fiff/tests/test_constants.py index 45a9899423d..55549b53974 100644 --- a/mne/_fiff/tests/test_constants.py +++ b/mne/_fiff/tests/test_constants.py @@ -283,7 +283,7 @@ def test_constants(tmp_path): # # Version - mne_version = "%d.%d" % (FIFF.FIFFC_MAJOR_VERSION, FIFF.FIFFC_MINOR_VERSION) + mne_version = f"{FIFF.FIFFC_MAJOR_VERSION:d}.{FIFF.FIFFC_MINOR_VERSION:d}" assert fiff_version == mne_version unknowns = list() @@ -359,8 +359,8 @@ def test_constants(tmp_path): assert _aliases.get(name) == con[check][val], msg else: con[check][val] = name - unknowns = "\n\t".join("{} ({})".format(*u) for u in unknowns) - assert len(unknowns) == 0, "Unknown types\n\t%s" % unknowns + unknowns = "\n\t".join(f"{u[0]} ({u[1]})" for u in unknowns) + assert len(unknowns) == 0, f"Unknown types\n\t{unknowns}" # Assert that all the FIF defs are in our constants assert set(fif.keys()) == set(con.keys()) @@ -384,14 +384,14 @@ def test_constants(tmp_path): bad_list = [] for key in fif["coil"]: if key not in _missing_coil_def and key not in coil_def: - bad_list.append((" %s," % key).ljust(10) + " # " + fif["coil"][key][1]) + bad_list.append((f" {key},").ljust(10) + " # " + fif["coil"][key][1]) assert len(bad_list) == 0, ( "\nIn fiff-constants, missing from coil_def:\n" + "\n".join(bad_list) ) # Assert that enum(coil) has all `coil_def.dat` entries for key, desc in zip(coil_def, coil_desc): if key not in fif["coil"]: - bad_list.append((" %s," % key).ljust(10) + " # " + desc) + bad_list.append((f" {key},").ljust(10) + " # " + desc) assert len(bad_list) == 0, ( "In coil_def, missing from fiff-constants:\n" + "\n".join(bad_list) ) diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index 8552585eec4..fb9488ce1a7 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -543,9 +543,9 @@ def test_check_consistency(): idx = 0 ch = info["chs"][idx] for key, bad, match in ( - ("ch_name", 1.0, "not a string"), + ("ch_name", 1.0, "must be an instance"), ("loc", np.zeros(15), "12 elements"), - ("cal", np.ones(1), "float or int"), + ("cal", np.ones(1), "numeric"), ): info._check_consistency() # okay old = ch[key] diff --git a/mne/_fiff/tests/test_reference.py b/mne/_fiff/tests/test_reference.py index 166b06e460a..0f48f354985 100644 --- a/mne/_fiff/tests/test_reference.py +++ b/mne/_fiff/tests/test_reference.py @@ -302,7 +302,7 @@ def test_set_eeg_reference_ch_type(ch_type, msg, projection): # gh-8739 raw2 = RawArray(data, create_info(5, 1000.0, ["mag"] * 4 + ["misc"])) with pytest.raises( - ValueError, match="No EEG, ECoG, sEEG or DBS channels " "found to rereference." + ValueError, match="No EEG, ECoG, sEEG or DBS channels found to rereference." ): set_eeg_reference(raw2, ch_type="auto", projection=projection) diff --git a/mne/_fiff/tree.py b/mne/_fiff/tree.py index 556dab1a537..dcb99e6fe74 100644 --- a/mne/_fiff/tree.py +++ b/mne/_fiff/tree.py @@ -50,7 +50,7 @@ def make_dir_tree(fid, directory, start=0, indent=0, verbose=None): else: block = 0 - logger.debug(" " * indent + "start { %d" % block) + logger.debug(" " * indent + f"start {{ {block}") this = start @@ -100,9 +100,8 @@ def make_dir_tree(fid, directory, start=0, indent=0, verbose=None): logger.debug( " " * (indent + 1) - + "block = %d nent = %d nchild = %d" - % (tree["block"], tree["nent"], tree["nchild"]) + + f"block = {tree['block']} nent = {tree['nent']} nchild = {tree['nchild']}" ) - logger.debug(" " * indent + "end } %d" % block) + logger.debug(" " * indent + f"end }} {block:d}") last = this return tree, last diff --git a/mne/_fiff/write.py b/mne/_fiff/write.py index 3e6621d0069..e68ffcff0b1 100644 --- a/mne/_fiff/write.py +++ b/mne/_fiff/write.py @@ -289,7 +289,7 @@ def start_file(fname, id_=None): ID to use for the FIFF_FILE_ID. """ if _file_like(fname): - logger.debug("Writing using %s I/O" % type(fname)) + logger.debug(f"Writing using {type(fname)} I/O") fid = fname fid.seek(0) else: diff --git a/mne/_freesurfer.py b/mne/_freesurfer.py index d0aef0b5225..52d7c24afeb 100644 --- a/mne/_freesurfer.py +++ b/mne/_freesurfer.py @@ -611,7 +611,7 @@ def read_talxfm(subject, subjects_dir=None, verbose=None): if not path.is_file(): path = subjects_dir / subject / "mri" / "T1.mgz" if not path.is_file(): - raise OSError("mri not found: %s" % path) + raise OSError(f"mri not found: {path}") _, _, mri_ras_t, _, _ = _read_mri_info(path) mri_mni_t = combine_transforms(mri_ras_t, ras_mni_t, "mri", "mni_tal") return mri_mni_t diff --git a/mne/_ola.py b/mne/_ola.py index a7da98905b9..eb289273760 100644 --- a/mne/_ola.py +++ b/mne/_ola.py @@ -103,7 +103,7 @@ def feed_generator(self, n_pts): # Left zero-order hold condition if self._position < self.control_points[self._left_idx]: n_use = min(self.control_points[self._left_idx] - self._position, n_pts) - logger.debug(" Left ZOH %s" % n_use) + logger.debug(f" Left ZOH {n_use}") this_sl = slice(None, n_use) assert used[this_sl].size == n_use assert not used[this_sl].any() @@ -170,7 +170,7 @@ def feed_generator(self, n_pts): if self.control_points[self._left_idx] <= self._position: n_use = stop - self._position if n_use > 0: - logger.debug(" Right ZOH %s" % n_use) + logger.debug(f" Right ZOH {n_use}") this_sl = slice(n_pts - n_use, None) assert not used[this_sl].any() used[this_sl] = True @@ -293,8 +293,8 @@ def __init__( del n_samples, n_overlap if n_total < self._n_samples: raise ValueError( - "Number of samples per window (%d) must be at " - "most the total number of samples (%s)" % (self._n_samples, n_total) + f"Number of samples per window ({self._n_samples}) must be at " + f"most the total number of samples ({n_total})" ) if not callable(process): raise TypeError(f"process must be callable, got type {type(process)}") @@ -348,16 +348,12 @@ def feed(self, *datas, verbose=None, **kwargs): self._in_buffers = [None] * len(datas) if len(datas) != len(self._in_buffers): raise ValueError( - "Got %d array(s), needed %d" % (len(datas), len(self._in_buffers)) + f"Got {len(datas)} array(s), needed {len(self._in_buffers)}" ) for di, data in enumerate(datas): if not isinstance(data, np.ndarray) or data.ndim < 1: raise TypeError( - "data entry %d must be an 2D ndarray, got %s" - % ( - di, - type(data), - ) + f"data entry {di} must be an 2D ndarray, got {type(data)}" ) if self._in_buffers[di] is None: # In practice, users can give large chunks, so we use @@ -375,8 +371,8 @@ def feed(self, *datas, verbose=None, **kwargs): f"{data.dtype} shape[:-1]={data.shape[:-1]}" ) logger.debug( - " + Appending %d->%d" - % (self._in_offset, self._in_offset + data.shape[-1]) + f" + Appending {self._in_offset:d}->" + f"{self._in_offset + data.shape[-1]:d}" ) self._in_buffers[di] = np.concatenate([self._in_buffers[di], data], -1) if self._in_offset > self.stops[-1]: @@ -422,7 +418,7 @@ def feed(self, *datas, verbose=None, **kwargs): delta = next_start - self.starts[self._idx - 1] for di in range(len(self._in_buffers)): self._in_buffers[di] = self._in_buffers[di][..., delta:] - logger.debug(" - Shifting input/output buffers by %d samples" % (delta,)) + logger.debug(f" - Shifting input/output buffers by {delta:d} samples") self._store(*[o[..., :delta] for o in self._out_buffers]) for ob in self._out_buffers: ob[..., :-delta] = ob[..., delta:] @@ -441,9 +437,9 @@ def _check_cola(win, nperseg, step, window_name, tol=1e-10): deviation = np.max(np.abs(binsums - const)) if deviation > tol: raise ValueError( - "segment length %d with step %d for %s window " + f"segment length {nperseg:d} with step {step:d} for {window_name} window " "type does not provide a constant output " - "(%g%% deviation)" % (nperseg, step, window_name, 100 * deviation / const) + f"({100 * deviation / const:g}% deviation)" ) return const diff --git a/mne/annotations.py b/mne/annotations.py index 1c66fee1be5..2e3d01af628 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -673,15 +673,12 @@ def crop( if emit_warning: omitted = np.array(out_of_bounds).sum() if omitted > 0: - warn( - "Omitted %s annotation(s) that were outside data" - " range." % omitted - ) + warn(f"Omitted {omitted} annotation(s) that were outside data range.") limited = (np.array(clip_left_elem) | np.array(clip_right_elem)).sum() if limited > 0: warn( - "Limited %s annotation(s) that were expanding outside the" - " data range." % limited + f"Limited {limited} annotation(s) that were expanding outside the" + " data range." ) return self @@ -1238,10 +1235,10 @@ def read_annotations( elif name.startswith("events_") and fname.endswith("mat"): annotations = _read_brainstorm_annotations(fname) else: - raise OSError('Unknown annotation file format "%s"' % fname) + raise OSError(f'Unknown annotation file format "{fname}"') if annotations is None: - raise OSError('No annotation data found in file "%s"' % fname) + raise OSError(f'No annotation data found in file "{fname}"') return annotations diff --git a/mne/baseline.py b/mne/baseline.py index 36ab0fc514f..37e3d8df72a 100644 --- a/mne/baseline.py +++ b/mne/baseline.py @@ -18,7 +18,7 @@ def _log_rescale(baseline, mode="mean"): mode, ["logratio", "ratio", "zscore", "mean", "percent", "zlogratio"], ) - msg = "Applying baseline correction (mode: %s)" % mode + msg = f"Applying baseline correction (mode: {mode})" else: msg = "No baseline correction applied" return msg diff --git a/mne/beamformer/_compute_beamformer.py b/mne/beamformer/_compute_beamformer.py index 16cbc18e6d7..a35285328e6 100644 --- a/mne/beamformer/_compute_beamformer.py +++ b/mne/beamformer/_compute_beamformer.py @@ -265,9 +265,7 @@ def _compute_beamformer( n_sources = G.shape[1] // n_orient assert nn.shape == (n_sources, 3) - logger.info( - "Computing beamformer filters for %d source%s" % (n_sources, _pl(n_sources)) - ) + logger.info(f"Computing beamformer filters for {n_sources} source{_pl(n_sources)}") n_channels = G.shape[0] assert n_orient in (3, 1) Gk = np.reshape(G.T, (n_sources, n_orient, n_channels)).transpose(0, 2, 1) diff --git a/mne/beamformer/_dics.py b/mne/beamformer/_dics.py index 8d8468cfa77..0a5e1b07a35 100644 --- a/mne/beamformer/_dics.py +++ b/mne/beamformer/_dics.py @@ -346,7 +346,7 @@ def _apply_dics(data, filters, info, tmin, tfr=False): for i, M in enumerate(data): if not one_epoch: - logger.info("Processing epoch : %d" % (i + 1)) + logger.info(f"Processing epoch : {i + 1}") # Apply SSPs if not tfr: # save computation, only compute once diff --git a/mne/beamformer/_lcmv.py b/mne/beamformer/_lcmv.py index b8639791846..c07b1dd22c3 100644 --- a/mne/beamformer/_lcmv.py +++ b/mne/beamformer/_lcmv.py @@ -286,7 +286,7 @@ def _apply_lcmv(data, filters, info, tmin): raise ValueError("data and picks must have the same length") if not return_single: - logger.info("Processing epoch : %d" % (i + 1)) + logger.info(f"Processing epoch : {i + 1}") M = _proj_whiten_data(M, info["projs"], filters) diff --git a/mne/beamformer/resolution_matrix.py b/mne/beamformer/resolution_matrix.py index ce55a09584b..63876604c24 100644 --- a/mne/beamformer/resolution_matrix.py +++ b/mne/beamformer/resolution_matrix.py @@ -53,9 +53,7 @@ def make_lcmv_resolution_matrix(filters, forward, info): # compute resolution matrix resmat = filtmat.dot(leadfield) - shape = resmat.shape - - logger.info("Dimensions of LCMV resolution matrix: %d by %d." % shape) + logger.info(f"Dimensions of LCMV resolution matrix: {resmat.shape}.") return resmat diff --git a/mne/beamformer/tests/test_dics.py b/mne/beamformer/tests/test_dics.py index bcde4503307..0a3fc128136 100644 --- a/mne/beamformer/tests/test_dics.py +++ b/mne/beamformer/tests/test_dics.py @@ -600,14 +600,12 @@ def test_real(_load_forward, idx): # check whether a filters object without src_type throws expected warning del filters_vol["src_type"] # emulate 0.16 behaviour to cause warning - with pytest.warns( - RuntimeWarning, match="spatial filter does not contain " "src_type" - ): + with pytest.warns(RuntimeWarning, match="spatial filter does not contain src_type"): apply_dics_csd(csd, filters_vol) @pytest.mark.filterwarnings( - "ignore:The use of several sensor types with the" ":RuntimeWarning" + "ignore:The use of several sensor types with the:RuntimeWarning" ) @idx_param def test_apply_dics_timeseries(_load_forward, idx): diff --git a/mne/beamformer/tests/test_lcmv.py b/mne/beamformer/tests/test_lcmv.py index 509afbcf79e..4a2df6d1938 100644 --- a/mne/beamformer/tests/test_lcmv.py +++ b/mne/beamformer/tests/test_lcmv.py @@ -380,7 +380,7 @@ def test_make_lcmv_bem(tmp_path, reg, proj, kind): assert "unknown subject" not in repr(filters) assert f'{fwd["nsource"]} vert' in repr(filters) assert "20 ch" in repr(filters) - assert "rank %s" % rank in repr(filters) + assert f"rank {rank}" in repr(filters) # I/O fname = tmp_path / "filters.h5" @@ -500,9 +500,7 @@ def test_make_lcmv_bem(tmp_path, reg, proj, kind): # check whether a filters object without src_type throws expected warning del filters["src_type"] # emulate 0.16 behaviour to cause warning - with pytest.warns( - RuntimeWarning, match="spatial filter does not contain " "src_type" - ): + with pytest.warns(RuntimeWarning, match="spatial filter does not contain src_type"): apply_lcmv(evoked, filters) # Now test single trial using fixed orientation forward solution @@ -852,7 +850,7 @@ def test_localization_bias_fixed( # Changes here should be synced with test_dics.py @pytest.mark.parametrize( - "reg, pick_ori, weight_norm, use_cov, depth, lower, upper, " "lower_ori, upper_ori", + "reg, pick_ori, weight_norm, use_cov, depth, lower, upper, lower_ori, upper_ori", [ ( 0.05, diff --git a/mne/bem.py b/mne/bem.py index 88104ea9cc2..9297cc773b2 100644 --- a/mne/bem.py +++ b/mne/bem.py @@ -98,13 +98,11 @@ def __repr__(self): # noqa: D105 center = ", ".join("%0.1f" % (x * 1000.0) for x in self["r0"]) rad = self.radius if rad is None: # no radius / MEG only - extra = "Sphere (no layers): r0=[%s] mm" % center + extra = f"Sphere (no layers): r0=[{center}] mm" else: - extra = "Sphere ({} layer{}): r0=[{}] R={:1.0f} mm".format( - len(self["layers"]) - 1, - _pl(self["layers"]), - center, - rad * 1000.0, + extra = ( + f"Sphere ({len(self['layers']) - 1} layer{_pl(self['layers'])}): " + f"r0=[{center}] R={rad * 1000.0:1.0f} mm" ) else: extra = f"BEM ({len(self['surfs'])} layer{_pl(self['surfs'])})" @@ -225,13 +223,8 @@ def _fwd_bem_lin_pot_coeff(surfs): rr_ord = np.arange(nps[si_1]) for si_2, surf2 in enumerate(surfs): logger.info( - " %s (%d) -> %s (%d) ..." - % ( - _bem_surf_name[surf1["id"]], - nps[si_1], - _bem_surf_name[surf2["id"]], - nps[si_2], - ) + f" {_bem_surf_name[surf1['id']]} ({nps[si_1]:d}) -> " + f"{_bem_surf_name[surf2['id']]} ({nps[si_2]}) ..." ) tri_rr = surf2["rr"][surf2["tris"]] tri_nn = surf2["tri_nn"] @@ -325,10 +318,9 @@ def _check_complete_surface(surf, copy=False, incomplete="raise", extra=""): fewer = (fewer[:80] + ["..."]) if len(fewer) > 80 else fewer fewer = ", ".join(str(f) for f in fewer) msg = ( - "Surface {} has topological defects: {:.0f} / {:.0f} vertices " - "have fewer than three neighboring triangles [{}]{}".format( - _bem_surf_name[surf["id"]], len(fewer), len(surf["rr"]), fewer, extra - ) + f"Surface {_bem_surf_name[surf['id']]} has topological defects: " + f"{len(fewer)} / {len(surf['rr'])} vertices have fewer than three " + f"neighboring triangles [{fewer}]{extra}" ) _on_missing(on_missing=incomplete, msg=msg, name="on_defects") return surf @@ -353,7 +345,7 @@ def _fwd_bem_linear_collocation_solution(bem): logger.info(" Inverting the coefficient matrix (homog)...") ip_solution = _fwd_bem_homog_solution(coeff, [bem["surfs"][-1]["np"]]) logger.info( - " Modify the original solution to incorporate " "IP approach..." + " Modify the original solution to incorporate IP approach..." ) _fwd_bem_ip_modify_solution(bem["solution"], ip_solution, ip_mult, nps) bem["bem_method"] = FIFF.FIFFV_BEM_APPROX_LINEAR @@ -469,11 +461,10 @@ def _ico_downsample(surf, dest_grade): """Downsample the surface if isomorphic to a subdivided icosahedron.""" n_tri = len(surf["tris"]) bad_msg = ( - "Cannot decimate to requested ico grade %d. The provided " - "BEM surface has %d triangles, which cannot be isomorphic with " - "a subdivided icosahedron. Consider manually decimating the " - "surface to a suitable density and then use ico=None in " - "make_bem_model." % (dest_grade, n_tri) + f"Cannot decimate to requested ico grade {dest_grade}. The provided " + f"BEM surface has {n_tri} triangles, which cannot be isomorphic with " + "a subdivided icosahedron. Consider manually decimating the surface to " + "a suitable density and then use ico=None in make_bem_model." ) if n_tri % 20 != 0: raise RuntimeError(bad_msg) @@ -485,8 +476,8 @@ def _ico_downsample(surf, dest_grade): if dest_grade > found: raise RuntimeError( - "For this surface, decimation grade should be %d " - "or less, not %s." % (found, dest_grade) + f"For this surface, decimation grade should be {found} or less, " + f"not {dest_grade}." ) source = _get_ico_surface(found) @@ -501,8 +492,8 @@ def _ico_downsample(surf, dest_grade): "triangles but ordering is wrong" ) logger.info( - "Going from %dth to %dth subdivision of an icosahedron " - "(n_tri: %d -> %d)" % (found, dest_grade, len(surf["tris"]), len(dest["tris"])) + f"Going from {found}th to {dest_grade}th subdivision of an icosahedron " + f"(n_tri: {len(surf['tris'])} -> {len(dest['tris'])})" ) # Find the mapping dest["rr"] = surf["rr"][_get_ico_map(source, dest)] @@ -514,7 +505,7 @@ def _get_ico_map(fro, to): nearest, dists = _compute_nearest(fro["rr"], to["rr"], return_dists=True) n_bads = (dists > 5e-3).sum() if n_bads > 0: - raise RuntimeError("No matching vertex for %d destination vertices" % (n_bads)) + raise RuntimeError(f"No matching vertex for {n_bads} destination vertices") return nearest @@ -530,7 +521,7 @@ def _order_surfaces(surfs): ] ids = np.array([surf["id"] for surf in surfs]) if set(ids) != set(surf_order): - raise RuntimeError("bad surface ids: %s" % ids) + raise RuntimeError(f"bad surface ids: {ids}") order = [np.where(ids == id_)[0][0] for id_ in surf_order] surfs = [surfs[idx] for idx in order] return surfs @@ -542,9 +533,10 @@ def _assert_complete_surface(surf, incomplete="raise"): # Center of mass.... cm = surf["rr"].mean(axis=0) logger.info( - "{} CM is {:6.2f} {:6.2f} {:6.2f} mm".format( - _bem_surf_name[surf["id"]], 1000 * cm[0], 1000 * cm[1], 1000 * cm[2] - ) + f"{_bem_surf_name[surf['id']]} CM is " + f"{1000 * cm[0]:6.2f} " + f"{1000 * cm[1]:6.2f} " + f"{1000 * cm[2]:6.2f} mm" ) tot_angle = _get_solids(surf["rr"][surf["tris"]], cm[np.newaxis, :])[0] prop = tot_angle / (2 * np.pi) @@ -955,15 +947,8 @@ def make_sphere_model( rv = _fwd_eeg_fit_berg_scherg(sphere, 200, 3) logger.info("\nEquiv. model fitting -> RV = %g %%" % (100 * rv)) for k in range(3): - logger.info( - "mu%d = %g lambda%d = %g" - % ( - k + 1, - sphere["mu"][k], - k + 1, - sphere["layers"][-1]["sigma"] * sphere["lambda"][k], - ) - ) + s_k = sphere["layers"][-1]["sigma"] * sphere["lambda"][k] + logger.info(f"mu{k + 1} = {sphere['mu'][k]:g} lambda{k + 1} = {s_k:g}") logger.info( f"Set up EEG sphere model with scalp radius {1000 * head_radius:7.1f} mm\n" ) @@ -1059,8 +1044,7 @@ def get_fitting_dig(info, dig_kinds="auto", exclude_frontal=True, verbose=None): dig_kinds[di] = _dig_kind_dict.get(d, d) if dig_kinds[di] not in _dig_kind_ints: raise ValueError( - "dig_kinds[#%d] (%s) must be one of %s" - % (di, d, sorted(list(_dig_kind_dict.keys()))) + f"dig_kinds[{di}] ({d}) must be one of {sorted(_dig_kind_dict)}" ) # get head digization points of the specified kind(s) @@ -1081,7 +1065,7 @@ def get_fitting_dig(info, dig_kinds="auto", exclude_frontal=True, verbose=None): hsp = np.array(hsp) if len(hsp) <= 10: - kinds_str = ", ".join(['"%s"' % _dig_kind_rev[d] for d in sorted(dig_kinds)]) + kinds_str = ", ".join([f'"{_dig_kind_rev[d]}"' for d in sorted(dig_kinds)]) msg = ( f"Only {len(hsp)} head digitization points of the specified " f"kind{_pl(dig_kinds)} ({kinds_str},)" @@ -1108,18 +1092,20 @@ def _fit_sphere_to_headshape(info, dig_kinds, verbose=None): _check_head_radius(radius) # > 2 cm away from head center in X or Y is strange + o_mm = origin_head * 1e3 + o_d = origin_device * 1e3 if np.linalg.norm(origin_head[:2]) > 0.02: warn( - "(X, Y) fit ({:0.1f}, {:0.1f}) more than 20 mm from head frame " - "origin".format(*tuple(1e3 * origin_head[:2])) + f"(X, Y) fit ({o_mm[0]:0.1f}, {o_mm[1]:0.1f}) " + "more than 20 mm from head frame origin" ) logger.info( "Origin head coordinates:".ljust(30) - + "{:0.1f} {:0.1f} {:0.1f} mm".format(*tuple(1e3 * origin_head)) + + f"{o_mm[0]:0.1f} {o_mm[1]:0.1f} {o_mm[2]:0.1f} mm" ) logger.info( "Origin device coordinates:".ljust(30) - + "{:0.1f} {:0.1f} {:0.1f} mm".format(*tuple(1e3 * origin_device)) + + f"{o_d[0]:0.1f} {o_d[1]:0.1f} {o_d[2]:0.1f} mm" ) return radius, origin_head, origin_device @@ -1278,8 +1264,8 @@ def make_watershed_bem( if op.isdir(ws_dir): if not overwrite: raise RuntimeError( - "%s already exists. Use the --overwrite option" - " to recreate it." % ws_dir + f"{ws_dir} already exists. Use the --overwrite option" + " to recreate it." ) else: shutil.rmtree(ws_dir) @@ -1287,7 +1273,7 @@ def make_watershed_bem( # put together the command cmd = ["mri_watershed"] if preflood: - cmd += ["-h", "%s" % int(preflood)] + cmd += ["-h", f"{int(preflood)}"] if T1 is None: T1 = gcaatlas @@ -1404,7 +1390,7 @@ def _extract_volume_info(mgz): version = header["version"] vol_info = dict() if version == 1: - version = "%s # volume info valid" % version + version = f"{version} # volume info valid" vol_info["valid"] = version vol_info["filename"] = mgz vol_info["volume"] = header["dims"][:3] @@ -1458,7 +1444,7 @@ def read_bem_surfaces( else: surf = _read_bem_surfaces_fif(fname, s_id) if s_id is not None and len(surf) != 1: - raise ValueError("surface with id %d not found" % s_id) + raise ValueError(f"surface with id {s_id} not found") for this in surf: if patch_stats or this["nn"] is None: _check_complete_surface(this, incomplete=on_defects) @@ -1494,7 +1480,7 @@ def _read_bem_surfaces_fif(fname, s_id): if bemsurf is None: raise ValueError("BEM surface data not found") - logger.info(" %d BEM surfaces found" % len(bemsurf)) + logger.info(f" {len(bemsurf)} BEM surfaces found") # Coordinate frame possibly at the top level tag = find_tag(fid, bem, FIFF.FIFF_BEM_COORD_FRAME) if tag is not None: @@ -1512,7 +1498,7 @@ def _read_bem_surfaces_fif(fname, s_id): this = _read_bem_surface(fid, bsurf, coord_frame) surf.append(this) logger.info("[done]") - logger.info(" %d BEM surfaces read" % len(surf)) + logger.info(f" {len(surf)} BEM surfaces read") return surf @@ -1667,12 +1653,12 @@ def read_bem_solution(fname, *, verbose=None): if len(dims) != 2 and solver != "openmeeg": raise RuntimeError( "Expected a two-dimensional solution matrix " - "instead of a %d dimensional one" % dims[0] + f"instead of a {dims[0]} dimensional one" ) if dims[0] != dim or dims[1] != dim: raise RuntimeError( - "Expected a %d x %d solution matrix instead of " - "a %d x %d one" % (dim, dim, dims[1], dims[0]) + f"Expected a {dim} x {dim} solution matrix instead of " + f"a {dims[1]} x {dims[0]} one" ) bem["nsol"] = bem["solution"].shape[0] # Gamma factors and multipliers @@ -1694,7 +1680,7 @@ def _read_bem_solution_fif(fname): # Find the BEM data nodes = dir_tree_find(tree, FIFF.FIFFB_BEM) if len(nodes) == 0: - raise RuntimeError("No BEM data in %s" % fname) + raise RuntimeError(f"No BEM data in {fname}") bem_node = nodes[0] # Approximation method @@ -1704,7 +1690,7 @@ def _read_bem_solution_fif(fname): solver = tag["solver"] tag = find_tag(f, bem_node, FIFF.FIFF_BEM_APPROX) if tag is None: - raise RuntimeError("No BEM solution found in %s" % fname) + raise RuntimeError(f"No BEM solution found in {fname}") method = tag.data[0] tag = find_tag(fid, bem_node, FIFF.FIFF_BEM_POT_SOLUTION) sol = tag.data @@ -2024,7 +2010,7 @@ def convert_flash_mris( template = op.join(flash_dir, "mef*_*.mgz") files = sorted(glob.glob(template)) if len(files) == 0: - raise ValueError("No suitable source files found (%s)" % template) + raise ValueError(f"No suitable source files found ({template})") if unwarp: logger.info("\n---- Unwarp mgz data sets ----") for infile in files: @@ -2068,7 +2054,7 @@ def convert_flash_mris( template = "mef05_*u.mgz" if unwarp else "mef05_*.mgz" files = sorted(flash_dir.glob(template)) if len(files) == 0: - raise ValueError("No suitable source files found (%s)" % template) + raise ValueError(f"No suitable source files found ({template})") cmd = ["mri_average", "-noconform"] + files + [pm_dir / "flash5.mgz"] run_subprocess_env(cmd) (pm_dir / "flash5_reg.mgz").unlink(missing_ok=True) @@ -2275,8 +2261,8 @@ def make_flash_bem( dest = bem_dir logger.info( "\nThank you for waiting.\nThe BEM triangulations for this " - "subject are now available at:\n%s.\nWe hope the BEM meshes " - "created will facilitate your MEG and EEG data analyses." % dest + f"subject are now available at:\n{dest}.\nWe hope the BEM meshes " + "created will facilitate your MEG and EEG data analyses." ) # Show computed BEM surfaces if show: @@ -2293,9 +2279,8 @@ def _check_bem_size(surfs): """Check bem surface sizes.""" if len(surfs) > 1 and surfs[0]["np"] > 10000: warn( - "The bem surfaces have %s data points. 5120 (ico grade=4) " + f"The bem surfaces have {surfs[0]['np']} data points. 5120 (ico grade=4) " "should be enough. Dense 3-layer bems may not save properly." - % surfs[0]["np"] ) @@ -2307,9 +2292,9 @@ def _symlink(src, dest, copy=False): os.symlink(src_link, dest) except OSError: warn( - "Could not create symbolic link %s. Check that your " + f"Could not create symbolic link {dest}. Check that your " "partition handles symbolic links. The file will be copied " - "instead." % dest + "instead." ) copy = True if copy: @@ -2325,7 +2310,7 @@ def _ensure_bem_surfaces(bem, extra_allow=(), name="bem"): _validate_type(bem, allowed, name) if isinstance(bem, path_like): # Load the surfaces - logger.info(f"Loading BEM surfaces from {str(bem)}...") + logger.info(f"Loading BEM surfaces from {bem}...") bem = read_bem_surfaces(bem) bem = ConductorModel(is_sphere=False, surfs=bem) elif isinstance(bem, list): @@ -2404,8 +2389,7 @@ def make_scalp_surfaces( subj_path = subjects_dir / subject if not subj_path.exists(): raise RuntimeError( - "%s does not exist. Please check your subject " - "directory path." % subj_path + f"{subj_path} does not exist. Please check your subject directory path." ) # Backward compat for old FreeSurfer (?) @@ -2459,9 +2443,9 @@ def check_seghead(surf_path=subj_path / "surf"): bem_dir = subjects_dir / subject / "bem" if not bem_dir.is_dir(): os.mkdir(bem_dir) - fname_template = bem_dir / ("%s-head-{}.fif" % subject) + fname_template = bem_dir / (f"{subject}-head-{{}}.fif") dense_fname = str(fname_template).format("dense") - logger.info("2. Creating %s ..." % dense_fname) + logger.info(f"2. Creating {dense_fname} ...") _check_file(dense_fname, overwrite) # Helpful message if we get a topology error msg = ( diff --git a/mne/channels/_dig_montage_utils.py b/mne/channels/_dig_montage_utils.py index 2136934972d..81cf2b0a542 100644 --- a/mne/channels/_dig_montage_utils.py +++ b/mne/channels/_dig_montage_utils.py @@ -24,7 +24,7 @@ def _read_dig_montage_egi( ): if not _all_data_kwargs_are_none: raise ValueError( - "hsp, hpi, elp, point_names, fif must all be " "None if egi is not None" + "hsp, hpi, elp, point_names, fif must all be None if egi is not None" ) _check_fname(fname, overwrite="read", must_exist=True) defusedxml = _soft_import("defusedxml", "reading EGI montages") @@ -59,8 +59,8 @@ def _read_dig_montage_egi( # Unknown else: warn( - "Unknown sensor type %s detected. Skipping sensor..." - "Proceed with caution!" % kind + f"Unknown sensor type {kind} detected. Skipping sensor..." + "Proceed with caution!" ) return Bunch( diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py index 4df6c685912..2efbf06bc6b 100644 --- a/mne/channels/_standard_montage_utils.py +++ b/mne/channels/_standard_montage_utils.py @@ -255,7 +255,7 @@ def _read_elc(fname, head_size): scale = dict(m=1.0, mm=1e-3)[units] break else: - raise RuntimeError("Could not detect units in file %s" % fname) + raise RuntimeError(f"Could not detect units in file {fname}") for line in fid: if "Positions\n" in line: break diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 6ad43f32ee5..54ad772ba18 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -168,7 +168,7 @@ def equalize_channels(instances, copy=True, verbose=None): Info, ) allowed_types_str = ( - "Raw, Epochs, Evoked, TFR, Forward, Covariance, " "CrossSpectralDensity or Info" + "Raw, Epochs, Evoked, TFR, Forward, Covariance, CrossSpectralDensity or Info" ) for inst in instances: _validate_type( @@ -207,7 +207,7 @@ def equalize_channels(instances, copy=True, verbose=None): equalized_instances.append(inst) if dropped: - logger.info("Dropped the following channels:\n%s" % dropped) + logger.info(f"Dropped the following channels:\n{dropped}") elif reordered: logger.info("Channels have been re-ordered.") @@ -1418,12 +1418,12 @@ def _ch_neighbor_adjacency(ch_names, neighbors): The adjacency matrix. """ if len(ch_names) != len(neighbors): - raise ValueError("`ch_names` and `neighbors` must " "have the same length") + raise ValueError("`ch_names` and `neighbors` must have the same length") set_neighbors = {c for d in neighbors for c in d} rest = set_neighbors - set(ch_names) if len(rest) > 0: raise ValueError( - "Some of your neighbors are not present in the " "list of channel names" + "Some of your neighbors are not present in the list of channel names" ) for neigh in neighbors: @@ -1494,7 +1494,7 @@ def find_ch_adjacency(info, ch_type): picks = channel_indices_by_type(info) if sum([len(p) != 0 for p in picks.values()]) != 1: raise ValueError( - "info must contain only one channel type if " "ch_type is None." + "info must contain only one channel type if ch_type is None." ) ch_type = channel_type(info, 0) else: @@ -2145,7 +2145,7 @@ def read_vectorview_selection(name, fname=None, info=None, verbose=None): # make sure we found at least one match for each name for n, found in name_found.items(): if not found: - raise ValueError('No match for selection name "%s" found' % n) + raise ValueError(f'No match for selection name "{n}" found') # make the selection a sorted list with unique elements sel = list(set(sel)) diff --git a/mne/channels/layout.py b/mne/channels/layout.py index d19794115d7..4b9968874b4 100644 --- a/mne/channels/layout.py +++ b/mne/channels/layout.py @@ -89,7 +89,7 @@ def save(self, fname, overwrite=False): elif fname.suffix == ".lay": out_str = "" else: - raise ValueError("Unknown layout type. Should be of type " ".lout or .lay.") + raise ValueError("Unknown layout type. Should be of type .lout or .lay.") for ii in range(x.shape[0]): out_str += "%03d %8.2f %8.2f %8.2f %8.2f %s\n" % ( @@ -1075,7 +1075,7 @@ def _merge_grad_data(data, method="rms"): elif method == "rms": data = np.sqrt(np.sum(data**2, axis=1) / 2) else: - raise ValueError('method must be "rms" or "mean", got %s.' % method) + raise ValueError(f'method must be "rms" or "mean", got {method}.') return data.reshape(data.shape[:1] + orig_shape[1:]) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index abc9f2f62b7..ea6d7ba92be 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -441,7 +441,7 @@ def is_fid_defined(fid): ( "Cannot add two DigMontage objects if they contain duplicated" " channel names. Duplicated channel(s) found: {}." - ).format(", ".join(["%r" % v for v in sorted(ch_names_intersection)])) + ).format(", ".join([f"{v!r}" for v in sorted(ch_names_intersection)])) ) # Check for unique matching fiducials @@ -461,7 +461,7 @@ def is_fid_defined(fid): raise RuntimeError( "Cannot add two DigMontage objects if " "fiducial locations do not match " - "(%s)" % kk + f"({kk})" ) # keep self @@ -1207,14 +1207,14 @@ def _backcompat_value(pos, ref_pos): n_dup = len(ch_pos) - len(ch_pos_use) if n_dup: raise ValueError( - "Cannot use match_case=False as %s montage " - "name(s) require case sensitivity" % n_dup + f"Cannot use match_case=False as {n_dup} montage " + "name(s) require case sensitivity" ) n_dup = len(info_names_use) - len(set(info_names_use)) if n_dup: raise ValueError( - "Cannot use match_case=False as %s channel " - "name(s) require case sensitivity" % n_dup + f"Cannot use match_case=False as {n_dup} channel " + "name(s) require case sensitivity" ) ch_pos = ch_pos_use del ch_pos_use @@ -1527,7 +1527,7 @@ def read_polhemus_fastscan( _check_option("fname", ext, VALID_FILE_EXT) if not _is_polhemus_fastscan(fname): - msg = "%s does not contain a valid Polhemus FastSCAN header" % fname + msg = f"{fname} does not contain a valid Polhemus FastSCAN header" _on_missing(on_header_missing, msg) points = _scale * np.loadtxt(fname, comments="%", ndmin=2) diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index 08971ab803b..e960a533eed 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -634,7 +634,7 @@ def test_read_dig_montage_using_polhemus_fastscan(): ) assert repr(montage) == ( - "" + "" ) assert set([d["coord_frame"] for d in montage.dig]) == {FIFF.FIFFV_COORD_UNKNOWN} @@ -679,7 +679,7 @@ def test_read_dig_polhemus_isotrak_hsp(): } montage = read_dig_polhemus_isotrak(fname=kit_dir / "test.hsp", ch_names=None) assert repr(montage) == ( - "" + "" ) fiducials, fid_coordframe = _get_fid_coords(montage.dig) @@ -698,7 +698,7 @@ def test_read_dig_polhemus_isotrak_elp(): } montage = read_dig_polhemus_isotrak(fname=kit_dir / "test.elp", ch_names=None) assert repr(montage) == ( - "" + "" ) fiducials, fid_coordframe = _get_fid_coords(montage.dig) @@ -735,7 +735,7 @@ def isotrak_eeg(tmp_path_factory): ) fid.write(f"{N_ROWS} {N_COLS}\n") for row in content: - fid.write("\t".join("%0.18e" % cell for cell in row) + "\n") + fid.write("\t".join(f"{cell:0.18e}" for cell in row) + "\n") return str(fname) @@ -756,7 +756,7 @@ def test_read_dig_polhemus_isotrak_eeg(isotrak_eeg): montage = read_dig_polhemus_isotrak(fname=isotrak_eeg, ch_names=ch_names) assert repr(montage) == ( - "" + "" ) fiducials, fid_coordframe = _get_fid_coords(montage.dig) @@ -835,7 +835,7 @@ def test_combining_digmontage_objects(): + ch_pos3 ) assert repr(montage) == ( - "" + "" ) EXPECTED_MONTAGE = make_dig_montage( @@ -1264,7 +1264,7 @@ def test_read_dig_captrak(tmp_path): assert montage.ch_names == EXPECTED_CH_NAMES assert repr(montage) == ( - "" + "" ) montage = transform_to_head(montage) # transform_to_head has to be tested @@ -1783,7 +1783,7 @@ def test_set_montage_with_missing_coordinates(): rpa=[-1, 0, 0], ) - with pytest.raises(ValueError, match="DigMontage is " "only a subset of info"): + with pytest.raises(ValueError, match="DigMontage is only a subset of info"): raw.set_montage(montage_in_mri) with pytest.raises(ValueError, match="Invalid value"): @@ -1792,7 +1792,7 @@ def test_set_montage_with_missing_coordinates(): with pytest.raises(TypeError, match="must be an instance"): raw.set_montage(montage_in_mri, on_missing=True) - with pytest.warns(RuntimeWarning, match="DigMontage is " "only a subset of info"): + with pytest.warns(RuntimeWarning, match="DigMontage is only a subset of info"): raw.set_montage(montage_in_mri, on_missing="warn") raw.set_montage(montage_in_mri, on_missing="ignore") @@ -1909,7 +1909,7 @@ def test_read_dig_hpts(): fname = io_dir / "brainvision" / "tests" / "data" / "test.hpts" montage = read_dig_hpts(fname) assert repr(montage) == ( - "" + "" ) diff --git a/mne/chpi.py b/mne/chpi.py index 780c892e3d6..090459e5855 100644 --- a/mne/chpi.py +++ b/mne/chpi.py @@ -115,7 +115,7 @@ def read_head_pos(fname): data = np.loadtxt(fname, skiprows=1) # first line is header, skip it data.shape = (-1, 10) # ensure it's the right size even if empty if np.isnan(data).any(): # make sure we didn't do something dumb - raise RuntimeError("positions could not be read properly from %s" % fname) + raise RuntimeError(f"positions could not be read properly from {fname}") return data @@ -470,7 +470,7 @@ def _get_hpi_initial_fit(info, adjust=False, verbose=None): if "moments" in hpi_result: logger.debug("Hpi coil moments (%d %d):" % hpi_result["moments"].shape[::-1]) for moment in hpi_result["moments"]: - logger.debug("{:g} {:g} {:g}".format(*tuple(moment))) + logger.debug(f"{moment[0]:g} {moment[1]:g} {moment[2]:g}") errors = np.linalg.norm(hpi_rrs - hpi_rrs_fit, axis=1) logger.debug(f"HPIFIT errors: {', '.join(f'{1000 * e:0.1f}' for e in errors)} mm.") if errors.sum() < len(errors) * dist_limit: @@ -638,11 +638,8 @@ def _setup_hpi_amplitude_fitting( ) else: line_freqs = np.zeros([0]) - logger.info( - "Line interference frequencies: {} Hz".format( - " ".join([f"{lf}" for lf in line_freqs]) - ) - ) + lfs = " ".join(f"{lf}" for lf in line_freqs) + logger.info(f"Line interference frequencies: {lfs} Hz") # worry about resampled/filtered data. # What to do e.g. if Raw has been resampled and some of our # HPI freqs would now be aliased @@ -757,7 +754,7 @@ def _setup_ext_proj(info, ext_order): def _time_prefix(fit_time): """Format log messages.""" - return (" t=%0.3f:" % fit_time).ljust(17) + return (f" t={fit_time:0.3f}:").ljust(17) def _fit_chpi_amplitudes(raw, time_sl, hpi, snr=False): @@ -995,16 +992,12 @@ def compute_head_pos( errs = np.linalg.norm(hpi_dig_head_rrs - est_coil_head_rrs, axis=1) n_good = ((g_coils >= gof_limit) & (errs < dist_limit)).sum() if n_good < 3: + warn_str = ", ".join( + f"{1000 * e:0.1f}::{g:0.2f}" for e, g in zip(errs, g_coils) + ) warn( - _time_prefix(fit_time) - + "{}/{} good HPI fits, cannot " - "determine the transformation ({} mm/GOF)!".format( - n_good, - n_coils, - ", ".join( - f"{1000 * e:0.1f}::{g:0.2f}" for e, g in zip(errs, g_coils) - ), - ) + f"{_time_prefix(fit_time)}{n_good}/{n_coils} good HPI fits, cannot " + f"determine the transformation ({warn_str} mm/GOF)!" ) continue @@ -1064,11 +1057,8 @@ def compute_head_pos( f" #t = {fit_time:0.3f}, #e = {100 * errs.mean():0.2f} cm, #g = {g:0.3f}" f", #v = {100 * v:0.2f} cm/s, #r = {r:0.2f} rad/s, #d = {d:0.2f} cm" ) - logger.debug( - " #t = {:0.3f}, #q = {} ".format( - fit_time, " ".join(map("{:8.5f}".format, this_quat)) - ) - ) + q_rep = " ".join(f"{qq:8.5f}" for qq in this_quat) + logger.debug(f" #t = {fit_time:0.3f}, #q = {q_rep}") quats.append( np.concatenate(([fit_time], this_quat, [g], [errs[use_idx].mean()], [v])) @@ -1519,11 +1509,11 @@ def filter_chpi( meg_picks = pick_types(raw.info, meg=True, exclude=()) # filter all chs n_times = len(raw.times) - msg = "Removing %s cHPI" % n_freqs + msg = f"Removing {n_freqs} cHPI" if include_line: n_remove += 2 * len(hpi["line_freqs"]) - msg += " and %s line harmonic" % len(hpi["line_freqs"]) - msg += " frequencies from %s MEG channels" % len(meg_picks) + msg += f" and {len(hpi['line_freqs'])} line harmonic" + msg += f" frequencies from {len(meg_picks)} MEG channels" recon = np.dot(hpi["model"][:, :n_remove], hpi["inv_model"][:n_remove]).T logger.info(msg) diff --git a/mne/commands/mne_anonymize.py b/mne/commands/mne_anonymize.py index a282f016ede..8a66472f1a6 100644 --- a/mne/commands/mne_anonymize.py +++ b/mne/commands/mne_anonymize.py @@ -119,7 +119,7 @@ def run(): daysback = options.daysback overwrite = options.overwrite if not fname.endswith(".fif"): - raise ValueError("%s does not seem to be a .fif file." % fname) + raise ValueError(f"{fname} does not seem to be a .fif file.") mne_anonymize(fname, out_fname, keep_his, daysback, overwrite) diff --git a/mne/commands/mne_browse_raw.py b/mne/commands/mne_browse_raw.py index 2e662e1768b..74b03e9e1e7 100644 --- a/mne/commands/mne_browse_raw.py +++ b/mne/commands/mne_browse_raw.py @@ -77,7 +77,7 @@ def run(): "-o", "--order", dest="group_by", - help="Order to use for grouping during plotting " "('type' or 'original')", + help="Order to use for grouping during plotting ('type' or 'original')", default="type", ) parser.add_option( diff --git a/mne/commands/mne_clean_eog_ecg.py b/mne/commands/mne_clean_eog_ecg.py index 10b84540756..d8d2ce2660f 100644 --- a/mne/commands/mne_clean_eog_ecg.py +++ b/mne/commands/mne_clean_eog_ecg.py @@ -77,7 +77,7 @@ def clean_ecg_eog( ecg_events, _, _ = mne.preprocessing.find_ecg_events( raw_in, reject_by_annotation=True ) - print("Writing ECG events in %s" % ecg_event_fname) + print(f"Writing ECG events in {ecg_event_fname}") mne.write_events(ecg_event_fname, ecg_events) print("Computing ECG projector") command = ( @@ -113,7 +113,7 @@ def clean_ecg_eog( mne.utils.run_subprocess(command, **kwargs) if eog: eog_events = mne.preprocessing.find_eog_events(raw_in) - print("Writing EOG events in %s" % eog_event_fname) + print(f"Writing EOG events in {eog_event_fname}") mne.write_events(eog_event_fname, eog_events) print("Computing EOG projector") command = ( @@ -168,7 +168,7 @@ def clean_ecg_eog( ) mne.utils.run_subprocess(command, **kwargs) print("Done removing artifacts.") - print("Cleaned raw data saved in: %s" % out_fif_fname) + print(f"Cleaned raw data saved in: {out_fif_fname}") print("IMPORTANT : Please eye-ball the data !!") else: print("Projection not applied to raw data.") diff --git a/mne/commands/mne_compute_proj_ecg.py b/mne/commands/mne_compute_proj_ecg.py index caab628bbb2..45c333585ba 100644 --- a/mne/commands/mne_compute_proj_ecg.py +++ b/mne/commands/mne_compute_proj_ecg.py @@ -86,21 +86,21 @@ def run(): "--ecg-l-freq", dest="ecg_l_freq", type="float", - help="Filter low cut-off frequency in Hz used " "for ECG event detection", + help="Filter low cut-off frequency in Hz used for ECG event detection", default=5, ) parser.add_option( "--ecg-h-freq", dest="ecg_h_freq", type="float", - help="Filter high cut-off frequency in Hz used " "for ECG event detection", + help="Filter high cut-off frequency in Hz used for ECG event detection", default=35, ) parser.add_option( "-p", "--preload", dest="preload", - help="Temporary file used during computation " "(to save memory)", + help="Temporary file used during computation (to save memory)", default=True, ) parser.add_option( @@ -133,35 +133,35 @@ def run(): "-c", "--channel", dest="ch_name", - help="Channel to use for ECG detection " "(Required if no ECG found)", + help="Channel to use for ECG detection (Required if no ECG found)", default=None, ) parser.add_option( "--rej-grad", dest="rej_grad", type="float", - help="Gradiometers rejection parameter " "in fT/cm (peak to peak amplitude)", + help="Gradiometers rejection parameter in fT/cm (peak to peak amplitude)", default=2000, ) parser.add_option( "--rej-mag", dest="rej_mag", type="float", - help="Magnetometers rejection parameter " "in fT (peak to peak amplitude)", + help="Magnetometers rejection parameter in fT (peak to peak amplitude)", default=3000, ) parser.add_option( "--rej-eeg", dest="rej_eeg", type="float", - help="EEG rejection parameter in µV " "(peak to peak amplitude)", + help="EEG rejection parameter in µV (peak to peak amplitude)", default=50, ) parser.add_option( "--rej-eog", dest="rej_eog", type="float", - help="EOG rejection parameter in µV " "(peak to peak amplitude)", + help="EOG rejection parameter in µV (peak to peak amplitude)", default=250, ) parser.add_option( @@ -175,13 +175,13 @@ def run(): "--no-proj", dest="no_proj", action="store_true", - help="Exclude the SSP projectors currently " "in the fiff file", + help="Exclude the SSP projectors currently in the fiff file", default=False, ) parser.add_option( "--bad", dest="bad_fname", - help="Text file containing bad channels list " "(one per line)", + help="Text file containing bad channels list (one per line)", default=None, ) parser.add_option( @@ -258,7 +258,7 @@ def run(): if bad_fname is not None: with open(bad_fname) as fid: bads = [w.rstrip() for w in fid.readlines()] - print("Bad channels read : %s" % bads) + print(f"Bad channels read : {bads}") else: bads = [] @@ -315,17 +315,17 @@ def run(): raw_event.close() if proj_fname is not None: - print("Including SSP projections from : %s" % proj_fname) + print(f"Including SSP projections from : {proj_fname}") # append the ecg projs, so they are last in the list projs = mne.read_proj(proj_fname) + projs if isinstance(preload, str) and os.path.exists(preload): os.remove(preload) - print("Writing ECG projections in %s" % ecg_proj_fname) + print(f"Writing ECG projections in {ecg_proj_fname}") mne.write_proj(ecg_proj_fname, projs) - print("Writing ECG events in %s" % ecg_event_fname) + print(f"Writing ECG events in {ecg_event_fname}") mne.write_events(ecg_event_fname, events) diff --git a/mne/commands/mne_compute_proj_eog.py b/mne/commands/mne_compute_proj_eog.py index 165818facc4..eba417b039c 100644 --- a/mne/commands/mne_compute_proj_eog.py +++ b/mne/commands/mne_compute_proj_eog.py @@ -96,21 +96,21 @@ def run(): "--eog-l-freq", dest="eog_l_freq", type="float", - help="Filter low cut-off frequency in Hz used for " "EOG event detection", + help="Filter low cut-off frequency in Hz used for EOG event detection", default=1, ) parser.add_option( "--eog-h-freq", dest="eog_h_freq", type="float", - help="Filter high cut-off frequency in Hz used for " "EOG event detection", + help="Filter high cut-off frequency in Hz used for EOG event detection", default=10, ) parser.add_option( "-p", "--preload", dest="preload", - help="Temporary file used during computation (to " "save memory)", + help="Temporary file used during computation (to save memory)", default=True, ) parser.add_option( @@ -143,28 +143,28 @@ def run(): "--rej-grad", dest="rej_grad", type="float", - help="Gradiometers rejection parameter in fT/cm (peak " "to peak amplitude)", + help="Gradiometers rejection parameter in fT/cm (peak to peak amplitude)", default=2000, ) parser.add_option( "--rej-mag", dest="rej_mag", type="float", - help="Magnetometers rejection parameter in fT (peak to " "peak amplitude)", + help="Magnetometers rejection parameter in fT (peak to peak amplitude)", default=3000, ) parser.add_option( "--rej-eeg", dest="rej_eeg", type="float", - help="EEG rejection parameter in µV (peak to peak " "amplitude)", + help="EEG rejection parameter in µV (peak to peak amplitude)", default=50, ) parser.add_option( "--rej-eog", dest="rej_eog", type="float", - help="EOG rejection parameter in µV (peak to peak " "amplitude)", + help="EOG rejection parameter in µV (peak to peak amplitude)", default=1e9, ) parser.add_option( @@ -178,13 +178,13 @@ def run(): "--no-proj", dest="no_proj", action="store_true", - help="Exclude the SSP projectors currently in the " "fiff file", + help="Exclude the SSP projectors currently in the fiff file", default=False, ) parser.add_option( "--bad", dest="bad_fname", - help="Text file containing bad channels list " "(one per line)", + help="Text file containing bad channels list (one per line)", default=None, ) parser.add_option( @@ -255,7 +255,7 @@ def run(): if bad_fname is not None: with open(bad_fname) as fid: bads = [w.rstrip() for w in fid.readlines()] - print("Bad channels read : %s" % bads) + print(f"Bad channels read : {bads}") else: bads = [] @@ -311,17 +311,17 @@ def run(): raw_event.close() if proj_fname is not None: - print("Including SSP projections from : %s" % proj_fname) + print(f"Including SSP projections from : {proj_fname}") # append the eog projs, so they are last in the list projs = mne.read_proj(proj_fname) + projs if isinstance(preload, str) and os.path.exists(preload): os.remove(preload) - print("Writing EOG projections in %s" % eog_proj_fname) + print(f"Writing EOG projections in {eog_proj_fname}") mne.write_proj(eog_proj_fname, projs) - print("Writing EOG events in %s" % eog_event_fname) + print(f"Writing EOG events in {eog_event_fname}") mne.write_events(eog_event_fname, events) diff --git a/mne/commands/mne_coreg.py b/mne/commands/mne_coreg.py index b0551346e43..45c9e803697 100644 --- a/mne/commands/mne_coreg.py +++ b/mne/commands/mne_coreg.py @@ -46,7 +46,7 @@ def run(): type=float, default=None, dest="head_opacity", - help="The opacity of the head surface, in the range " "[0, 1].", + help="The opacity of the head surface, in the range [0, 1].", ) parser.add_option( "--high-res-head", @@ -82,7 +82,7 @@ def run(): if options.low_res_head: if options.high_res_head: raise ValueError( - "Can't specify --high-res-head and " "--low-res-head at the same time." + "Can't specify --high-res-head and --low-res-head at the same time." ) head_high_res = False elif options.high_res_head: diff --git a/mne/commands/mne_flash_bem.py b/mne/commands/mne_flash_bem.py index 9dde09d2208..2d907da9c44 100644 --- a/mne/commands/mne_flash_bem.py +++ b/mne/commands/mne_flash_bem.py @@ -83,9 +83,7 @@ def run(): dest="flash5", action="callback", callback=_vararg_callback, - help=( - "Path to the multiecho flash 5 images. " "Can be one file or one per echo." - ), + help=("Path to the multiecho flash 5 images. Can be one file or one per echo."), ) parser.add_option( "-r", diff --git a/mne/commands/mne_freeview_bem_surfaces.py b/mne/commands/mne_freeview_bem_surfaces.py index 502e4fe2d67..32fc21d2d24 100644 --- a/mne/commands/mne_freeview_bem_surfaces.py +++ b/mne/commands/mne_freeview_bem_surfaces.py @@ -58,9 +58,9 @@ def freeview_bem_surfaces(subject, subjects_dir, method=None): if method == "watershed": bem_dir = op.join(bem_dir, "watershed") - outer_skin = op.join(bem_dir, "%s_outer_skin_surface" % subject) - outer_skull = op.join(bem_dir, "%s_outer_skull_surface" % subject) - inner_skull = op.join(bem_dir, "%s_inner_skull_surface" % subject) + outer_skin = op.join(bem_dir, f"{subject}_outer_skin_surface") + outer_skull = op.join(bem_dir, f"{subject}_outer_skull_surface") + inner_skull = op.join(bem_dir, f"{subject}_inner_skull_surface") else: if method == "flash": bem_dir = op.join(bem_dir, "flash") @@ -71,9 +71,9 @@ def freeview_bem_surfaces(subject, subjects_dir, method=None): # put together the command cmd = ["freeview"] cmd += ["--volume", mri] - cmd += ["--surface", "%s:color=red:edgecolor=red" % inner_skull] - cmd += ["--surface", "%s:color=yellow:edgecolor=yellow" % outer_skull] - cmd += ["--surface", "%s:color=255,170,127:edgecolor=255,170,127" % outer_skin] + cmd += ["--surface", f"{inner_skull}:color=red:edgecolor=red"] + cmd += ["--surface", f"{outer_skull}:color=yellow:edgecolor=yellow"] + cmd += ["--surface", f"{outer_skin}:color=255,170,127:edgecolor=255,170,127"] run_subprocess(cmd, env=env, stdout=sys.stdout) print("[done]") diff --git a/mne/commands/mne_kit2fiff.py b/mne/commands/mne_kit2fiff.py index a3a294d8312..5fa770825a2 100644 --- a/mne/commands/mne_kit2fiff.py +++ b/mne/commands/mne_kit2fiff.py @@ -82,7 +82,7 @@ def run(): from mne_kit_gui import kit2fiff # noqa except ImportError: raise ImportError( - "The mne-kit-gui package is required, install it using " "conda or pip" + "The mne-kit-gui package is required, install it using conda or pip" ) from None kit2fiff() sys.exit(0) diff --git a/mne/commands/mne_make_scalp_surfaces.py b/mne/commands/mne_make_scalp_surfaces.py index 85b7acd2883..0d810a41339 100644 --- a/mne/commands/mne_make_scalp_surfaces.py +++ b/mne/commands/mne_make_scalp_surfaces.py @@ -77,7 +77,7 @@ def run(): "-n", "--no-decimate", dest="no_decimate", - help="Disable medium and sparse decimations " "(dense only)", + help="Disable medium and sparse decimations (dense only)", action="store_true", ) _add_verbose_flag(parser) diff --git a/mne/commands/mne_maxfilter.py b/mne/commands/mne_maxfilter.py deleted file mode 100644 index 4cbb1dc9522..00000000000 --- a/mne/commands/mne_maxfilter.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/env python -"""Apply MaxFilter. - -Examples --------- -.. code-block:: console - - $ mne maxfilter -i sample_audvis_raw.fif --st - -This will apply MaxFilter with the MaxSt extension. The origin used -by MaxFilter is computed by mne-python by fitting a sphere to the -headshape points. -""" - -# Authors : Martin Luessi -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - -import os -import sys - -import mne - - -def run(): - """Run command.""" - from mne.commands.utils import get_optparser - - parser = get_optparser(__file__) - - parser.add_option( - "-i", "--in", dest="in_fname", help="Input raw FIF file", metavar="FILE" - ) - parser.add_option( - "-o", - dest="out_fname", - help="Output FIF file (if not set, suffix '_sss' will " "be used)", - metavar="FILE", - default=None, - ) - parser.add_option( - "--origin", - dest="origin", - help="Head origin in mm, or a filename to read the " - "origin from. If not set it will be estimated from " - "headshape points", - default=None, - ) - parser.add_option( - "--origin-out", - dest="origin_out", - help="Filename to use for computed origin", - default=None, - ) - parser.add_option( - "--frame", - dest="frame", - type="string", - help="Coordinate frame for head center ('device' or " "'head')", - default="device", - ) - parser.add_option( - "--bad", - dest="bad", - type="string", - help="List of static bad channels", - default=None, - ) - parser.add_option( - "--autobad", - dest="autobad", - type="string", - help="Set automated bad channel detection ('on', 'off', " "'n')", - default="off", - ) - parser.add_option( - "--skip", - dest="skip", - help="Skips raw data sequences, time intervals pairs in " - "s, e.g.: 0 30 120 150", - default=None, - ) - parser.add_option( - "--force", - dest="force", - action="store_true", - help="Ignore program warnings", - default=False, - ) - parser.add_option( - "--st", - dest="st", - action="store_true", - help="Apply the time-domain MaxST extension", - default=False, - ) - parser.add_option( - "--buflen", - dest="st_buflen", - type="float", - help="MaxSt buffer length in s", - default=16.0, - ) - parser.add_option( - "--corr", - dest="st_corr", - type="float", - help="MaxSt subspace correlation", - default=0.96, - ) - parser.add_option( - "--trans", - dest="mv_trans", - help="Transforms the data into the coil definitions of " - "in_fname, or into the default frame", - default=None, - ) - parser.add_option( - "--movecomp", - dest="mv_comp", - action="store_true", - help="Estimates and compensates head movements in " "continuous raw data", - default=False, - ) - parser.add_option( - "--headpos", - dest="mv_headpos", - action="store_true", - help="Estimates and stores head position parameters, " - "but does not compensate movements", - default=False, - ) - parser.add_option( - "--hp", - dest="mv_hp", - type="string", - help="Stores head position data in an ascii file", - default=None, - ) - parser.add_option( - "--hpistep", - dest="mv_hpistep", - type="float", - help="Sets head position update interval in ms", - default=None, - ) - parser.add_option( - "--hpisubt", - dest="mv_hpisubt", - type="string", - help="Subtracts hpi signals: sine amplitudes, amp + " "baseline, or switch off", - default=None, - ) - parser.add_option( - "--nohpicons", - dest="mv_hpicons", - action="store_false", - help="Do not check initial consistency isotrak vs " "hpifit", - default=True, - ) - parser.add_option( - "--linefreq", - dest="linefreq", - type="float", - help="Sets the basic line interference frequency (50 or " "60 Hz)", - default=None, - ) - parser.add_option( - "--nooverwrite", - dest="overwrite", - action="store_false", - help="Do not overwrite output file if it already exists", - default=True, - ) - parser.add_option( - "--args", - dest="mx_args", - type="string", - help="Additional command line arguments to pass to " "MaxFilter", - default="", - ) - - options, args = parser.parse_args() - - in_fname = options.in_fname - - if in_fname is None: - parser.print_help() - sys.exit(1) - - out_fname = options.out_fname - origin = options.origin - origin_out = options.origin_out - frame = options.frame - bad = options.bad - autobad = options.autobad - skip = options.skip - force = options.force - st = options.st - st_buflen = options.st_buflen - st_corr = options.st_corr - mv_trans = options.mv_trans - mv_comp = options.mv_comp - mv_headpos = options.mv_headpos - mv_hp = options.mv_hp - mv_hpistep = options.mv_hpistep - mv_hpisubt = options.mv_hpisubt - mv_hpicons = options.mv_hpicons - linefreq = options.linefreq - overwrite = options.overwrite - mx_args = options.mx_args - - if in_fname.endswith("_raw.fif") or in_fname.endswith("-raw.fif"): - prefix = in_fname[:-8] - else: - prefix = in_fname[:-4] - - if out_fname is None: - if st: - out_fname = prefix + "_tsss.fif" - else: - out_fname = prefix + "_sss.fif" - - if origin is not None and os.path.exists(origin): - with open(origin) as fid: - origin = fid.readlines()[0].strip() - - origin = mne.preprocessing.apply_maxfilter( - in_fname, - out_fname, - origin, - frame, - bad, - autobad, - skip, - force, - st, - st_buflen, - st_corr, - mv_trans, - mv_comp, - mv_headpos, - mv_hp, - mv_hpistep, - mv_hpisubt, - mv_hpicons, - linefreq, - mx_args, - overwrite, - ) - - if origin_out is not None: - with open(origin_out, "w") as fid: - fid.write(origin + "\n") - - -mne.utils.run_command_if_main() diff --git a/mne/commands/mne_report.py b/mne/commands/mne_report.py index bf7010cc8a3..ce3e1b42805 100644 --- a/mne/commands/mne_report.py +++ b/mne/commands/mne_report.py @@ -80,7 +80,7 @@ @verbose def log_elapsed(t, verbose=None): """Log elapsed time.""" - logger.info("Report complete in %s seconds" % round(t, 1)) + logger.info(f"Report complete in {round(t, 1)} seconds") def run(): @@ -112,13 +112,13 @@ def run(): parser.add_option( "--bmin", dest="bmin", - help="Time at which baseline correction starts for " "evokeds", + help="Time at which baseline correction starts for evokeds", default=None, ) parser.add_option( "--bmax", dest="bmax", - help="Time at which baseline correction stops for " "evokeds", + help="Time at which baseline correction stops for evokeds", default=None, ) parser.add_option( @@ -138,7 +138,7 @@ def run(): help="Overwrite html report if it already exists", ) parser.add_option( - "-j", "--jobs", dest="n_jobs", help="Number of jobs to" " run in parallel" + "-j", "--jobs", dest="n_jobs", help="Number of jobs to run in parallel" ) parser.add_option( "-m", @@ -146,14 +146,14 @@ def run(): type="int", dest="mri_decim", default=2, - help="Integer factor used to decimate " "BEM plots", + help="Integer factor used to decimate BEM plots", ) parser.add_option( "--image-format", type="str", dest="image_format", default="png", - help="Image format to use " "(can be 'png' or 'svg')", + help="Image format to use (can be 'png' or 'svg')", ) _add_verbose_flag(parser) diff --git a/mne/commands/mne_setup_source_space.py b/mne/commands/mne_setup_source_space.py index f5f5dc8b343..723a1ee67d8 100644 --- a/mne/commands/mne_setup_source_space.py +++ b/mne/commands/mne_setup_source_space.py @@ -69,7 +69,7 @@ def run(): parser.add_option( "--oct", dest="oct", - help="use the recursively subdivided octahedron " "to create the source space.", + help="use the recursively subdivided octahedron to create the source space.", default=None, type="int", ) diff --git a/mne/commands/mne_show_info.py b/mne/commands/mne_show_info.py index d81e5c8f2a6..96db7734abd 100644 --- a/mne/commands/mne_show_info.py +++ b/mne/commands/mne_show_info.py @@ -29,10 +29,10 @@ def run(): fname = args[0] if not fname.endswith(".fif"): - raise ValueError("%s does not seem to be a .fif file." % fname) + raise ValueError(f"{fname} does not seem to be a .fif file.") info = mne.io.read_info(fname) - print("File : %s" % fname) + print(f"File : {fname}") print(info) diff --git a/mne/commands/mne_surf2bem.py b/mne/commands/mne_surf2bem.py index 18a09a6402d..0dbcba0c55f 100644 --- a/mne/commands/mne_surf2bem.py +++ b/mne/commands/mne_surf2bem.py @@ -46,7 +46,7 @@ def run(): parser.print_help() sys.exit(1) - print("Converting %s to BEM FIF file." % options.surf) + print(f"Converting {options.surf} to BEM FIF file.") surf = mne.bem._surfaces_to_bem([options.surf], [int(options.id)], sigmas=[1]) mne.write_bem_surfaces(options.fif, surf) diff --git a/mne/commands/mne_watershed_bem.py b/mne/commands/mne_watershed_bem.py index 23c7e3ebbe5..caf06378f27 100644 --- a/mne/commands/mne_watershed_bem.py +++ b/mne/commands/mne_watershed_bem.py @@ -57,7 +57,7 @@ def run(): "-g", "--gcaatlas", dest="gcaatlas", - help="Specify the --brain_atlas option for " "mri_watershed", + help="Specify the --brain_atlas option for mri_watershed", default=False, action="store_true", ) diff --git a/mne/commands/utils.py b/mne/commands/utils.py index 112ff27deca..b3ed3d1d213 100644 --- a/mne/commands/utils.py +++ b/mne/commands/utils.py @@ -93,16 +93,16 @@ def print_help(): # noqa print("Usage : mne command options\n") print("Accepted commands :\n") for c in valid_commands: - print("\t- %s" % c) + print(f"\t- {c}") print("\nExample : mne browse_raw --raw sample_audvis_raw.fif") print("\nGetting help example : mne compute_proj_eog -h") if len(sys.argv) == 1 or "help" in sys.argv[1] or "-h" in sys.argv[1]: print_help() elif sys.argv[1] == "--version": - print("MNE %s" % mne.__version__) + print(f"MNE {mne.__version__}") elif sys.argv[1] not in valid_commands: - print('Invalid command: "%s"\n' % sys.argv[1]) + print(f'Invalid command: "{sys.argv[1]}"\n') print_help() else: cmd = sys.argv[1] diff --git a/mne/conftest.py b/mne/conftest.py index 8e50be0bcde..acc4f792700 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -1009,7 +1009,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): def pytest_report_header(config, startdir=None): """Add information to the pytest run header.""" - return f"MNE {mne.__version__} -- {str(Path(mne.__file__).parent)}" + return f"MNE {mne.__version__} -- {Path(mne.__file__).parent}" @pytest.fixture(scope="function", params=("Numba", "NumPy")) diff --git a/mne/coreg.py b/mne/coreg.py index 7dae561c2a2..0b87023b50b 100644 --- a/mne/coreg.py +++ b/mne/coreg.py @@ -180,13 +180,13 @@ def coregister_fiducials(info, fiducials, tol=0.01): def create_default_subject(fs_home=None, update=False, subjects_dir=None, verbose=None): """Create an average brain subject for subjects without structural MRI. - Create a copy of fsaverage from the Freesurfer directory in subjects_dir + Create a copy of fsaverage from the FreeSurfer directory in subjects_dir and add auxiliary files from the mne package. Parameters ---------- fs_home : None | str - The freesurfer home directory (only needed if ``FREESURFER_HOME`` is + The FreeSurfer home directory (only needed if ``FREESURFER_HOME`` is not specified as environment variable). update : bool In cases where a copy of the fsaverage brain already exists in the @@ -200,10 +200,10 @@ def create_default_subject(fs_home=None, update=False, subjects_dir=None, verbos Notes ----- When no structural MRI is available for a subject, an average brain can be - substituted. Freesurfer comes with such an average brain model, and MNE + substituted. FreeSurfer comes with such an average brain model, and MNE comes with some auxiliary files which make coregistration easier. :py:func:`create_default_subject` copies the relevant - files from Freesurfer into the current subjects_dir, and also adds the + files from FreeSurfer into the current subjects_dir, and also adds the auxiliary files provided by MNE. """ subjects_dir = str(get_subjects_dir(subjects_dir, raise_error=True)) @@ -216,17 +216,17 @@ def create_default_subject(fs_home=None, update=False, subjects_dir=None, verbos "create_default_subject()." ) - # make sure freesurfer files exist + # make sure FreeSurfer files exist fs_src = os.path.join(fs_home, "subjects", "fsaverage") if not os.path.exists(fs_src): raise OSError( - "fsaverage not found at %r. Is fs_home specified correctly?" % fs_src + f"fsaverage not found at {fs_src!r}. Is fs_home specified correctly?" ) for name in ("label", "mri", "surf"): dirname = os.path.join(fs_src, name) if not os.path.isdir(dirname): raise OSError( - "Freesurfer fsaverage seems to be incomplete: No directory named " + "FreeSurfer fsaverage seems to be incomplete: No directory named " f"{name} found in {fs_src}" ) @@ -234,20 +234,20 @@ def create_default_subject(fs_home=None, update=False, subjects_dir=None, verbos dest = os.path.join(subjects_dir, "fsaverage") if dest == fs_src: raise OSError( - "Your subjects_dir points to the freesurfer subjects_dir (%r). " - "The default subject can not be created in the freesurfer " - "installation directory; please specify a different " - "subjects_dir." % subjects_dir + "Your subjects_dir points to the FreeSurfer subjects_dir " + f"({repr(subjects_dir)}). The default subject can not be created in the " + "FreeSurfer installation directory; please specify a different " + "subjects_dir." ) elif (not update) and os.path.exists(dest): raise OSError( - "Can not create fsaverage because {!r} already exists in " - "subjects_dir {!r}. Delete or rename the existing fsaverage " - "subject folder.".format("fsaverage", subjects_dir) + 'Can not create fsaverage because "fsaverage" already exists in ' + f"subjects_dir {repr(subjects_dir)}. Delete or rename the existing " + "fsaverage subject folder." ) - # copy fsaverage from freesurfer - logger.info("Copying fsaverage subject from freesurfer directory...") + # copy fsaverage from FreeSurfer + logger.info("Copying fsaverage subject from FreeSurfer directory...") if (not update) or not os.path.exists(dest): shutil.copytree(fs_src, dest) _make_writable_recursive(dest) @@ -460,7 +460,7 @@ def fit_matched_points( est_pts = np.dot(src_pts, trans.T)[:, :3] err = np.sqrt(np.sum((est_pts - tgt_pts) ** 2, axis=1)) if np.any(err > tol): - raise RuntimeError("Error exceeds tolerance. Error = %r" % err) + raise RuntimeError(f"Error exceeds tolerance. Error = {err!r}") if out == "params": return x @@ -468,7 +468,7 @@ def fit_matched_points( return trans else: raise ValueError( - "Invalid out parameter: %r. Needs to be 'params' or 'trans'." % out + f"Invalid out parameter: {out!r}. Needs to be 'params' or 'trans'." ) @@ -669,11 +669,11 @@ def _find_mri_paths(subject, skip_fiducials, subjects_dir): # check that we found at least one if len(paths["fid"]) == 0: raise OSError( - "No fiducials file found for %s. The fiducials " + f"No fiducials file found for {subject}. The fiducials " "file should be named " "{subject}/bem/{subject}-fiducials.fif. In " "order to scale an MRI without fiducials set " - "skip_fiducials=True." % subject + "skip_fiducials=True." ) # duplicate files (curvature and some surfaces) @@ -706,7 +706,7 @@ def _find_mri_paths(subject, skip_fiducials, subjects_dir): prefix = subject + "-" for fname in fnames: if fname.startswith(prefix): - fname = "{subject}-%s" % fname[len(prefix) :] + fname = f"{{subject}}-{fname[len(prefix) :]}" path = os.path.join(bem_dirname, fname) src.append(path) @@ -827,7 +827,7 @@ def read_mri_cfg(subject, subjects_dir=None): "exist." ) - logger.info("Reading MRI cfg file %s" % fname) + logger.info(f"Reading MRI cfg file {fname}") config = configparser.RawConfigParser() config.read(fname) n_params = config.getint("MRI Scaling", "n_params") @@ -963,7 +963,7 @@ def scale_bem( dst = bem_fname.format(subjects_dir=subjects_dir, subject=subject_to, name=bem_name) if os.path.exists(dst): - raise OSError("File already exists: %s" % dst) + raise OSError(f"File already exists: {dst}") surfs = read_bem_surfaces(src, on_defects=on_defects) for surf in surfs: @@ -1353,7 +1353,7 @@ def _scale_xfm(subject_to, xfm_fname, mri_name, subject_from, scale, subjects_di # The "talairach.xfm" file stores the ras_mni transform. # # For "from" subj F, "to" subj T, F->T scaling S, some equivalent vertex - # positions F_x and T_x in MRI (Freesurfer RAS) coords, knowing that + # positions F_x and T_x in MRI (FreeSurfer RAS) coords, knowing that # we have T_x = S @ F_x, we want to have the same MNI coords computed # for these vertices: # @@ -1425,8 +1425,8 @@ def _read_surface(filename, *, on_defects): complete_surface_info(bem, copy=False) except Exception: raise ValueError( - "Error loading surface from %s (see " - "Terminal for details)." % filename + f"Error loading surface from {filename} (see " + "Terminal for details)." ) return bem diff --git a/mne/cov.py b/mne/cov.py index 7772a0a8324..2ade2d43f9b 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -1777,9 +1777,7 @@ def prepare_noise_cov( else: missing.append(c) if len(missing): - raise RuntimeError( - "Not all channels present in noise covariance:\n%s" % missing - ) + raise RuntimeError(f"Not all channels present in noise covariance:\n{missing}") C = noise_cov._get_square()[np.ix_(noise_cov_idx, noise_cov_idx)] info = pick_info(info, pick_channels(info["ch_names"], ch_names, ordered=False)) projs = info["projs"] + noise_cov["projs"] @@ -2054,7 +2052,7 @@ def regularize( idx_cov[ch_type].append(i) break else: - raise Exception("channel %s is unknown type" % ch) + raise Exception(f"channel {ch} is unknown type") C = cov_good["data"] diff --git a/mne/cuda.py b/mne/cuda.py index be645506de3..6941e3f1fc6 100644 --- a/mne/cuda.py +++ b/mne/cuda.py @@ -82,13 +82,13 @@ def init_cuda(ignore_config=False, verbose=None): except Exception: warn( "so CUDA device could be initialized, likely a hardware error, " - "CUDA not enabled%s" % _explain_exception() + f"CUDA not enabled{_explain_exception()}" ) return _cuda_capable = True # Figure out limit for CUDA FFT calculations - logger.info("Enabling CUDA with %s available memory" % get_cuda_memory()) + logger.info(f"Enabling CUDA with {get_cuda_memory()} available memory") @verbose @@ -178,12 +178,11 @@ def _setup_cuda_fft_multiply_repeated(n_jobs, h, n_fft, kind="FFT FIR filtering" try: # do the IFFT normalization now so we don't have to later h_fft = cupy.array(cuda_dict["h_fft"]) - logger.info("Using CUDA for %s" % kind) + logger.info(f"Using CUDA for {kind}") except Exception as exp: logger.info( - "CUDA not used, could not instantiate memory " - '(arrays may be too large: "%s"), falling back to ' - "n_jobs=None" % str(exp) + "CUDA not used, could not instantiate memory (arrays may be too " + f'large: "{exp}"), falling back to n_jobs=None' ) cuda_dict.update(h_fft=h_fft, rfft=_cuda_upload_rfft, irfft=_cuda_irfft_get) else: diff --git a/mne/datasets/_fetch.py b/mne/datasets/_fetch.py index 2b07ea29be0..8e8c559b183 100644 --- a/mne/datasets/_fetch.py +++ b/mne/datasets/_fetch.py @@ -219,11 +219,9 @@ def fetch_dataset( else: # If they don't have stdin, just accept the license # https://github.com/mne-tools/mne-python/issues/8513#issuecomment-726823724 # noqa: E501 - answer = _safe_input("%sAgree (y/[n])? " % _bst_license_text, use="y") + answer = _safe_input(f"{_bst_license_text}Agree (y/[n])? ", use="y") if answer.lower() != "y": - raise RuntimeError( - "You must agree to the license to use this " "dataset" - ) + raise RuntimeError("You must agree to the license to use this dataset") # downloader & processors download_params = _downloader_params(auth=auth, token=token) if name == "fake": diff --git a/mne/datasets/brainstorm/bst_phantom_elekta.py b/mne/datasets/brainstorm/bst_phantom_elekta.py index 2bafc2e98ef..31f9efadb63 100644 --- a/mne/datasets/brainstorm/bst_phantom_elekta.py +++ b/mne/datasets/brainstorm/bst_phantom_elekta.py @@ -40,7 +40,7 @@ def data_path( name="brainstorm", conf="MNE_DATASETS_BRAINSTORM_DATA_PATH" ) _data_path_doc = _data_path_doc.replace( - "brainstorm dataset", "brainstorm (bst_phantom_elekta) " "dataset" + "brainstorm dataset", "brainstorm (bst_phantom_elekta) dataset" ) data_path.__doc__ = _data_path_doc diff --git a/mne/datasets/config.py b/mne/datasets/config.py index 22fd45475bc..a2f2d7781b7 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -358,9 +358,7 @@ MNE_DATASETS["fake"] = dict( archive_name="foo.tgz", hash="md5:3194e9f7b46039bb050a74f3e1ae9908", - url=( - "https://github.com/mne-tools/mne-testing-data/raw/master/" "datasets/foo.tgz" - ), + url="https://github.com/mne-tools/mne-testing-data/raw/master/datasets/foo.tgz", folder_name="foo", config_key="MNE_DATASETS_FAKE_PATH", ) diff --git a/mne/datasets/tests/test_datasets.py b/mne/datasets/tests/test_datasets.py index d3a361786d7..2c71d3f3998 100644 --- a/mne/datasets/tests/test_datasets.py +++ b/mne/datasets/tests/test_datasets.py @@ -179,8 +179,8 @@ def test_fetch_parcellations(tmp_path): os.mkdir(op.join(this_subjects_dir, "fsaverage", "surf")) for hemi in ("lh", "rh"): shutil.copyfile( - op.join(subjects_dir, "fsaverage", "surf", "%s.white" % hemi), - op.join(this_subjects_dir, "fsaverage", "surf", "%s.white" % hemi), + op.join(subjects_dir, "fsaverage", "surf", f"{hemi}.white"), + op.join(this_subjects_dir, "fsaverage", "surf", f"{hemi}.white"), ) # speed up by prenteding we have one of them with open( @@ -192,9 +192,7 @@ def test_fetch_parcellations(tmp_path): datasets.fetch_hcp_mmp_parcellation(subjects_dir=this_subjects_dir) for hemi in ("lh", "rh"): assert op.isfile( - op.join( - this_subjects_dir, "fsaverage", "label", "%s.aparc_sub.annot" % hemi - ) + op.join(this_subjects_dir, "fsaverage", "label", f"{hemi}.aparc_sub.annot") ) # test our annot round-trips here kwargs = dict( @@ -235,7 +233,7 @@ def test_manifest_check_download(tmp_path, n_have, monkeypatch): manifest_path = op.join(str(tmp_path), "manifest.txt") with open(manifest_path, "w") as fid: for fname in _zip_fnames: - fid.write("%s\n" % fname) + fid.write(f"{fname}\n") assert n_have in range(len(_zip_fnames) + 1) assert not op.isdir(destination) if n_have > 0: @@ -311,9 +309,7 @@ def test_fetch_uncompressed_file(tmp_path): """Test downloading an uncompressed file with our fetch function.""" dataset_dict = dict( dataset_name="license", - url=( - "https://raw.githubusercontent.com/mne-tools/mne-python/main/" "LICENSE.txt" - ), + url="https://raw.githubusercontent.com/mne-tools/mne-python/main/LICENSE.txt", archive_name="LICENSE.foo", folder_name=op.join(tmp_path, "foo"), hash=None, diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index d4a8f4af459..5c3143300aa 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -119,7 +119,7 @@ def _get_path(path, key, name): return path # 4. ~/mne_data (but use a fake home during testing so we don't # unnecessarily create ~/mne_data) - logger.info("Using default location ~/mne_data for %s..." % name) + logger.info(f"Using default location ~/mne_data for {name}...") path = op.join(os.getenv("_MNE_FAKE_HOME_DIR", op.expanduser("~")), "mne_data") if not op.exists(path): logger.info("Creating ~/mne_data") @@ -128,10 +128,10 @@ def _get_path(path, key, name): except OSError: raise OSError( "User does not have write permissions " - "at '%s', try giving the path as an " + f"at '{path}', try giving the path as an " "argument to data_path() where user has " "write permissions, for ex:data_path" - "('/home/xyz/me2/')" % (path) + "('/home/xyz/me2/')" ) return Path(path).expanduser() @@ -464,9 +464,9 @@ def fetch_hcp_mmp_parcellation( if accept or "--accept-hcpmmp-license" in sys.argv: answer = "y" else: - answer = _safe_input("%s\nAgree (y/[n])? " % _hcp_mmp_license_text) + answer = _safe_input(f"{_hcp_mmp_license_text}\nAgree (y/[n])? ") if answer.lower() != "y": - raise RuntimeError("You must agree to the license to use this " "dataset") + raise RuntimeError("You must agree to the license to use this dataset") downloader = pooch.HTTPDownloader(**_downloader_params()) for hemi, fpath in zip(("lh", "rh"), fnames): if not op.isfile(fpath): @@ -481,7 +481,7 @@ def fetch_hcp_mmp_parcellation( if combine: fnames = [ - op.join(destination, "%s.HCPMMP1_combined.annot" % hemi) + op.join(destination, f"{hemi}.HCPMMP1_combined.annot") for hemi in ("lh", "rh") ] if all(op.isfile(fname) for fname in fnames): diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index b45453dc2dc..fd937193f21 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -158,7 +158,7 @@ def __init__( def _check_Xy(self, X, y=None): """Check input data.""" if not isinstance(X, np.ndarray): - raise ValueError("X should be of type ndarray (got %s)." % type(X)) + raise ValueError(f"X should be of type ndarray (got {type(X)}).") if y is not None: if len(X) != len(y) or len(y) < 1: raise ValueError("X and y must have the same length.") @@ -235,10 +235,10 @@ def transform(self, X): space and shape is (n_epochs, n_components, n_times). """ if not isinstance(X, np.ndarray): - raise ValueError("X should be of type ndarray (got %s)." % type(X)) + raise ValueError(f"X should be of type ndarray (got {type(X)}).") if self.filters_ is None: raise RuntimeError( - "No filters available. Please first fit CSP " "decomposition." + "No filters available. Please first fit CSP decomposition." ) pick_filters = self.filters_[: self.n_components] diff --git a/mne/decoding/receptive_field.py b/mne/decoding/receptive_field.py index 3bb3d6da903..8c5bfc62d90 100644 --- a/mne/decoding/receptive_field.py +++ b/mne/decoding/receptive_field.py @@ -381,7 +381,7 @@ def _check_dimensions(self, X, y, predict=False): y = y[:, :, np.newaxis] # Add an outputs dim elif y.ndim != 3: raise ValueError( - "If X has 3 dimensions, " "y must have 2 or 3 dimensions" + "If X has 3 dimensions, y must have 2 or 3 dimensions" ) else: raise ValueError( diff --git a/mne/decoding/search_light.py b/mne/decoding/search_light.py index c8d56b88d6e..1e811c4e7bd 100644 --- a/mne/decoding/search_light.py +++ b/mne/decoding/search_light.py @@ -144,7 +144,7 @@ def _transform(self, X, method): X = self._check_Xy(X) method = _check_method(self.base_estimator, method) if X.shape[-1] != len(self.estimators_): - raise ValueError("The number of estimators does not match " "X.shape[-1]") + raise ValueError("The number of estimators does not match X.shape[-1]") # For predictions/transforms the parallelization is across the data and # not across the estimators to avoid memory load. parallel, p_func, n_jobs = parallel_func( @@ -304,7 +304,7 @@ def score(self, X, y): X = self._check_Xy(X, y) if X.shape[-1] != len(self.estimators_): - raise ValueError("The number of estimators does not match " "X.shape[-1]") + raise ValueError("The number of estimators does not match X.shape[-1]") scoring = check_scoring(self.base_estimator, self.scoring) y = _fix_auc(scoring, y) @@ -450,7 +450,7 @@ def _check_method(estimator, method): if method == "transform" and not hasattr(estimator, "transform"): method = "predict" if not hasattr(estimator, method): - ValueError("base_estimator does not have `%s` method." % method) + ValueError(f"base_estimator does not have `{method}` method.") return method @@ -732,7 +732,7 @@ def _fix_auc(scoring, y): ): if np.ndim(y) != 1 or len(set(y)) != 2: raise ValueError( - "roc_auc scoring can only be computed for " "two-class problems." + "roc_auc scoring can only be computed for two-class problems." ) y = LabelEncoder().fit_transform(y) return y diff --git a/mne/decoding/tests/test_receptive_field.py b/mne/decoding/tests/test_receptive_field.py index 8585aa0170e..c1d9bc79d1b 100644 --- a/mne/decoding/tests/test_receptive_field.py +++ b/mne/decoding/tests/test_receptive_field.py @@ -221,7 +221,7 @@ def test_receptive_field_basic(n_jobs): with pytest.raises(ValueError, match="n_features in X does not match"): rf.fit(X[:, :1], y) # auto-naming features - feature_names = ["feature_%s" % ii for ii in [0, 1, 2]] + feature_names = [f"feature_{ii}" for ii in [0, 1, 2]] rf = ReceptiveField(tmin, tmax, 1, estimator=mod, feature_names=feature_names) assert_equal(rf.feature_names, feature_names) rf = ReceptiveField(tmin, tmax, 1, estimator=mod) diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 5e105bd399d..90af1e22345 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -60,7 +60,7 @@ def fit_transform(self, X, y=None): def _sklearn_reshape_apply(func, return_result, X, *args, **kwargs): """Reshape epochs and apply function.""" if not isinstance(X, np.ndarray): - raise ValueError("data should be an np.ndarray, got %s." % type(X)) + raise ValueError(f"data should be an np.ndarray, got {type(X)}.") orig_shape = X.shape X = np.reshape(X.transpose(0, 2, 1), (-1, orig_shape[1])) X = func(X, *args, **kwargs) @@ -115,14 +115,14 @@ def __init__(self, info=None, scalings=None, with_mean=True, with_std=True): if not (scalings is None or isinstance(scalings, (dict, str))): raise ValueError( - "scalings type should be dict, str, or None, " "got %s" % type(scalings) + "scalings type should be dict, str, or None, " f"got {type(scalings)}" ) if isinstance(scalings, str): _check_option("scalings", scalings, ["mean", "median"]) if scalings is None or isinstance(scalings, dict): if info is None: raise ValueError( - 'Need to specify "info" if scalings is' "%s" % type(scalings) + 'Need to specify "info" if scalings is' f"{type(scalings)}" ) self._scaler = _ConstantScaler(info, scalings, self.with_std) elif scalings == "mean": @@ -299,7 +299,7 @@ def transform(self, X): """ X = np.asarray(X) if X.shape[1:] != self.features_shape_: - raise ValueError("Shape of X used in fit and transform must be " "same") + raise ValueError("Shape of X used in fit and transform must be same") return X.reshape(len(X), -1) def fit_transform(self, X, y=None): @@ -417,7 +417,7 @@ def fit(self, epochs_data, y): """ if not isinstance(epochs_data, np.ndarray): raise ValueError( - "epochs_data should be of type ndarray (got %s)." % type(epochs_data) + f"epochs_data should be of type ndarray (got {type(epochs_data)})." ) return self @@ -437,7 +437,7 @@ def transform(self, epochs_data): """ if not isinstance(epochs_data, np.ndarray): raise ValueError( - "epochs_data should be of type ndarray (got %s)." % type(epochs_data) + f"epochs_data should be of type ndarray (got {type(epochs_data)})." ) psd, _ = psd_array_multitaper( epochs_data, @@ -548,7 +548,7 @@ def fit(self, epochs_data, y): """ if not isinstance(epochs_data, np.ndarray): raise ValueError( - "epochs_data should be of type ndarray (got %s)." % type(epochs_data) + f"epochs_data should be of type ndarray (got {type(epochs_data)})." ) if self.picks is None: @@ -598,7 +598,7 @@ def transform(self, epochs_data): """ if not isinstance(epochs_data, np.ndarray): raise ValueError( - "epochs_data should be of type ndarray (got %s)." % type(epochs_data) + f"epochs_data should be of type ndarray (got {type(epochs_data)})." ) epochs_data = np.atleast_3d(epochs_data) return filter_data( @@ -637,12 +637,12 @@ def __init__(self, estimator, average=False): if not hasattr(estimator, attr): raise ValueError( "estimator must be a scikit-learn " - "transformer, missing %s method" % attr + f"transformer, missing {attr} method" ) if not isinstance(average, bool): raise ValueError( - "average parameter must be of bool type, got " "%s instead" % type(bool) + "average parameter must be of bool type, got " f"{type(bool)} instead" ) self.estimator = estimator @@ -859,7 +859,7 @@ def __init__( if not isinstance(self.n_jobs, int) and self.n_jobs == "cuda": raise ValueError( - 'n_jobs must be int or "cuda", got %s instead.' % type(self.n_jobs) + f'n_jobs must be int or "cuda", got {type(self.n_jobs)} instead.' ) def fit(self, X, y=None): @@ -899,7 +899,7 @@ def transform(self, X): if X.ndim > 3: raise ValueError( "Array must be of at max 3 dimensions instead " - "got %s dimensional matrix" % (X.ndim) + f"got {X.ndim} dimensional matrix" ) shape = X.shape diff --git a/mne/dipole.py b/mne/dipole.py index 008f394f0ea..9f31b3f2e1e 100644 --- a/mne/dipole.py +++ b/mne/dipole.py @@ -146,10 +146,10 @@ def __init__( self.nfree = np.array(nfree) if nfree is not None else None def __repr__(self): # noqa: D105 - s = "n_times : %s" % len(self.times) - s += ", tmin : %0.3f" % np.min(self.times) - s += ", tmax : %0.3f" % np.max(self.times) - return "" % s + s = f"n_times : {len(self.times)}" + s += f", tmin : {np.min(self.times):0.3f}" + s += f", tmax : {np.max(self.times):0.3f}" + return f"" @verbose def save(self, fname, overwrite=False, *, verbose=None): @@ -435,7 +435,7 @@ def __len__(self): def _read_dipole_fixed(fname): """Read a fixed dipole FIF file.""" - logger.info("Reading %s ..." % fname) + logger.info(f"Reading {fname} ...") info, nave, aspect_kind, comment, times, data, _ = _read_evoked(fname) return DipoleFixed(info, data, times, nave, aspect_kind, comment=comment) @@ -494,10 +494,10 @@ def __init__( self._update_first_last() def __repr__(self): # noqa: D105 - s = "n_times : %s" % len(self.times) - s += ", tmin : %s" % np.min(self.times) - s += ", tmax : %s" % np.max(self.times) - return "" % s + s = f"n_times : {len(self.times)}" + s += f", tmin : {np.min(self.times)}" + s += f", tmax : {np.max(self.times)}" + return f"" def copy(self): """Copy the DipoleFixed object. @@ -781,9 +781,7 @@ def _write_dipole_text(fname, dip): fid.write((header + "\n").encode("utf-8")) np.savetxt(fid, out, fmt=fmt) if dip.name is not None: - fid.write( - ('## Name "%s dipoles" Style "Dipoles"' % dip.name).encode("utf-8") - ) + fid.write((f'## Name "{dip.name} dipoles" Style "Dipoles"').encode()) _BDIP_ERROR_KEYS = ("depth", "long", "trans", "qlong", "qtrans") @@ -1258,7 +1256,7 @@ def _fit_dipole( # Find a good starting point (find_best_guess in C) B2 = np.dot(B, B) if B2 == 0: - warn("Zero field found for time %s" % t) + warn(f"Zero field found for time {t}") return np.zeros(3), 0, np.zeros(3), 0, B idx = np.argmin( @@ -1353,7 +1351,7 @@ def _fit_dipole_fixed( B = np.dot(whitener, B_orig) B2 = np.dot(B, B) if B2 == 0: - warn("Zero field found for time %s" % t) + warn(f"Zero field found for time {t}") return np.zeros(3), 0, np.zeros(3), 0, np.zeros(6) # Compute the dipole moment Q, gof, residual_noproj = _fit_Q( @@ -1476,9 +1474,9 @@ def fit_dipole( # Determine if a list of projectors has an average EEG ref if _needs_eeg_average_ref_proj(evoked.info): - raise ValueError("EEG average reference is mandatory for dipole " "fitting.") + raise ValueError("EEG average reference is mandatory for dipole fitting.") if min_dist < 0: - raise ValueError("min_dist should be positive. Got %s" % min_dist) + raise ValueError(f"min_dist should be positive. Got {min_dist}") if ori is not None and pos is None: raise ValueError("pos must be provided if ori is not None") @@ -1499,9 +1497,9 @@ def fit_dipole( bem_extra = bem else: bem_extra = repr(bem) - logger.info("BEM : %s" % bem_extra) + logger.info(f"BEM : {bem_extra}") mri_head_t, trans = _get_trans(trans) - logger.info("MRI transform : %s" % trans) + logger.info(f"MRI transform : {trans}") safe_false = _verbose_safe_false() bem = _setup_bem(bem, bem_extra, neeg, mri_head_t, verbose=safe_false) if not bem["is_sphere"]: diff --git a/mne/epochs.py b/mne/epochs.py index d49c5792fa3..c893171612c 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -797,10 +797,7 @@ def _reject_setup(self, reject, flat, *, allow_callable=False): reject = deepcopy(reject) if reject is not None else dict() flat = deepcopy(flat) if flat is not None else dict() for rej, kind in zip((reject, flat), ("reject", "flat")): - if not isinstance(rej, dict): - raise TypeError( - "reject and flat must be dict or None, not %s" % type(rej) - ) + _validate_type(rej, dict, kind) bads = set(rej.keys()) - set(idx.keys()) if len(bads) > 0: raise KeyError(f"Unknown channel types found in {kind}: {bads}") @@ -1034,11 +1031,11 @@ def subtract_evoked(self, evoked=None): bad_str = ", ".join([diff_ch[ii] for ii in bad_idx]) raise ValueError( "The following data channels are missing " - "in the evoked response: %s" % bad_str + f"in the evoked response: {bad_str}" ) logger.info( - " The following channels are not included in the " - "subtraction: %s" % ", ".join(diff_ch) + " The following channels are not included in the subtraction: " + + ", ".join(diff_ch) ) # make sure the times match @@ -1047,7 +1044,7 @@ def subtract_evoked(self, evoked=None): or np.max(np.abs(self.times - evoked.times)) >= 1e-7 ): raise ValueError( - "Epochs and Evoked object do not contain " "the same time points." + "Epochs and Evoked object do not contain the same time points." ) # handle SSPs @@ -1147,7 +1144,7 @@ def _compute_aggregate(self, picks, mode="mean"): check_ICA = [x.startswith("ICA") for x in self.ch_names] if np.all(check_ICA): raise TypeError( - "picks must be specified (i.e. not None) for " "ICA channel data" + "picks must be specified (i.e. not None) for ICA channel data" ) elif np.any(check_ICA): warn( @@ -2467,7 +2464,7 @@ def equalize_event_counts(self, event_ids=None, method="mintime"): elif len({sub_id in ids for sub_id in id_}) != 1: err = ( "Don't mix hierarchical and regular event_ids" - " like in '%s'." % ", ".join(id_) + f" like in '{', '.join(id_)}'." ) raise ValueError(err) @@ -3762,9 +3759,7 @@ def __init__( len(events) != np.isin(self.events[:, 2], list(self.event_id.values())).sum() ): - raise ValueError( - "The events must only contain event numbers from " "event_id" - ) + raise ValueError("The events must only contain event numbers from event_id") detrend_picks = self._detrend_picks for e in self._data: # This is safe without assignment b/c there is no decim @@ -4270,7 +4265,7 @@ def __init__(self, fname, proj=True, preload=True, verbose=None): raw = list() for fname in fnames: fname_rep = _get_fname_rep(fname) - logger.info("Reading %s ..." % fname_rep) + logger.info(f"Reading {fname_rep} ...") fid, tree, _ = fiff_open(fname, preload=preload) next_fname = _get_next_fname(fid, fname, tree) ( @@ -4863,7 +4858,7 @@ def average_movements( trans = np.vstack( [np.hstack([rot[use_idx], trn[[use_idx]].T]), [[0.0, 0.0, 0.0, 1.0]]] ) - loc_str = ", ".join("%0.1f" % tr for tr in (trans[:3, 3] * 1000)) + loc_str = ", ".join(f"{tr:0.1f}" for tr in (trans[:3, 3] * 1000)) if last_trans is None or not np.allclose(last_trans, trans): logger.info( f" Processing epoch {ei + 1} (device location: {loc_str} mm)" diff --git a/mne/event.py b/mne/event.py index a79ea13dbcc..482f484d6c6 100644 --- a/mne/event.py +++ b/mne/event.py @@ -824,7 +824,7 @@ def _mask_trigs(events, mask, mask_type): elif mask_type != "and": raise ValueError( "'mask_type' should be either 'and'" - " or 'not_and', instead of '%s'" % mask_type + f" or 'not_and', instead of '{mask_type}'" ) events[:, 1:] = np.bitwise_and(events[:, 1:], mask) events = events[events[:, 1] != events[:, 2]] @@ -995,7 +995,7 @@ def make_fixed_length_events( n_events = len(ts) if n_events == 0: raise ValueError( - "No events produced, check the values of start, " "stop, and duration" + "No events produced, check the values of start, stop, and duration" ) events = np.c_[ts, np.zeros(n_events, dtype=int), id * np.ones(n_events, dtype=int)] return events diff --git a/mne/evoked.py b/mne/evoked.py index 461fdb61ba6..f01eb6b4dc5 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -461,7 +461,7 @@ def __repr__(self): # noqa: D105 on_baseline_outside_data="adjust", ): s += " (baseline period was cropped after baseline correction)" - s += ", %s ch" % self.data.shape[0] + s += f", {self.data.shape[0]} ch" s += f", ~{sizeof_fmt(self._size)}" return f"" @@ -1505,7 +1505,7 @@ def _get_entries(fid, evoked_node, allow_maxshield=False): aspect_kinds = np.atleast_1d(aspect_kinds) if len(comments) != len(aspect_kinds) or len(comments) == 0: fid.close() - raise ValueError("Dataset names in FIF file " "could not be found.") + raise ValueError("Dataset names in FIF file could not be found.") t = [_aspect_rev[a] for a in aspect_kinds] t = ['"' + c + '" (' + tt + ")" for tt, c in zip(t, comments)] t = "\n".join(t) @@ -1712,7 +1712,7 @@ def read_evokeds( """ fname = str(_check_fname(fname, overwrite="read", must_exist=True)) check_fname(fname, "evoked", ("-ave.fif", "-ave.fif.gz", "_ave.fif", "_ave.fif.gz")) - logger.info("Reading %s ..." % fname) + logger.info(f"Reading {fname} ...") return_list = True if condition is None: evoked_node = _get_evoked_node(fname) @@ -1853,7 +1853,7 @@ def _read_evoked(fname, condition=None, kind="average", allow_maxshield=False): if nchan > 0: if chs is None: raise ValueError( - "Local channel information was not found " "when it was expected." + "Local channel information was not found when it was expected." ) if len(chs) != nchan: @@ -1866,7 +1866,7 @@ def _read_evoked(fname, condition=None, kind="average", allow_maxshield=False): info["chs"] = chs info["bads"][:] = _rename_list(info["bads"], ch_names_mapping) logger.info( - " Found channel information in evoked data. " "nchan = %d" % nchan + f" Found channel information in evoked data. nchan = {nchan}" ) if sfreq > 0: info["sfreq"] = sfreq diff --git a/mne/export/_export.py b/mne/export/_export.py index aed7e44e0c8..684de220c7f 100644 --- a/mne/export/_export.py +++ b/mne/export/_export.py @@ -206,7 +206,7 @@ def _infer_check_export_fmt(fmt, fname, supported_formats): ) # default to original fmt for raising error later else: raise ValueError( - f"Couldn't infer format from filename {fname}" " (no extension found)" + f"Couldn't infer format from filename {fname} (no extension found)" ) if fmt not in supported_formats: diff --git a/mne/filter.py b/mne/filter.py index 82b77a17a7c..d78987fdcdd 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -317,7 +317,7 @@ def _overlap_add_filter( if len(h) == 1: return x * h**2 if phase == "zero-double" else x * h n_edge = max(min(len(h), x.shape[1]) - 1, 0) - logger.debug("Smart-padding with: %s samples on each edge" % n_edge) + logger.debug(f"Smart-padding with: {n_edge} samples on each edge") n_x = x.shape[1] + 2 * n_edge if phase == "zero-double": @@ -346,7 +346,7 @@ def _overlap_add_filter( else: # Use only a single block n_fft = next_fast_len(min_fft) - logger.debug("FFT block length: %s" % n_fft) + logger.debug(f"FFT block length: {n_fft}") if n_fft < min_fft: raise ValueError( f"n_fft is too short, has to be at least 2 * len(h) - 1 ({min_fft}), got " @@ -801,7 +801,7 @@ def construct_iir_filter( "elliptic", ) if not isinstance(iir_params, dict): - raise TypeError("iir_params must be a dict, got %s" % type(iir_params)) + raise TypeError(f"iir_params must be a dict, got {type(iir_params)}") # if the filter has been designed, we're good to go Wp = None if "sos" in iir_params: @@ -1539,7 +1539,7 @@ def notch_filter( if freqs is not None: freqs = np.atleast_1d(freqs) elif method != "spectrum_fit": - raise ValueError("freqs=None can only be used with method " "spectrum_fit") + raise ValueError("freqs=None can only be used with method spectrum_fit") # Only have to deal with notch_widths for non-autodetect if freqs is not None: @@ -1553,7 +1553,7 @@ def notch_filter( notch_widths = notch_widths[0] * np.ones_like(freqs) elif len(notch_widths) != len(freqs): raise ValueError( - "notch_widths must be None, scalar, or the " "same length as freqs" + "notch_widths must be None, scalar, or the same length as freqs" ) if method in ("fir", "iir"): @@ -2121,7 +2121,7 @@ def _to_samples(filter_length, sfreq, phase, fir_design): err_msg = ( "filter_length, if a string, must be a " 'human-readable time, e.g. "10s", or "auto", not ' - '"%s"' % filter_length + f'"{filter_length}"' ) if filter_length.lower().endswith("ms"): mult_fact = 1e-3 @@ -2280,7 +2280,7 @@ def float_array(c): if h_trans_bandwidth != "auto": raise ValueError( 'h_trans_bandwidth must be "auto" if ' - 'string, got "%s"' % h_trans_bandwidth + f'string, got "{h_trans_bandwidth}"' ) h_trans_bandwidth = np.minimum( np.maximum(0.25 * h_freq, 2.0), sfreq / 2.0 - h_freq @@ -2360,7 +2360,7 @@ def float_array(c): "distortion is likely. Reduce filter length or filter a longer signal." ) - logger.debug("Using filter length: %s" % filter_length) + logger.debug(f"Using filter length: {filter_length}") return ( x, sfreq, @@ -2903,7 +2903,7 @@ def design_mne_c_filter( start = h_start - h_width + 1 stop = start + 2 * h_width - 1 if start < 0 or stop >= n_freqs: - raise RuntimeError("h_freq too high or h_trans_bandwidth too " "large") + raise RuntimeError("h_freq too high or h_trans_bandwidth too large") k = np.arange(-h_width + 1, h_width) / float(h_width) + 1.0 freq_resp[start:stop] *= np.cos(np.pi / 4.0 * k) ** 2 freq_resp[stop:] = 0.0 diff --git a/mne/fixes.py b/mne/fixes.py index c5410476246..269b15d5582 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -383,7 +383,7 @@ def empirical_covariance(X, assume_centered=False): if X.shape[0] == 1: warnings.warn( - "Only one sample available. " "You may want to reshape your data array" + "Only one sample available. You may want to reshape your data array" ) if assume_centered: @@ -648,7 +648,7 @@ def _assess_dimension_(spectrum, rank, n_samples, n_features): from scipy.special import gammaln if rank > len(spectrum): - raise ValueError("The tested rank cannot exceed the rank of the" " dataset") + raise ValueError("The tested rank cannot exceed the rank of the dataset") pu = -rank * log(2.0) for i in range(rank): @@ -860,7 +860,7 @@ def minimum_phase(h, method="homomorphic", n_fft=None, *, half=True): n_fft = 2 ** int(np.ceil(np.log2(2 * (len(h) - 1) / 0.01))) n_fft = int(n_fft) if n_fft < len(h): - raise ValueError("n_fft must be at least len(h)==%s" % len(h)) + raise ValueError(f"n_fft must be at least len(h)=={len(h)}") # zero-pad; calculate the DFT h_temp = np.abs(fft(h, n_fft)) diff --git a/mne/forward/_compute_forward.py b/mne/forward/_compute_forward.py index 641f315239a..db62bf60152 100644 --- a/mne/forward/_compute_forward.py +++ b/mne/forward/_compute_forward.py @@ -57,7 +57,7 @@ def _check_coil_frame(coils, coord_frame, bem): # Make a transformed duplicate coils, coord_Frame = _dup_coil_set(coils, coord_frame, bem["head_mri_t"]) else: - raise RuntimeError("Bad coil coordinate frame %s" % coord_frame) + raise RuntimeError(f"Bad coil coordinate frame {coord_frame}") return coils, coord_frame diff --git a/mne/forward/_lead_dots.py b/mne/forward/_lead_dots.py index 3b2118de409..504f352bd7a 100644 --- a/mne/forward/_lead_dots.py +++ b/mne/forward/_lead_dots.py @@ -79,14 +79,14 @@ def _get_legen_table( extra_str = "" lut_shape = (n_interp + 1, n_coeff) if not op.isfile(fname) or force_calc: - logger.info("Generating Legendre%s table..." % extra_str) + logger.info(f"Generating Legendre{extra_str} table...") x_interp = np.linspace(-1, 1, n_interp + 1) lut = leg_fun(x_interp, n_coeff).astype(np.float32) if not force_calc: with open(fname, "wb") as fid: fid.write(lut.tobytes()) else: - logger.info("Reading Legendre%s table..." % extra_str) + logger.info(f"Reading Legendre{extra_str} table...") with open(fname, "rb", buffering=0) as fid: lut = np.fromfile(fid, np.float32) lut.shape = lut_shape diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index 24131ad4a10..58c4c21ea89 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -156,7 +156,7 @@ def _create_meg_coil(coilset, ch, acc, do_es): """Create a coil definition using templates, transform if necessary.""" # Also change the coordinate frame if so desired if ch["kind"] not in [FIFF.FIFFV_MEG_CH, FIFF.FIFFV_REF_MEG_CH]: - raise RuntimeError("%s is not a MEG channel" % ch["ch_name"]) + raise RuntimeError(f"{ch['ch_name']} is not a MEG channel") # Simple linear search from the coil definitions for coil in coilset: @@ -204,8 +204,8 @@ def _create_eeg_el(ch, t=None): """Create an electrode definition, transform coords if necessary.""" if ch["kind"] != FIFF.FIFFV_EEG_CH: raise RuntimeError( - "%s is not an EEG channel. Cannot create an " - "electrode definition." % ch["ch_name"] + f"{ch['ch_name']} is not an EEG channel. Cannot create an electrode " + "definition." ) if t is None: t = Transform("head", "head") # identity, no change @@ -284,7 +284,7 @@ def _setup_bem(bem, bem_extra, neeg, mri_head_t, allow_none=False, verbose=None) logger.info("") _validate_type(bem, ("path-like", ConductorModel), bem) if not isinstance(bem, ConductorModel): - logger.info("Setting up the BEM model using %s...\n" % bem_extra) + logger.info(f"Setting up the BEM model using {bem_extra}...\n") bem = read_bem_solution(bem) else: bem = bem.copy() @@ -292,7 +292,7 @@ def _setup_bem(bem, bem_extra, neeg, mri_head_t, allow_none=False, verbose=None) logger.info("Using the sphere model.\n") if len(bem["layers"]) == 0 and neeg > 0: raise RuntimeError( - "Spherical model has zero shells, cannot use " "with EEG data" + "Spherical model has zero shells, cannot use with EEG data" ) if bem["coord_frame"] != FIFF.FIFFV_COORD_HEAD: raise RuntimeError("Spherical model is not in head coordinates") @@ -308,12 +308,10 @@ def _setup_bem(bem, bem_extra, neeg, mri_head_t, allow_none=False, verbose=None) "for EEG forward calculations, consider " "using a 3-layer BEM instead" ) - logger.info( - "Employing the head->MRI coordinate transform with the " "BEM model." - ) + logger.info("Employing the head->MRI coordinate transform with the BEM model.") # fwd_bem_set_head_mri_t: Set the coordinate transformation bem["head_mri_t"] = _ensure_trans(mri_head_t, "head", "mri") - logger.info("BEM model %s is now set up" % op.split(bem_extra)[1]) + logger.info(f"BEM model {op.split(bem_extra)[1]} is now set up") logger.info("") return bem @@ -486,7 +484,7 @@ def _prepare_for_forward( # make a new dict with the relevant information arg_list = [info_extra, trans, src, bem_extra, meg, eeg, mindist, n_jobs, verbose] - cmd = "make_forward_solution(%s)" % (", ".join([str(a) for a in arg_list])) + cmd = f"make_forward_solution({', '.join(str(a) for a in arg_list)})" mri_id = dict(machid=np.zeros(2, np.int32), version=0, secs=0, usecs=0) info_trans = str(trans) if isinstance(trans, Path) else trans @@ -527,7 +525,7 @@ def _prepare_for_forward( for s in src: transform_surface_to(s, "head", mri_head_t) logger.info( - "Source spaces are now in %s coordinates." % _coord_frame_name(s["coord_frame"]) + f"Source spaces are now in {_coord_frame_name(s['coord_frame'])} coordinates." ) # Prepare the BEM model @@ -689,14 +687,14 @@ def make_forward_solution( info_extra = "instance of Info" # Report the setup - logger.info("Source space : %s" % src) - logger.info("MRI -> head transform : %s" % trans) - logger.info("Measurement data : %s" % info_extra) + logger.info(f"Source space : {src}") + logger.info(f"MRI -> head transform : {trans}") + logger.info(f"Measurement data : {info_extra}") if isinstance(bem, ConductorModel) and bem["is_sphere"]: logger.info(f"Sphere model : origin at {bem['r0']} mm") logger.info("Standard field computations") else: - logger.info("Conductor model : %s" % bem_extra) + logger.info(f"Conductor model : {bem_extra}") logger.info("Accurate field computations") logger.info( "Do computations in %s coordinates", _coord_frame_name(FIFF.FIFFV_COORD_HEAD) diff --git a/mne/forward/forward.py b/mne/forward/forward.py index ebe005787fb..96ae003b805 100644 --- a/mne/forward/forward.py +++ b/mne/forward/forward.py @@ -375,7 +375,7 @@ def _read_one(fid, node): one["sol_grad"]["data"].shape[1] != 3 * one["nsource"] and one["sol_grad"]["data"].shape[1] != 3 * 3 * one["nsource"] ): - raise ValueError("Forward solution gradient matrix has " "wrong dimensions") + raise ValueError("Forward solution gradient matrix has wrong dimensions") return one @@ -572,7 +572,7 @@ def read_forward_solution(fname, include=(), exclude=(), *, ordered=True, verbos ) fname = _check_fname(fname=fname, must_exist=True, overwrite="read") # Open the file, create directory - logger.info("Reading forward solution from %s..." % fname) + logger.info(f"Reading forward solution from {fname}...") if fname.suffix == ".h5": return _read_forward_hdf5(fname) f, tree, _ = fiff_open(fname) @@ -580,12 +580,12 @@ def read_forward_solution(fname, include=(), exclude=(), *, ordered=True, verbos # Find all forward solutions fwds = dir_tree_find(tree, FIFF.FIFFB_MNE_FORWARD_SOLUTION) if len(fwds) == 0: - raise ValueError("No forward solutions in %s" % fname) + raise ValueError(f"No forward solutions in {fname}") # Parent MRI data parent_mri = dir_tree_find(tree, FIFF.FIFFB_MNE_PARENT_MRI_FILE) if len(parent_mri) == 0: - raise ValueError("No parent MRI information in %s" % fname) + raise ValueError(f"No parent MRI information in {fname}") parent_mri = parent_mri[0] src = _read_source_spaces_from_tree(fid, tree, patch_stats=False) @@ -600,9 +600,7 @@ def read_forward_solution(fname, include=(), exclude=(), *, ordered=True, verbos for k in range(len(fwds)): tag = find_tag(fid, fwds[k], FIFF.FIFF_MNE_INCLUDED_METHODS) if tag is None: - raise ValueError( - "Methods not listed for one of the forward " "solutions" - ) + raise ValueError("Methods not listed for one of the forward solutions") if tag.data == FIFF.FIFFV_MNE_MEG: megnode = fwds[k] @@ -656,7 +654,7 @@ def read_forward_solution(fname, include=(), exclude=(), *, ordered=True, verbos or mri_head_t["to"] != FIFF.FIFFV_COORD_HEAD ): fid.close() - raise ValueError("MRI/head coordinate transformation not " "found") + raise ValueError("MRI/head coordinate transformation not found") fwd["mri_head_t"] = mri_head_t # @@ -696,7 +694,7 @@ def read_forward_solution(fname, include=(), exclude=(), *, ordered=True, verbos try: s = transform_surface_to(s, fwd["coord_frame"], mri_head_t) except Exception as inst: - raise ValueError("Could not transform source space (%s)" % inst) + raise ValueError(f"Could not transform source space ({inst})") nuse += s["nuse"] @@ -705,7 +703,7 @@ def read_forward_solution(fname, include=(), exclude=(), *, ordered=True, verbos raise ValueError("Source spaces do not match the forward solution.") logger.info( - " Source spaces transformed to the forward solution " "coordinate frame" + " Source spaces transformed to the forward solution coordinate frame" ) fwd["src"] = src @@ -966,7 +964,7 @@ def _write_forward_solution(fid, fwd): # usually MRI s = transform_surface_to(s, fwd["mri_head_t"]["from"], fwd["mri_head_t"]) except Exception as inst: - raise ValueError("Could not transform source space (%s)" % inst) + raise ValueError(f"Could not transform source space ({inst})") src.append(s) # @@ -1466,7 +1464,7 @@ def compute_depth_prior( if not is_fixed_ori and combine_xyz is False: patch_areas = np.repeat(patch_areas, 3) d /= patch_areas**2 - logger.info(" Patch areas taken into account in the depth " "weighting") + logger.info(" Patch areas taken into account in the depth weighting") w = 1.0 / d if limit is not None: @@ -1532,7 +1530,7 @@ def _stc_src_sel( n_stc = sum(len(v) for v in vertices) n_joint = len(src_sel) if n_joint != n_stc: - msg = "Only %i of %i SourceEstimate %s found in " "source space%s" % ( + msg = "Only %i of %i SourceEstimate %s found in source space%s" % ( n_joint, n_stc, "vertex" if n_stc == 1 else "vertices", @@ -1671,8 +1669,8 @@ def apply_forward( for ch_name in fwd["sol"]["row_names"]: if ch_name not in info["ch_names"]: raise ValueError( - "Channel %s of forward operator not present in " - "evoked_template." % ch_name + f"Channel {ch_name} of forward operator not present in " + "evoked_template." ) # project the source estimate to the sensor space @@ -1747,7 +1745,7 @@ def apply_forward_raw( for ch_name in fwd["sol"]["row_names"]: if ch_name not in info["ch_names"]: raise ValueError( - "Channel %s of forward operator not present in " "info." % ch_name + f"Channel {ch_name} of forward operator not present in info." ) # project the source estimate to the sensor space @@ -2042,7 +2040,7 @@ def _do_forward_solution( raise ValueError('mindist, if string, must be "all"') mindist = ["--all"] else: - mindist = ["--mindist", "%g" % mindist] + mindist = ["--mindist", f"{mindist:g}"] # src, spacing, bem for element, name, kind in zip( @@ -2051,7 +2049,7 @@ def _do_forward_solution( ("path-like", "str", "path-like"), ): if element is not None: - _validate_type(element, kind, name, "%s or None" % kind) + _validate_type(element, kind, name, f"{kind} or None") # put together the actual call cmd = [ @@ -2074,7 +2072,7 @@ def _do_forward_solution( # allow both "ico4" and "ico-4" style values match = re.match(r"(oct|ico)-?(\d+)$", spacing) if match is None: - raise ValueError("Invalid spacing parameter: %r" % spacing) + raise ValueError(f"Invalid spacing parameter: {spacing!r}") spacing = "-".join(match.groups()) cmd += ["--spacing", spacing] if mindist is not None: @@ -2082,9 +2080,9 @@ def _do_forward_solution( if bem is not None: cmd += ["--bem", bem] if mri is not None: - cmd += ["--mri", "%s" % str(mri.absolute())] + cmd += ["--mri", f"{mri.absolute()}"] if trans is not None: - cmd += ["--trans", "%s" % str(trans.absolute())] + cmd += ["--trans", f"{trans.absolute()}"] if not meg: cmd.append("--eegonly") if not eeg: @@ -2105,7 +2103,7 @@ def _do_forward_solution( try: logger.info( "Running forward solution generation command with " - "subjects_dir %s" % subjects_dir + f"subjects_dir {subjects_dir}" ) run_subprocess(cmd, env=env) except Exception: diff --git a/mne/gui/_coreg.py b/mne/gui/_coreg.py index 983b4b5b067..226bbbaa350 100644 --- a/mne/gui/_coreg.py +++ b/mne/gui/_coreg.py @@ -1521,7 +1521,7 @@ def _save_subject_callback(self, overwrite=False): except Exception: logger.error(f"Error computing {bem_name} solution") else: - self._display_message(f"Computing {bem_name} solution..." " Done!") + self._display_message(f"Computing {bem_name} solution... Done!") self._display_message(f"Saving {self._subject_to}... Done!") self._renderer._window_set_cursor(default_cursor) self._mri_scale_modified = False @@ -1704,7 +1704,7 @@ def _configure_dock(self): desc="Load", func=self._set_info_file, icon=True, - tooltip="Load the FIFF file with digitization data for " "coregistration", + tooltip="Load the FIFF file with digitization data for coregistration", layout=info_file_layout, ) self._renderer._layout_add_widget( @@ -1891,7 +1891,7 @@ def _configure_dock(self): self._widgets["fit_icp"] = self._renderer._dock_add_button( name="Fit ICP", callback=self._fit_icp, - tooltip="Find rotation and translation to match the " "head shape points", + tooltip="Find rotation and translation to match the head shape points", layout=fit_layout, ) self._renderer._layout_add_widget(param_layout, fit_layout) diff --git a/mne/inverse_sparse/_gamma_map.py b/mne/inverse_sparse/_gamma_map.py index 6da9f9d2cc3..689222b9c95 100644 --- a/mne/inverse_sparse/_gamma_map.py +++ b/mne/inverse_sparse/_gamma_map.py @@ -81,7 +81,7 @@ def _gamma_map_opt( if n_sources % group_size != 0: raise ValueError( - "Number of sources has to be evenly dividable by the " "group size" + "Number of sources has to be evenly dividable by the group size" ) n_active = n_sources diff --git a/mne/inverse_sparse/mxne_inverse.py b/mne/inverse_sparse/mxne_inverse.py index 703a0d30ca4..cb49deaa213 100644 --- a/mne/inverse_sparse/mxne_inverse.py +++ b/mne/inverse_sparse/mxne_inverse.py @@ -312,7 +312,7 @@ def make_stc_from_dipoles(dipoles, src, verbose=None): raise ValueError( "Dipoles must be an instance of Dipole or " "a list of instances of Dipole. " - "Got %s!" % type(dipoles) + f"Got {type(dipoles)}!" ) tmin = dipoles[0].times[0] tstep = dipoles[0].times[1] - tmin @@ -324,7 +324,7 @@ def make_stc_from_dipoles(dipoles, src, verbose=None): for i in range(len(dipoles)): if not np.all(dipoles[i].pos == dipoles[i].pos[0]): raise ValueError( - "Only dipoles with fixed position over time " "are supported!" + "Only dipoles with fixed position over time are supported!" ) X[i] = dipoles[i].amplitude idx = np.all(source_rr == dipoles[i].pos[0], axis=1) @@ -460,8 +460,7 @@ def mixed_norm( _check_option("alpha", alpha, ("sure",)) elif not 0.0 <= alpha < 100: raise ValueError( - 'If not equal to "sure" alpha must be in [0, 100). ' - "Got alpha = %s" % alpha + 'If not equal to "sure" alpha must be in [0, 100). ' f"Got alpha = {alpha}" ) if n_mxne_iter < 1: raise ValueError( @@ -470,21 +469,21 @@ def mixed_norm( ) if dgap_freq <= 0.0: raise ValueError( - "dgap_freq must be a positive integer." " Got dgap_freq = %s" % dgap_freq + f"dgap_freq must be a positive integer. Got dgap_freq = {dgap_freq}" ) if not ( isinstance(sure_alpha_grid, (np.ndarray, list)) or sure_alpha_grid == "auto" ): raise ValueError( 'If not equal to "auto" sure_alpha_grid must be an ' - "array. Got %s" % type(sure_alpha_grid) + f"array. Got {type(sure_alpha_grid)}" ) if (isinstance(sure_alpha_grid, str) and sure_alpha_grid != "auto") and ( isinstance(alpha, str) and alpha != "sure" ): raise Exception( "If sure_alpha_grid is manually specified, alpha must " - 'be "sure". Got %s' % alpha + f'be "sure". Got {alpha}' ) pca = True if not isinstance(evoked, list): @@ -553,7 +552,7 @@ def mixed_norm( dgap_freq=dgap_freq, verbose=verbose, ) - logger.info("Selected alpha: %s" % best_alpha_) + logger.info(f"Selected alpha: {best_alpha_}") else: if n_mxne_iter == 1: X, active_set, E = mixed_norm_solver( @@ -786,24 +785,22 @@ def tf_mixed_norm( info = evoked.info if not (0.0 <= alpha < 100.0): - raise ValueError("alpha must be in [0, 100). " "Got alpha = %s" % alpha) + raise ValueError(f"alpha must be in [0, 100). Got alpha = {alpha}") if not (0.0 <= l1_ratio <= 1.0): - raise ValueError( - "l1_ratio must be in range [0, 1]." " Got l1_ratio = %s" % l1_ratio - ) + raise ValueError(f"l1_ratio must be in range [0, 1]. Got l1_ratio = {l1_ratio}") alpha_space = alpha * (1.0 - l1_ratio) alpha_time = alpha * l1_ratio if n_tfmxne_iter < 1: raise ValueError( "TF-MxNE has to be computed at least 1 time. " - "Requires n_tfmxne_iter >= 1, got %s" % n_tfmxne_iter + f"Requires n_tfmxne_iter >= 1, got {n_tfmxne_iter}" ) if dgap_freq <= 0.0: raise ValueError( - "dgap_freq must be a positive integer." " Got dgap_freq = %s" % dgap_freq + f"dgap_freq must be a positive integer. Got dgap_freq = {dgap_freq}" ) tstep = np.atleast_1d(tstep) @@ -876,9 +873,7 @@ def tf_mixed_norm( ) if active_set.sum() == 0: - raise Exception( - "No active dipoles found. " "alpha_space/alpha_time are too big." - ) + raise Exception("No active dipoles found. alpha_space/alpha_time are too big.") # Compute estimated whitened sensor data for each dipole (dip, ch, time) gain_active = gain[:, active_set] @@ -1036,7 +1031,7 @@ def _fit_on_grid(gain, M, eps, delta): # warm start - first iteration (leverages convexity) logger.info("Warm starting...") for j, alpha in enumerate(alpha_grid): - logger.info("alpha: %s" % alpha) + logger.info(f"alpha: {alpha}") X, a_set = _run_solver(alpha, M, 1) X_eps, a_set_eps = _run_solver(alpha, M_eps, 1) coefs_grid_1_0[j][a_set, :] = X @@ -1051,7 +1046,7 @@ def _fit_on_grid(gain, M, eps, delta): coefs_grid_2 = coefs_grid_2_0.copy() logger.info("Fitting SURE on grid.") for j, alpha in enumerate(alpha_grid): - logger.info("alpha: %s" % alpha) + logger.info(f"alpha: {alpha}") if active_sets[j].sum() > 0: w = gprime(coefs_grid_1[j]) X, a_set = _run_solver(alpha, M, n_mxne_iter - 1, w_init=w) diff --git a/mne/inverse_sparse/mxne_optim.py b/mne/inverse_sparse/mxne_optim.py index 03a5fa20df7..5c77b611b51 100644 --- a/mne/inverse_sparse/mxne_optim.py +++ b/mne/inverse_sparse/mxne_optim.py @@ -415,7 +415,7 @@ def mixed_norm_solver( n_positions = n_dipoles // n_orient _, n_times = M.shape alpha_max = norm_l2inf(np.dot(G.T, M), n_orient, copy=False) - logger.info("-- ALPHA MAX : %s" % alpha_max) + logger.info(f"-- ALPHA MAX : {alpha_max}") alpha = float(alpha) X = np.zeros((n_dipoles, n_times), dtype=G.dtype) @@ -756,7 +756,7 @@ def norm(self, z, ord=2): # noqa: A002 """Squared L2 norm if ord == 2 and L1 norm if order == 1.""" if ord not in (1, 2): raise ValueError( - "Only supported norm order are 1 and 2. " "Got ord = %s" % ord + "Only supported norm order are 1 and 2. " f"Got ord = {ord}" ) stft_norm = stft_norm1 if ord == 1 else stft_norm2 norm = 0.0 @@ -1261,7 +1261,7 @@ def _tf_mixed_norm_solver_bcd_active_set( if Z_init is not None: if Z_init.shape != (n_sources, phi.n_coefs.sum()): raise Exception( - "Z_init must be None or an array with shape " "(n_sources, n_coefs)." + "Z_init must be None or an array with shape (n_sources, n_coefs)." ) for ii in range(n_positions): if np.any(Z_init[ii * n_orient : (ii + 1) * n_orient]): diff --git a/mne/inverse_sparse/tests/test_mxne_inverse.py b/mne/inverse_sparse/tests/test_mxne_inverse.py index 639b1daeef8..17a088f5c1e 100644 --- a/mne/inverse_sparse/tests/test_mxne_inverse.py +++ b/mne/inverse_sparse/tests/test_mxne_inverse.py @@ -38,7 +38,7 @@ fname_raw = data_path / "MEG" / "sample" / "sample_audvis_trunc_raw.fif" fname_fwd = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-6-fwd.fif" label = "Aud-rh" -fname_label = data_path / "MEG" / "sample" / "labels" / ("%s.label" % label) +fname_label = data_path / "MEG" / "sample" / "labels" / f"{label}.label" @pytest.fixture(scope="module", params=[testing._pytest_param]) diff --git a/mne/io/array/tests/test_array.py b/mne/io/array/tests/test_array.py index 10b7c834d98..ef0a8a9573c 100644 --- a/mne/io/array/tests/test_array.py +++ b/mne/io/array/tests/test_array.py @@ -36,7 +36,7 @@ def test_long_names(): info = create_info(["a" * 16] * 11, 1000.0, verbose="error") data = np.zeros((11, 1000)) raw = RawArray(data, info) - assert raw.ch_names == ["a" * 16 + "-%s" % ii for ii in range(11)] + assert raw.ch_names == ["a" * 16 + f"-{ii}" for ii in range(11)] def test_array_copy(): diff --git a/mne/io/artemis123/artemis123.py b/mne/io/artemis123/artemis123.py index 99b00d36f45..6c2801e998f 100644 --- a/mne/io/artemis123/artemis123.py +++ b/mne/io/artemis123/artemis123.py @@ -123,8 +123,7 @@ def _get_artemis123_info(fname, pos_fname=None): values = line.strip().split("\t") if len(values) != 7: raise OSError( - "Error parsing line \n\t:%s\n" % line - + "from file %s" % header + f"Error parsing line \n\t:{line}\nfrom file {header}" ) tmp = dict() for k, v in zip(chan_keys, values): @@ -143,9 +142,9 @@ def _get_artemis123_info(fname, pos_fname=None): "Spatial Filter Active?", ]: if header_info[k] != "FALSE": - warn("%s - set to but is not supported" % k) + warn(f"{k} - set to but is not supported") if header_info["filter_hist"]: - warn("Non-Empty Filter history found, BUT is not supported" % k) + warn("Non-Empty Filter history found, BUT is not supported") # build mne info struct info = _empty_info(float(header_info["DAQ Sample Rate"])) @@ -171,7 +170,7 @@ def _get_artemis123_info(fname, pos_fname=None): desc = "" for k in ["Purpose", "Notes"]: desc += f"{k} : {header_info[k]}\n" - desc += "Comments : {}".format(header_info["comments"]) + desc += f"Comments : {header_info['comments']}" info.update( { @@ -256,7 +255,7 @@ def _get_artemis123_info(fname, pos_fname=None): t["kind"] = FIFF.FIFFV_MISC_CH else: raise ValueError( - "Channel does not match expected" + ' channel Types:"%s"' % chan["name"] + f'Channel does not match expected channel Types:"{chan["name"]}"' ) # incorporate multiplier (unit_mul) into calibration @@ -354,7 +353,7 @@ def __init__( ) if not op.exists(input_fname): - raise RuntimeError("%s - Not Found" % input_fname) + raise RuntimeError(f"{input_fname} - Not Found") info, header_info = _get_artemis123_info(input_fname, pos_fname=pos_fname) @@ -377,8 +376,8 @@ def __init__( n_hpis += 1 if n_hpis < 3: warn( - "%d HPIs active. At least 3 needed to perform" % n_hpis - + "head localization\n *NO* head localization performed" + f"{n_hpis:d} HPIs active. At least 3 needed to perform" + "head localization\n *NO* head localization performed" ) else: # Localized HPIs using the 1st 250 milliseconds of data. @@ -409,11 +408,11 @@ def __init__( # only use HPI coils with localizaton goodness_of_fit > 0.98 bad_idx = [] for i, g in enumerate(hpi_g): - msg = "HPI coil %d - location goodness of fit (%0.3f)" + msg = f"HPI coil {i + 1} - location goodness of fit ({g:0.3f})" if g < 0.98: bad_idx.append(i) msg += " *Removed from coregistration*" - logger.info(msg % (i + 1, g)) + logger.info(msg) hpi_dev = np.delete(hpi_dev, bad_idx, axis=0) hpi_g = np.delete(hpi_g, bad_idx, axis=0) @@ -428,12 +427,10 @@ def __init__( ) if len(hpi_head) != len(hpi_dev): - mesg = ( - "number of digitized (%d) and " - + "active (%d) HPI coils are " - + "not the same." + raise RuntimeError( + f"number of digitized ({len(hpi_head)}) and active " + f"({len(hpi_dev)}) HPI coils are not the same." ) - raise RuntimeError(mesg % (len(hpi_head), len(hpi_dev))) # compute initial head to dev transform and hpi ordering head_to_dev_t, order, trans_g = _fit_coil_order_dev_head_trans( @@ -460,10 +457,11 @@ def __init__( tmp_dists = np.abs(dig_dists - dev_dists) dist_limit = tmp_dists.max() * 1.1 - msg = "HPI-Dig corrregsitration\n" - msg += "\tGOF : %0.3f\n" % trans_g - msg += "\tMax Coil Error : %0.3f cm\n" % (100 * tmp_dists.max()) - logger.info(msg) + logger.info( + "HPI-Dig corrregsitration\n" + f"\tGOF : {trans_g:0.3f}\n" + f"\tMax Coil Error : {100 * tmp_dists.max():0.3f} cm\n" + ) else: logger.info("Assuming Cardinal HPIs") @@ -519,9 +517,9 @@ def __init__( if hpi_result["dist_limit"] > 0.005: warn( "Large difference between digitized geometry" - + " and HPI geometry. Max coil to coil difference" - + " is %0.2f cm\n" % (100.0 * tmp_dists.max()) - + "beware of *POOR* head localization" + " and HPI geometry. Max coil to coil difference" + f" is {100.0 * tmp_dists.max():0.2f} cm\n" + "beware of *POOR* head localization" ) # store it diff --git a/mne/io/artemis123/tests/test_artemis123.py b/mne/io/artemis123/tests/test_artemis123.py index 9b002c7b712..c43409664dc 100644 --- a/mne/io/artemis123/tests/test_artemis123.py +++ b/mne/io/artemis123/tests/test_artemis123.py @@ -36,9 +36,9 @@ def _assert_trans(actual, desired, dist_tol=0.017, angle_tol=5.0): angle = np.rad2deg(_angle_between_quats(quat_est, quat)) dist = np.linalg.norm(trans - trans_est) - assert dist <= dist_tol, ( - f"{1000 * dist:0.3f} > {1000 * dist_tol:0.3f} " "mm translation" - ) + assert ( + dist <= dist_tol + ), f"{1000 * dist:0.3f} > {1000 * dist_tol:0.3f} mm translation" assert angle <= angle_tol, f"{angle:0.3f} > {angle_tol:0.3f}° rotation" diff --git a/mne/io/artemis123/utils.py b/mne/io/artemis123/utils.py index 432e593553d..92673c9b04f 100644 --- a/mne/io/artemis123/utils.py +++ b/mne/io/artemis123/utils.py @@ -17,7 +17,7 @@ def _load_mne_locs(fname=None): fname = op.join(resource_dir, "Artemis123_mneLoc.csv") if not op.exists(fname): - raise OSError('MNE locs file "%s" does not exist' % (fname)) + raise OSError(f'MNE locs file "{fname}" does not exist') logger.info(f"Loading mne loc file {fname}") locs = dict() @@ -42,7 +42,7 @@ def _generate_mne_locs_file(output_fname): # write it out to output_fname with open(output_fname, "w") as fid: for n in sorted(locs.keys()): - fid.write("%s," % n) + fid.write(f"{n},") fid.write(",".join(locs[n].astype(str))) fid.write("\n") diff --git a/mne/io/base.py b/mne/io/base.py index ae622cfa307..625eeb54684 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -211,7 +211,7 @@ def __init__( # some functions (e.g., filtering) only work w/64-bit data if preload.dtype not in (np.float64, np.complex128): raise RuntimeError( - "datatype must be float64 or complex128, " "not %s" % preload.dtype + f"datatype must be float64 or complex128, not {preload.dtype}" ) if preload.dtype != dtype: raise ValueError("preload and dtype must match") @@ -223,7 +223,7 @@ def __init__( else: if last_samps is None: raise ValueError( - "last_samps must be given unless preload is " "an ndarray" + "last_samps must be given unless preload is an ndarray" ) if not preload: self.preload = False @@ -781,7 +781,7 @@ def _parse_get_set_params(self, item): if len(item) != 2: # should be channels and time instants raise RuntimeError( - "Unable to access raw data (need both channels " "and time)" + "Unable to access raw data (need both channels and time)" ) sel = _picks_to_idx(self.info, item[0]) @@ -2155,7 +2155,7 @@ def add_events(self, events, stim_channel=None, replace=False): stim_channel = _get_stim_channel(stim_channel, self.info) pick = pick_channels(self.ch_names, stim_channel, ordered=False) if len(pick) == 0: - raise ValueError("Channel %s not found" % stim_channel) + raise ValueError(f"Channel {stim_channel} not found") pick = pick[0] idx = events[:, 0].astype(int) if np.any(idx < self.first_samp) or np.any(idx > self.last_samp): @@ -2576,7 +2576,7 @@ def _get_scaling(ch_type, target_unit): unit_list = target_unit.split("/") if ch_type not in si_units.keys(): raise KeyError( - f"{ch_type} is not a channel type that can be scaled " "from units." + f"{ch_type} is not a channel type that can be scaled from units." ) si_unit_list = si_units_splitted[ch_type] if len(unit_list) != len(si_unit_list): @@ -2843,8 +2843,8 @@ def _write_raw_data( raise ValueError( 'file is larger than "split_size" after writing ' "measurement information, you must use a larger " - "value for split size: %s plus enough bytes for " - "the chosen buffer_size" % pos_prev + f"value for split size: {pos_prev} plus enough bytes for " + "the chosen buffer_size" ) # Check to see if this has acquisition skips and, if so, if we can @@ -2890,7 +2890,7 @@ def _write_raw_data( data = np.dot(projector, data) if drop_small_buffer and (first > start) and (len(times) < buffer_size): - logger.info("Skipping data chunk due to small buffer ... " "[done]") + logger.info("Skipping data chunk due to small buffer ... [done]") break logger.debug(f"Writing FIF {first:6d} ... {last:6d} ...") _write_raw_buffer(fid, data, cals, fmt) @@ -3025,7 +3025,7 @@ def _check_raw_compatibility(raw): a, b = raw[ri].info[key], raw[0].info[key] if a != b: raise ValueError( - f"raw[{ri}].info[{key}] must match:\n" f"{repr(a)} != {repr(b)}" + f"raw[{ri}].info[{key}] must match:\n{repr(a)} != {repr(b)}" ) for kind in ("bads", "ch_names"): set1 = set(raw[0].info[kind]) @@ -3033,7 +3033,7 @@ def _check_raw_compatibility(raw): mismatch = set1.symmetric_difference(set2) if mismatch: raise ValueError( - f"raw[{ri}]['info'][{kind}] do not match: " f"{sorted(mismatch)}" + f"raw[{ri}]['info'][{kind}] do not match: {sorted(mismatch)}" ) if any(raw[ri]._cals != raw[0]._cals): raise ValueError("raw[%d]._cals must match" % ri) @@ -3092,7 +3092,7 @@ def concatenate_raws( if events_list is not None: if len(events_list) != len(raws): raise ValueError( - "`raws` and `event_list` are required " "to be of the same length" + "`raws` and `event_list` are required to be of the same length" ) first, last = zip(*[(r.first_samp, r.last_samp) for r in raws]) events = concatenate_events(events_list, first, last) diff --git a/mne/io/besa/besa.py b/mne/io/besa/besa.py index 7af8a066204..47058688274 100644 --- a/mne/io/besa/besa.py +++ b/mne/io/besa/besa.py @@ -249,7 +249,7 @@ def _read_elp_sidecar(fname): """ fname_elp = fname.parent / (fname.stem + ".elp") if not fname_elp.exists(): - logger.info(f"No {fname_elp} file present containing electrode " "information.") + logger.info(f"No {fname_elp} file present containing electrode information.") return None logger.info(f"Reading electrode names and types from {fname_elp}") @@ -264,7 +264,7 @@ def _read_elp_sidecar(fname): else: # No channel types present logger.info( - "No channel types present in .elp file. Marking all " "channels as EEG." + "No channel types present in .elp file. Marking all channels as EEG." ) for line in lines: ch_name = line.split()[:1] diff --git a/mne/io/boxy/boxy.py b/mne/io/boxy/boxy.py index a3beefc218c..85791349294 100644 --- a/mne/io/boxy/boxy.py +++ b/mne/io/boxy/boxy.py @@ -59,7 +59,7 @@ class RawBOXY(BaseRaw): @verbose def __init__(self, fname, preload=False, verbose=None): - logger.info("Loading %s" % fname) + logger.info(f"Loading {fname}") # Read header file and grab some info. start_line = np.inf @@ -105,8 +105,7 @@ def __init__(self, fname, preload=False, verbose=None): # Check that the BOXY version is supported if boxy_ver not in ["0.40", "0.84"]: raise RuntimeError( - "MNE has not been tested with BOXY " - "version (%s)" % boxy_ver + f"MNE has not been tested with BOXY version ({boxy_ver})" ) elif "Detector Channels" in i_line: raw_extras["detect_num"] = int(i_line.rsplit(" ")[0]) diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 1942744afe3..3fdc0b49715 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -76,7 +76,7 @@ def __init__( verbose=None, ): # noqa: D107 # Channel info and events - logger.info("Extracting parameters from %s..." % vhdr_fname) + logger.info(f"Extracting parameters from {vhdr_fname}...") hdr_fname = op.abspath(vhdr_fname) ext = op.splitext(hdr_fname)[-1] ahdr_format = True if ext == ".ahdr" else False @@ -330,7 +330,7 @@ def _read_annotations_brainvision(fname, sfreq="auto"): # if vhdr file does not exist assume that the format is ahdr if not op.exists(hdr_fname): hdr_fname = op.splitext(fname)[0] + ".ahdr" - logger.info("Finding 'sfreq' from header file: %s" % hdr_fname) + logger.info(f"Finding 'sfreq' from header file: {hdr_fname}") _, _, _, info = _aux_hdr_info(hdr_fname) sfreq = info["sfreq"] @@ -510,7 +510,7 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): if ext not in (".vhdr", ".ahdr"): raise OSError( "The header file must be given to read the data, " - "not a file with extension '%s'." % ext + f"not a file with extension '{ext}'." ) settings, cfg, cinfostr, info = _aux_hdr_info(hdr_fname) @@ -518,14 +518,14 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): order = cfg.get(cinfostr, "DataOrientation") if order not in _orientation_dict: - raise NotImplementedError("Data Orientation %s is not supported" % order) + raise NotImplementedError(f"Data Orientation {order} is not supported") order = _orientation_dict[order] data_format = cfg.get(cinfostr, "DataFormat") if data_format == "BINARY": fmt = cfg.get("Binary Infos", "BinaryFormat") if fmt not in _fmt_dict: - raise NotImplementedError("Datatype %s is not supported" % fmt) + raise NotImplementedError(f"Datatype {fmt} is not supported") fmt = _fmt_dict[fmt] else: if order == "C": # channels in rows @@ -797,8 +797,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): if heterogeneous_hp_filter: warn( "Channels contain different highpass filters. " - "Lowest (weakest) filter setting (%0.2f Hz) " - "will be stored." % info["highpass"] + f"Lowest (weakest) filter setting ({info['highpass']:0.2f} Hz) " + "will be stored." ) if len(lowpass) == 0: diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 9e31bb2d1d6..c65a3865e64 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -538,11 +538,10 @@ def test_brainvision_data(): elif ch["ch_name"] == "ReRef": assert ch["kind"] == FIFF.FIFFV_MISC_CH assert ch["unit"] == FIFF.FIFF_UNIT_CEL - elif ch["ch_name"] in raw_py.info["ch_names"]: + else: + assert ch["ch_name"] in raw_py.info["ch_names"], f"Unknown: {ch['ch_name']}" assert ch["kind"] == FIFF.FIFFV_EEG_CH assert ch["unit"] == FIFF.FIFF_UNIT_V - else: - raise RuntimeError("Unknown Channel: %s" % ch["ch_name"]) # test loading v2 read_raw_brainvision(vhdr_v2_path, eog=eog, preload=True, verbose="error") diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py index 616602892dd..b6a66a3e2f6 100644 --- a/mne/io/bti/bti.py +++ b/mne/io/bti/bti.py @@ -187,7 +187,7 @@ def _check_nan_dev_head_t(dev_ctf_t): has_nan = np.isnan(dev_ctf_t["trans"]) if np.any(has_nan): logger.info( - "Missing values BTI dev->head transform. " "Replacing with identity matrix." + "Missing values BTI dev->head transform. Replacing with identity matrix." ) dev_ctf_t["trans"] = np.identity(4) @@ -330,8 +330,8 @@ def _read_config(fname): if num_channels is None: raise ValueError( - "Cannot find block %s to determine " - "number of channels" % BTI.UB_B_WHC_CHAN_MAP_VER + f"Cannot find block {BTI.UB_B_WHC_CHAN_MAP_VER} to " + "determine number of channels" ) dta["channels"] = list() @@ -355,8 +355,8 @@ def _read_config(fname): if num_subsys is None: raise ValueError( - "Cannot find block %s to determine" - " number of subsystems" % BTI.UB_B_WHS_SUBSYS_VER + f"Cannot find block {BTI.UB_B_WHS_SUBSYS_VER} to determine" + " number of subsystems" ) dta["subsys"] = list() @@ -1164,7 +1164,7 @@ def _make_bti_digitization( ): with info._unlock(): if head_shape_fname: - logger.info("... Reading digitization points from %s" % head_shape_fname) + logger.info(f"... Reading digitization points from {head_shape_fname}") nasion, lpa, rpa, hpi, dig_points = _read_head_shape(head_shape_fname) info["dig"], dev_head_t, ctf_head_t = _make_bti_dig_points( @@ -1217,9 +1217,7 @@ def _get_bti_info( """ if pdf_fname is None: - logger.info( - "No pdf_fname passed, trying to construct partial info " "from config" - ) + logger.info("No pdf_fname passed, trying to construct partial info from config") if pdf_fname is not None and not isinstance(pdf_fname, BytesIO): if not op.isabs(pdf_fname): pdf_fname = op.abspath(pdf_fname) @@ -1236,9 +1234,9 @@ def _get_bti_info( break if not op.isfile(config_fname): raise ValueError( - "Could not find the config file %s. Please check" + f"Could not find the config file {config_fname}. Please check" " whether you are in the right directory " - "or pass the full name" % config_fname + "or pass the full name" ) if head_shape_fname is not None and not isinstance(head_shape_fname, BytesIO): @@ -1248,13 +1246,13 @@ def _get_bti_info( if not op.isfile(head_shape_fname): raise ValueError( - 'Could not find the head_shape file "%s". ' + f'Could not find the head_shape file "{orig_name}". ' "You should check whether you are in the " "right directory, pass the full file name, " - "or pass head_shape_fname=None." % orig_name + "or pass head_shape_fname=None." ) - logger.info("Reading 4D PDF file %s..." % pdf_fname) + logger.info(f"Reading 4D PDF file {pdf_fname}...") bti_info = _read_bti_header( pdf_fname, config_fname, sort_by_ch_name=sort_by_ch_name ) @@ -1339,7 +1337,7 @@ def _get_bti_info( if convert: if idx == 0: logger.info( - "... putting coil transforms in Neuromag " "coordinates" + "... putting coil transforms in Neuromag coordinates" ) t = _loc_to_coil_trans(bti_info["chs"][idx]["loc"]) t = _convert_coil_trans(t, dev_ctf_t, bti_dev_t) diff --git a/mne/io/bti/read.py b/mne/io/bti/read.py index 4c13ed2f426..6489a77850a 100644 --- a/mne/io/bti/read.py +++ b/mne/io/bti/read.py @@ -30,7 +30,7 @@ def _unpack_simple(fid, dtype, out_dtype): def read_char(fid, count=1): """Read character from bti file.""" - return _unpack_simple(fid, ">S%s" % count, "S") + return _unpack_simple(fid, f">S{count}", "S") def read_bool(fid): diff --git a/mne/io/cnt/_utils.py b/mne/io/cnt/_utils.py index 86842ad60c6..6a05427ff20 100644 --- a/mne/io/cnt/_utils.py +++ b/mne/io/cnt/_utils.py @@ -84,7 +84,7 @@ def _get_event_parser(event_type): event_maker = CNTEventType3 struct_pattern = " 0: parts = line.decode("utf-8").split() if len(parts) != 5: - raise RuntimeError("Illegal data in EEG position file: %s" % line) + raise RuntimeError(f"Illegal data in EEG position file: {line}") r = np.array([float(p) for p in parts[2:]]) / 100.0 if (r * r).sum() > 1e-4: label = parts[1] @@ -72,7 +72,7 @@ def _read_pos(directory, transformations): elif len(fname) > 1: warn(" Found multiple pos files. Extra digitizer points not added.") return list() - logger.info(" Reading digitizer points from %s..." % fname) + logger.info(f" Reading digitizer points from {fname}...") if transformations["t_ctf_head_head"] is None: warn(" No transformation found. Extra digitizer points not added.") return list() diff --git a/mne/io/ctf/hc.py b/mne/io/ctf/hc.py index 7beb8149960..5de790d0dac 100644 --- a/mne/io/ctf/hc.py +++ b/mne/io/ctf/hc.py @@ -62,7 +62,7 @@ def _read_one_coil_point(fid): continue sp = sp.split(" ") if len(sp) != 3 or sp[0] != coord or sp[1] != "=": - raise RuntimeError("Bad line: %s" % one) + raise RuntimeError(f"Bad line: {one}") # We do not deal with centimeters p["r"][ii] = float(sp[2]) / 100.0 return p diff --git a/mne/io/ctf/info.py b/mne/io/ctf/info.py index 791fdceaf51..e12ee4a3d68 100644 --- a/mne/io/ctf/info.py +++ b/mne/io/ctf/info.py @@ -104,10 +104,10 @@ def _convert_time(date_str, time_str): break else: raise RuntimeError( - "Illegal date: %s.\nIf the language of the date does not " + f"Illegal date: {date_str}.\nIf the language of the date does not " "correspond to your local machine's language try to set the " "locale to the language of the date string:\n" - 'locale.setlocale(locale.LC_ALL, "en_US")' % date_str + 'locale.setlocale(locale.LC_ALL, "en_US")' ) for fmt in ("%H:%M:%S", "%H:%M"): @@ -118,7 +118,7 @@ def _convert_time(date_str, time_str): else: break else: - raise RuntimeError("Illegal time: %s" % time_str) + raise RuntimeError(f"Illegal time: {time_str}") # MNE-C uses mktime which uses local time, but here we instead decouple # conversion location from the process, and instead assume that the # acquisition was in GMT. This will be wrong for most sites, but at least @@ -294,8 +294,8 @@ def _convert_channel_info(res4, t, use_eeg_pos): if not _at_origin(ch["loc"][:3]): if t["t_ctf_head_head"] is None: warn( - "EEG electrode (%s) location omitted because of " - "missing HPI information" % ch["ch_name"] + f"EEG electrode ({ch['ch_name']}) location omitted because " + "of missing HPI information" ) ch["loc"].fill(np.nan) coord_frame = FIFF.FIFFV_MNE_COORD_CTF_HEAD @@ -428,7 +428,7 @@ def _add_eeg_pos(eeg, t, c): return if t is None or t["t_ctf_head_head"] is None: raise RuntimeError( - "No coordinate transformation available for EEG " "position data" + "No coordinate transformation available for EEG position data" ) eeg_assigned = 0 if eeg["assign_to_chs"]: @@ -443,7 +443,7 @@ def _add_eeg_pos(eeg, t, c): elif eeg["coord_frame"] != FIFF.FIFFV_COORD_HEAD: raise RuntimeError( "Illegal coordinate frame for EEG electrode " - "positions : %s" % _coord_frame_name(eeg["coord_frame"]) + f"positions : {_coord_frame_name(eeg['coord_frame'])}" ) # Use the logical channel number as an identifier eeg["ids"][k] = ch["logno"] @@ -465,8 +465,8 @@ def _add_eeg_pos(eeg, t, c): d["r"] = apply_trans(t["t_ctf_head_head"], d["r"]) elif eeg["coord_frame"] != FIFF.FIFFV_COORD_HEAD: raise RuntimeError( - "Illegal coordinate frame for EEG electrode " - "positions: %s" % _coord_frame_name(eeg["coord_frame"]) + "Illegal coordinate frame for EEG electrode positions: " + + _coord_frame_name(eeg["coord_frame"]) ) if eeg["kinds"][k] == FIFF.FIFFV_POINT_CARDINAL: fid_count += 1 @@ -552,7 +552,7 @@ def _annotate_bad_segments(directory, start_time, meas_date): with open(fname) as fid: for f in fid.readlines(): tmp = f.strip().split() - desc.append("bad_%s" % tmp[0]) + desc.append(f"bad_{tmp[0]}") onsets.append(np.float64(tmp[1]) - start_time) durations.append(np.float64(tmp[2]) - np.float64(tmp[1])) # return None if there are no bad segments diff --git a/mne/io/ctf/res4.py b/mne/io/ctf/res4.py index 1f69e356c09..0c964f03af1 100644 --- a/mne/io/ctf/res4.py +++ b/mne/io/ctf/res4.py @@ -20,7 +20,7 @@ def _make_ctf_name(directory, extra, raise_error=True): found = True if not op.isfile(fname): if raise_error: - raise OSError("Standard file %s not found" % fname) + raise OSError(f"Standard file {fname} not found") found = False return fname, found @@ -83,7 +83,7 @@ def _read_comp_coeff(fid, d): ("coeff_type", ">i4"), ("d0", ">i4"), ("ncoeff", ">i2"), - ("sensors", "S%s" % CTF.CTFV_SENSOR_LABEL, CTF.CTFV_MAX_BALANCING), + ("sensors", f"S{CTF.CTFV_SENSOR_LABEL}", CTF.CTFV_MAX_BALANCING), ("coeffs", ">f8", CTF.CTFV_MAX_BALANCING), ] ) diff --git a/mne/io/ctf/tests/test_ctf.py b/mne/io/ctf/tests/test_ctf.py index bf4415d90b8..df15e24f02c 100644 --- a/mne/io/ctf/tests/test_ctf.py +++ b/mne/io/ctf/tests/test_ctf.py @@ -92,7 +92,7 @@ def test_read_ctf(tmp_path): args = ( str(ch_num + 1), raw.ch_names[ch_num], - ) + tuple("%0.5f" % x for x in 100 * pos[ii]) # convert to cm + ) + tuple(f"{x:0.5f}" for x in 100 * pos[ii]) # convert to cm fid.write(("\t".join(args) + "\n").encode("ascii")) pos_read_old = np.array([raw.info["chs"][p]["loc"][:3] for p in picks]) with pytest.warns(RuntimeWarning, match="RMSP .* changed to a MISC ch"): diff --git a/mne/io/ctf/trans.py b/mne/io/ctf/trans.py index 5491b5fb972..b50f659aa5a 100644 --- a/mne/io/ctf/trans.py +++ b/mne/io/ctf/trans.py @@ -45,7 +45,7 @@ def _quaternion_align(from_frame, to_frame, from_pts, to_pts, diff_tol=1e-4): ) if diff > diff_tol: raise RuntimeError( - "Something is wrong: quaternion matching did " "not work (see above)" + "Something is wrong: quaternion matching did not work (see above)" ) return Transform(from_frame, to_frame, trans) @@ -65,7 +65,7 @@ def _make_ctf_coord_trans_set(res4, coils): nas = p if lpa is None or rpa is None or nas is None: raise RuntimeError( - "Some of the mandatory HPI device-coordinate " "info was not there." + "Some of the mandatory HPI device-coordinate info was not there." ) t = _make_transform_card("head", "ctf_head", lpa["r"], nas["r"], rpa["r"]) T3 = invert_transform(t) @@ -107,11 +107,11 @@ def _make_ctf_coord_trans_set(res4, coils): d_pts[kind] = p["r"] if any(kind not in h_pts for kind in kinds[:-1]): raise RuntimeError( - "Some of the mandatory HPI device-coordinate " "info was not there." + "Some of the mandatory HPI device-coordinate info was not there." ) if any(kind not in d_pts for kind in kinds[:-1]): raise RuntimeError( - "Some of the mandatory HPI head-coordinate " "info was not there." + "Some of the mandatory HPI head-coordinate info was not there." ) use_kinds = [kind for kind in kinds if (kind in h_pts and kind in d_pts)] r_head = np.array([h_pts[kind] for kind in use_kinds]) diff --git a/mne/io/curry/curry.py b/mne/io/curry/curry.py index 3d0fb9afbca..3754a7ab92d 100644 --- a/mne/io/curry/curry.py +++ b/mne/io/curry/curry.py @@ -68,7 +68,7 @@ CurryParameters = namedtuple( "CurryParameters", - "n_samples, sfreq, is_ascii, unit_dict, " "n_chans, dt_start, chanidx_in_file", + "n_samples, sfreq, is_ascii, unit_dict, n_chans, dt_start, chanidx_in_file", ) @@ -608,8 +608,8 @@ def __init__(self, fname, preload=False, verbose=None): if "events" in curry_paths: logger.info( - "Event file found. Extracting Annotations from" - " %s..." % curry_paths["events"] + "Event file found. Extracting Annotations from " + f"{curry_paths['events']}..." ) annots = _read_annotations_curry( curry_paths["events"], sfreq=self.info["sfreq"] diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 8a982f43e86..023687ee74b 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -513,7 +513,7 @@ def _read_header(fname, exclude, infer_types, include=None, exclude_after_unique (edf_info, orig_units) : tuple """ ext = os.path.splitext(fname)[1][1:].lower() - logger.info("%s file detected" % ext.upper()) + logger.info(f"{ext.upper()} file detected") if ext in ("bdf", "edf"): return _read_edf_header( fname, exclude, infer_types, include, exclude_after_unique diff --git a/mne/io/edf/tests/test_edf.py b/mne/io/edf/tests/test_edf.py index 7517693b6ea..bc00b605ca6 100644 --- a/mne/io/edf/tests/test_edf.py +++ b/mne/io/edf/tests/test_edf.py @@ -100,7 +100,7 @@ def test_orig_units(): def test_units_params(): """Test enforcing original channel units.""" with pytest.raises( - ValueError, match=r"Unit for channel .* is present .* cannot " "overwrite it" + ValueError, match=r"Unit for channel .* is present .* cannot overwrite it" ): _ = read_raw_edf(edf_path, units="V", preload=True) @@ -1015,9 +1015,8 @@ def test_include(): raw = read_raw_edf(edf_path, include="I[1-4]") assert sorted(raw.ch_names) == ["I1", "I2", "I3", "I4"] - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="'exclude' must be empty if 'include' is "): raw = read_raw_edf(edf_path, include=["I1", "I2"], exclude="I[1-4]") - assert str(e.value) == "'exclude' must be empty" "if 'include' is assigned." @pytest.mark.parametrize( diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index 905e9620010..cfd089beaa9 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -234,7 +234,7 @@ def _get_info(eeg, *, eog, montage_units): ) update_ch_names = False else: # if eeg.chanlocs is empty, we still need default chan names - ch_names = ["EEG %03d" % ii for ii in range(eeg.nbchan)] + ch_names = [f"EEG {ii:03d}" for ii in range(eeg.nbchan)] ch_types = "eeg" eeg_montage = None update_ch_names = True @@ -452,9 +452,9 @@ def __init__( eeg = _check_load_mat(input_fname, uint16_codec) if eeg.trials != 1: raise TypeError( - "The number of trials is %d. It must be 1 for raw" + f"The number of trials is {eeg.trials:d}. It must be 1 for raw" " files. Please use `mne.io.read_epochs_eeglab` if" - " the .set file contains epochs." % eeg.trials + " the .set file contains epochs." ) last_samps = [eeg.pnts - 1] @@ -463,7 +463,7 @@ def __init__( # read the data if isinstance(eeg.data, str): data_fname = _check_eeglab_fname(input_fname, eeg.data) - logger.info("Reading %s" % data_fname) + logger.info(f"Reading {data_fname}") super().__init__( info, @@ -610,7 +610,7 @@ def __init__( (events is None and event_id is None) or (events is not None and event_id is not None) ): - raise ValueError("Both `events` and `event_id` must be " "None or not None") + raise ValueError("Both `events` and `event_id` must be None or not None") if eeg.trials <= 1: raise ValueError( @@ -668,13 +668,13 @@ def __init__( elif isinstance(events, (str, Path, PathLike)): events = read_events(events) - logger.info("Extracting parameters from %s..." % input_fname) + logger.info(f"Extracting parameters from {input_fname}...") info, eeg_montage, _ = _get_info(eeg, eog=eog, montage_units=montage_units) for key, val in event_id.items(): if val not in events[:, 2]: raise ValueError( - "No matching events found for %s " "(event id %i)" % (key, val) + f"No matching events found for {key} (event id {val:i})" ) if isinstance(eeg.data, str): diff --git a/mne/io/egi/egi.py b/mne/io/egi/egi.py index b0124bdc541..e95577f86ad 100644 --- a/mne/io/egi/egi.py +++ b/mne/io/egi/egi.py @@ -26,9 +26,7 @@ def _read_header(fid): if version > 6 & ~np.bitwise_and(version, 6): version = version.byteswap().astype(np.uint32) else: - raise ValueError( - "Watchout. This does not seem to be a simple " "binary EGI file." - ) + raise ValueError("Watchout. This does not seem to be a simple binary EGI file.") def my_fread(*x, **y): return int(np.fromfile(*x, **y)[0]) @@ -200,7 +198,7 @@ def __init__( if misc is None: misc = [] with open(input_fname, "rb") as fid: # 'rb' important for py3k - logger.info("Reading EGI header from %s..." % input_fname) + logger.info(f"Reading EGI header from {input_fname}...") egi_info = _read_header(fid) logger.info(" Reading events ...") egi_events = _read_events(fid, egi_info) # update info + jump @@ -226,7 +224,7 @@ def __init__( more_excludes.append(ii) if len(exclude_inds) + len(more_excludes) == len(event_codes): warn( - "Did not find any event code with more than one " "event.", + "Did not find any event code with more than one event.", RuntimeWarning, ) else: @@ -245,16 +243,16 @@ def __init__( if isinstance(v, list): for k in v: if k not in event_codes: - raise ValueError('Could find event named "%s"' % k) + raise ValueError(f'Could find event named "{k}"') elif v is not None: - raise ValueError("`%s` must be None or of type list" % kk) + raise ValueError(f"`{kk}` must be None or of type list") event_ids = np.arange(len(include_)) + 1 logger.info(' Synthesizing trigger channel "STI 014" ...') - logger.info( - " Excluding events {%s} ..." - % ", ".join([k for i, k in enumerate(event_codes) if i not in include_]) + excl_events = ", ".join( + k for i, k in enumerate(event_codes) if i not in include_ ) + logger.info(f" Excluding events {{{excl_events}}} ...") egi_info["new_trigger"] = _combine_triggers( egi_events[include_], remapping=event_ids ) diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index 3a039b0c784..6d5559a966e 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -462,7 +462,7 @@ def __init__( need_dir=True, ) ) - logger.info("Reading EGI MFF Header from %s..." % input_fname) + logger.info(f"Reading EGI MFF Header from {input_fname}...") egi_info = _read_header(input_fname) if eog is None: eog = [] @@ -487,7 +487,7 @@ def __init__( more_excludes.append(ii) if len(exclude_inds) + len(more_excludes) == len(event_codes): warn( - "Did not find any event code with more than one " "event.", + "Did not find any event code with more than one event.", RuntimeWarning, ) else: @@ -508,12 +508,12 @@ def __init__( if k not in event_codes: raise ValueError(f"Could not find event named {repr(k)}") elif v is not None: - raise ValueError("`%s` must be None or of type list" % kk) + raise ValueError(f"`{kk}` must be None or of type list") logger.info(' Synthesizing trigger channel "STI 014" ...') - logger.info( - " Excluding events {%s} ..." - % ", ".join([k for i, k in enumerate(event_codes) if i not in include_]) + excl_events = ", ".join( + k for i, k in enumerate(event_codes) if i not in include_ ) + logger.info(f" Excluding events {{{excl_events}}} ...") if all(ch.startswith("D") for ch in include_names): # support the DIN format DIN1, DIN2, ..., DIN9, DI10, DI11, ... DI99, # D100, D101, ..., D255 that we get when sending 0-255 triggers on a @@ -615,7 +615,7 @@ def __init__( np.concatenate([idx[key] for key in keys]), np.arange(len(chs)) ): raise ValueError( - "Currently interlacing EEG and PNS channels" "is not supported" + "Currently interlacing EEG and PNS channels is not supported" ) egi_info["kind_bounds"] = [0] for key in keys: diff --git a/mne/io/egi/general.py b/mne/io/egi/general.py index 9ca6dc7f0b9..1dec9b9ae5f 100644 --- a/mne/io/egi/general.py +++ b/mne/io/egi/general.py @@ -113,9 +113,9 @@ def _get_blocks(filepath): position = fid.tell() if any([n != n_channels[0] for n in n_channels]): - raise RuntimeError("All the blocks don't have the same amount of " "channels.") + raise RuntimeError("All the blocks don't have the same amount of channels.") if any([f != sfreq[0] for f in sfreq]): - raise RuntimeError("All the blocks don't have the same sampling " "frequency.") + raise RuntimeError("All the blocks don't have the same sampling frequency.") if len(samples_block) < 1: raise RuntimeError("There seems to be no data") samples_block = np.array(samples_block) diff --git a/mne/io/eximia/eximia.py b/mne/io/eximia/eximia.py index 1d253f369d1..b627f85997c 100644 --- a/mne/io/eximia/eximia.py +++ b/mne/io/eximia/eximia.py @@ -56,7 +56,7 @@ class RawEximia(BaseRaw): def __init__(self, fname, preload=False, verbose=None): fname = str(_check_fname(fname, "read", True, "fname")) data_name = op.basename(fname) - logger.info("Loading %s" % data_name) + logger.info(f"Loading {data_name}") # Create vhdr and vmrk files so that we can use mne_brain_vision2fiff n_chan = 64 sfreq = 1450.0 diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 7f57596ac38..54afcef5427 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -224,7 +224,7 @@ def _simulate_eye_tracking_data(in_file, out_file): elif event_type == "END": pass else: - fp.write("%s\n" % line) + fp.write(f"{line}\n") continue events.append("\t".join(tokens)) if event_type == "END": @@ -232,7 +232,7 @@ def _simulate_eye_tracking_data(in_file, out_file): events.clear() in_recording_block = False else: - fp.write("%s\n" % line) + fp.write(f"{line}\n") fp.write("START\t7452389\tRIGHT\tSAMPLES\tEVENTS\n") fp.write(f"{new_samples_line}\n") diff --git a/mne/io/fieldtrip/fieldtrip.py b/mne/io/fieldtrip/fieldtrip.py index 3dac2992be1..192782851db 100644 --- a/mne/io/fieldtrip/fieldtrip.py +++ b/mne/io/fieldtrip/fieldtrip.py @@ -76,7 +76,7 @@ def read_raw_fieldtrip(fname, info, data_name="data") -> RawArray: if data.ndim != 2: raise RuntimeError( - "The data you are trying to load does not seem to " "be raw data" + "The data you are trying to load does not seem to be raw data" ) raw = RawArray(data, info) # create an MNE RawArray diff --git a/mne/io/fieldtrip/tests/test_fieldtrip.py b/mne/io/fieldtrip/tests/test_fieldtrip.py index 11546e82607..15c374bb9ad 100644 --- a/mne/io/fieldtrip/tests/test_fieldtrip.py +++ b/mne/io/fieldtrip/tests/test_fieldtrip.py @@ -257,7 +257,7 @@ def test_throw_exception_on_cellarray(version, type_): fname = get_data_paths("cellarray") / f"{type_}_{version}.mat" info = get_raw_info("CNT") with pytest.raises( - RuntimeError, match="Loading of data in cell arrays " "is not supported" + RuntimeError, match="Loading of data in cell arrays is not supported" ): if type_ == "averaged": mne.read_evoked_fieldtrip(fname, info) @@ -291,7 +291,7 @@ def test_throw_error_on_non_uniform_time_field(): with pytest.raises( RuntimeError, - match="Loading data with non-uniform " "times per epoch is not supported", + match="Loading data with non-uniform times per epoch is not supported", ): mne.io.read_epochs_fieldtrip(fname, info=None) diff --git a/mne/io/fieldtrip/utils.py b/mne/io/fieldtrip/utils.py index 9a4274f6a43..594451bfab2 100644 --- a/mne/io/fieldtrip/utils.py +++ b/mne/io/fieldtrip/utils.py @@ -54,7 +54,7 @@ def _create_info(ft_struct, raw_info): if missing_channels: warn( "The following channels are present in the FieldTrip data " - f"but cannot be found in the provided info: {str(missing_channels)}.\n" + f"but cannot be found in the provided info: {missing_channels}.\n" "These channels will be removed from the resulting data!" ) @@ -216,7 +216,7 @@ def _set_tmin(ft_struct): tmin = times[0][0] else: raise RuntimeError( - "Loading data with non-uniform " "times per epoch is not supported" + "Loading data with non-uniform times per epoch is not supported" ) return tmin @@ -238,7 +238,7 @@ def _create_events(ft_struct, trialinfo_column): if trialinfo_column > (available_ti_cols - 1): raise ValueError( - "trialinfo_column is higher than the amount of" "columns in trialinfo." + "trialinfo_column is higher than the amount of columns in trialinfo." ) event_trans_val = np.zeros(len(event_type)) diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py index 54bfe9e1921..1e15faa6a2d 100644 --- a/mne/io/fiff/raw.py +++ b/mne/io/fiff/raw.py @@ -168,7 +168,7 @@ def _read_raw_file( self, fname, allow_maxshield, preload, do_check_ext=True, verbose=None ): """Read in header information from a raw file.""" - logger.info("Opening raw data file %s..." % fname) + logger.info(f"Opening raw data file {fname}...") # Read in the whole file if preload is on and .fif.gz (saves time) if not _file_like(fname): @@ -208,7 +208,7 @@ def _read_raw_file( if len(raw_node) == 0: raw_node = dir_tree_find(meas, FIFF.FIFFB_IAS_RAW_DATA) if len(raw_node) == 0: - raise ValueError("No raw data in %s" % fname_rep) + raise ValueError(f"No raw data in {fname_rep}") _check_maxshield(allow_maxshield) with info._unlock(): info["maxshield"] = True diff --git a/mne/io/hitachi/hitachi.py b/mne/io/hitachi/hitachi.py index 4b5c0b9fac6..d0b1ac5a187 100644 --- a/mne/io/hitachi/hitachi.py +++ b/mne/io/hitachi/hitachi.py @@ -96,7 +96,7 @@ def __init__(self, fname, preload=False, *, verbose=None): info = infos[0] if len(set(last_samps)) != 1: raise RuntimeError( - "All files must have the same number of " "samples, got: {last_samps}" + "All files must have the same number of samples, got: {last_samps}" ) last_samps = [last_samps[0]] raw_extras = [dict(probes=probes)] @@ -136,7 +136,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): def _get_hitachi_info(fname, S_offset, D_offset, ignore_names): - logger.info("Loading %s" % fname) + logger.info(f"Loading {fname}") raw_extra = dict(fname=fname) info_extra = dict() subject_info = dict() diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index 71cc38e6c94..5c795f55048 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -70,7 +70,7 @@ def _call_digitization(info, mrk, elp, hsp, kit_info, *, bad_coils=()): ) elif mrk is not None or elp is not None or hsp is not None: raise ValueError( - "mrk, elp and hsp need to be provided as a group " "(all or none)" + "mrk, elp and hsp need to be provided as a group (all or none)" ) return info @@ -142,7 +142,7 @@ def __init__( bad_coils=(), verbose=None, ): - logger.info("Extracting SQD Parameters from %s..." % input_fname) + logger.info(f"Extracting SQD Parameters from {input_fname}...") input_fname = op.abspath(input_fname) self.preload = False logger.info("Creating Raw.info structure...") @@ -152,7 +152,7 @@ def __init__( kit_info["slope"] = slope kit_info["stimthresh"] = stimthresh if kit_info["acq_type"] != KIT.CONTINUOUS: - raise TypeError("SQD file contains epochs, not raw data. Wrong " "reader.") + raise TypeError("SQD file contains epochs, not raw data. Wrong reader.") logger.info("Creating Info structure...") last_samps = [kit_info["n_samples"] - 1] @@ -276,7 +276,7 @@ def _set_stimchannels(inst, info, stim, stim_code): stim = picks else: raise ValueError( - "stim needs to be list of int, '>' or " "'<', not %r" % str(stim) + "stim needs to be list of int, '>' or " f"'<', not {str(stim)!r}" ) else: stim = np.asarray(stim, int) @@ -327,7 +327,7 @@ def _make_stim_channel(trigger_chs, slope, threshold, stim_code, trigger_values) trigger_values = 2 ** np.arange(len(trigger_chs)) elif stim_code != "channel": raise ValueError( - "stim_code must be 'binary' or 'channel', got %s" % repr(stim_code) + f"stim_code must be 'binary' or 'channel', got {repr(stim_code)}" ) trig_chs = trig_chs_bin * trigger_values[:, np.newaxis] return np.array(trig_chs.sum(axis=0), ndmin=2) @@ -401,7 +401,7 @@ def __init__( input_fname = str( _check_fname(fname=input_fname, must_exist=True, overwrite="read") ) - logger.info("Extracting KIT Parameters from %s..." % input_fname) + logger.info(f"Extracting KIT Parameters from {input_fname}...") self.info, kit_info = get_kit_info( input_fname, allow_unknown_format, standardize_names ) @@ -415,7 +415,7 @@ def __init__( self._raw_extras[0]["data_length"] = KIT.INT else: raise TypeError( - "SQD file contains raw data, not epochs or " "average. Wrong reader." + "SQD file contains raw data, not epochs or average. Wrong reader." ) if event_id is None: # convert to int to make typing-checks happy @@ -424,7 +424,7 @@ def __init__( for key, val in event_id.items(): if val not in events[:, 2]: raise ValueError( - "No matching events found for %s " "(event id %i)" % (key, val) + "No matching events found for %s (event id %i)" % (key, val) ) data = self._read_kit_data() @@ -543,7 +543,7 @@ def get_kit_info(rawfile, allow_unknown_format, standardize_names=None, verbose= version_string = "V%iR%03i" % (version, revision) if allow_unknown_format: unsupported_format = True - warn("Force loading KIT format %s" % version_string) + warn(f"Force loading KIT format {version_string}") else: raise UnsupportedKITFormat( version_string, diff --git a/mne/io/nicolet/nicolet.py b/mne/io/nicolet/nicolet.py index 0ef0c0a4f4a..3ebefd53f48 100644 --- a/mne/io/nicolet/nicolet.py +++ b/mne/io/nicolet/nicolet.py @@ -129,7 +129,7 @@ def _get_nicolet_info(fname, ch_type, eog, ecg, emg, misc): ch_kind = FIFF.FIFFV_SEEG_CH else: raise TypeError( - "Channel type not recognized. Available types are " "'eeg' and 'seeg'." + "Channel type not recognized. Available types are 'eeg' and 'seeg'." ) cals = np.repeat(header_info["conversion_factor"] * 1e-6, len(ch_names)) info["chs"] = _create_chs(ch_names, cals, ch_coil, ch_kind, eog, ecg, emg, misc) diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index ef14a735ca9..4e54cb04363 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -184,7 +184,7 @@ def _read_nihon_header(fname): fid.seek(0x17FE) waveform_sign = np.fromfile(fid, np.uint8, 1)[0] if waveform_sign != 1: - raise ValueError("Not a valid Nihon Kohden EEG file " "(waveform block)") + raise ValueError("Not a valid Nihon Kohden EEG file (waveform block)") header["version"] = version fid.seek(0x0091) @@ -267,11 +267,11 @@ def _read_nihon_header(fname): ) if block_0["channels"] != t_block["channels"]: raise ValueError( - "Cannot read NK file with different channels in each " "datablock" + "Cannot read NK file with different channels in each datablock" ) if block_0["sfreq"] != t_block["sfreq"]: raise ValueError( - "Cannot read NK file with different sfreq in each " "datablock" + "Cannot read NK file with different sfreq in each datablock" ) return header @@ -382,7 +382,7 @@ class RawNihon(BaseRaw): def __init__(self, fname, preload=False, verbose=None): fname = _check_fname(fname, "read", True, "fname") data_name = fname.name - logger.info("Loading %s" % data_name) + logger.info(f"Loading {data_name}") header = _read_nihon_header(fname) metadata = _read_nihon_metadata(fname) diff --git a/mne/io/nirx/nirx.py b/mne/io/nirx/nirx.py index 52826f266f3..59ce271f404 100644 --- a/mne/io/nirx/nirx.py +++ b/mne/io/nirx/nirx.py @@ -91,7 +91,7 @@ class RawNIRX(BaseRaw): @verbose def __init__(self, fname, saturated, preload=False, verbose=None): - logger.info("Loading %s" % fname) + logger.info(f"Loading {fname}") _validate_type(fname, "path-like", "fname") _validate_type(saturated, str, "saturated") _check_option("saturated", saturated, ("annotate", "nan", "ignore")) @@ -210,8 +210,8 @@ def __init__(self, fname, saturated, preload=False, verbose=None): ): warn( "Only import of data from NIRScout devices have been " - "thoroughly tested. You are using a %s device. " - % hdr["GeneralInfo"]["Device"] + f'thoroughly tested. You are using a {hdr["GeneralInfo"]["Device"]}' + " device." ) # Parse required header fields diff --git a/mne/io/nirx/tests/test_nirx.py b/mne/io/nirx/tests/test_nirx.py index 3cc510612e0..f0346e189ff 100644 --- a/mne/io/nirx/tests/test_nirx.py +++ b/mne/io/nirx/tests/test_nirx.py @@ -396,8 +396,8 @@ def test_nirx_15_3_short(): assert raw.info["subject_info"] == dict( birthday=(2020, 8, 18), sex=0, - first_name="testMontage\\0A" "TestMontage", - his_id="testMontage\\0A" "TestMontage", + first_name="testMontage\\0ATestMontage", + his_id="testMontage\\0ATestMontage", ) # Test distance between optodes matches values from diff --git a/mne/io/persyst/persyst.py b/mne/io/persyst/persyst.py index c260f413205..7df91d5b503 100644 --- a/mne/io/persyst/persyst.py +++ b/mne/io/persyst/persyst.py @@ -68,7 +68,7 @@ class RawPersyst(BaseRaw): @verbose def __init__(self, fname, preload=False, verbose=None): fname = str(_check_fname(fname, "read", True, "fname")) - logger.info("Loading %s" % fname) + logger.info(f"Loading {fname}") # make sure filename is the Lay file if not fname.endswith(".lay"): @@ -165,7 +165,7 @@ def __init__(self, fname, preload=False, verbose=None): warn( "Cannot read in the measurement date due " "to incompatible format. Please set manually " - "for %s " % lay_fname + f"for {lay_fname} " ) meas_date = None else: @@ -297,13 +297,13 @@ def _get_subjectinfo(patient_dict): birthdate = datetime.strptime(birthdate, "%m/%d/%y") except ValueError: birthdate = None - print("Unable to process birthdate of %s " % birthdate) + print(f"Unable to process birthdate of {birthdate} ") elif "-" in birthdate: try: birthdate = datetime.strptime(birthdate, "%d-%m-%y") except ValueError: birthdate = None - print("Unable to process birthdate of %s " % birthdate) + print(f"Unable to process birthdate of {birthdate} ") subject_info = { "first_name": patient_dict.get("first"), @@ -456,9 +456,9 @@ def _process_lay_line(line, section): else: if "=" not in line: raise RuntimeError( - "The line %s does not conform " + f"The line {line} does not conform " "to the standards. Please check the " - ".lay file." % line + ".lay file." ) # noqa pos = line.index("=") status = 2 diff --git a/mne/io/persyst/tests/test_persyst.py b/mne/io/persyst/tests/test_persyst.py index 76e117817fd..11cf042a6d7 100644 --- a/mne/io/persyst/tests/test_persyst.py +++ b/mne/io/persyst/tests/test_persyst.py @@ -237,7 +237,7 @@ def test_persyst_errors(tmp_path): line = "WaveformCount=1\n" fout.write(line) # file should break - with pytest.raises(RuntimeError, match="Channels in lay " "file do not"): + with pytest.raises(RuntimeError, match="Channels in lay file do not"): read_raw_persyst(new_fname_lay) # reformat the lay file to have testdate diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index bde3e045528..f46ce2f09c0 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -91,7 +91,7 @@ def __init__(self, fname, optode_frame="unknown", preload=False, verbose=None): h5py = _import_h5py() fname = str(_check_fname(fname, "read", True, "fname")) - logger.info("Loading %s" % fname) + logger.info(f"Loading {fname}") with h5py.File(fname, "r") as dat: if "data2" in dat["nirs"]: diff --git a/mne/label.py b/mne/label.py index 5c8a1b8ca30..ef3c08ee4c7 100644 --- a/mne/label.py +++ b/mne/label.py @@ -155,7 +155,7 @@ def _n_colors(n, bytes_=False, cmap="hsv"): """ n_max = 2**10 if n > n_max: - raise NotImplementedError("Can't produce more than %i unique " "colors" % n_max) + raise NotImplementedError("Can't produce more than %i unique colors" % n_max) from .viz.utils import _get_cmap @@ -245,7 +245,7 @@ def __init__( ): # check parameters if not isinstance(hemi, str): - raise ValueError("hemi must be a string, not %s" % type(hemi)) + raise ValueError(f"hemi must be a string, not {type(hemi)}") vertices = np.asarray(vertices, int) if np.any(np.diff(vertices.astype(int)) <= 0): raise ValueError("Vertices must be ordered in increasing order.") @@ -765,7 +765,7 @@ def split(self, parts=2, subject=None, subjects_dir=None, freesurfer=False): else: raise ValueError( "Need integer, tuple of strings, or string " - "('contiguous'). Got %s)" % type(parts) + f"('contiguous'). Got {type(parts)})" ) def get_vertices_used(self, vertices=None): @@ -809,7 +809,7 @@ def get_tris(self, tris, vertices=None): selection = np.all(np.isin(tris, vertices_).reshape(tris.shape), axis=1) label_tris = tris[selection] if len(np.unique(label_tris)) < len(vertices_): - logger.info("Surprising label structure. Trying to repair " "triangles.") + logger.info("Surprising label structure. Trying to repair triangles.") dropped_vertices = np.setdiff1d(vertices_, label_tris) n_dropped = len(dropped_vertices) assert n_dropped == (len(vertices_) - len(np.unique(label_tris))) @@ -1058,7 +1058,7 @@ def __add__(self, other): lh = self.lh + other.lh rh = self.rh + other.rh else: - raise TypeError("Need: Label or BiHemiLabel. Got: %r" % other) + raise TypeError(f"Need: Label or BiHemiLabel. Got: {other!r}") name = f"{self.name} + {other.name}" color = _blend_colors(self.color, other.color) @@ -1207,7 +1207,7 @@ def write_label(filename, label, verbose=None): name += "-" + hemi filename = op.join(path_head, name) + ".label" - logger.info("Saving label to : %s" % filename) + logger.info(f"Saving label to : {filename}") with open(filename, "wb") as fid: n_vertices = len(label.vertices) @@ -1534,7 +1534,7 @@ def stc_to_label( If no Label is available in an hemisphere, an empty list is returned. """ if not isinstance(smooth, bool): - raise ValueError("smooth should be True or False. Got %s." % smooth) + raise ValueError(f"smooth should be True or False. Got {smooth}.") src = stc.subject if src is None else src if src is None: @@ -1831,7 +1831,7 @@ def grow_labels( names = [names] if len(names) != n_seeds: raise ValueError( - "The names parameter has to be None or have " "length len(seeds)" + "The names parameter has to be None or have length len(seeds)" ) for i, hemi in enumerate(hemis): if not names[i].endswith(hemi): @@ -2152,8 +2152,8 @@ def _read_annot(fname): cands = _read_annot_cands(dir_name) if len(cands) == 0: raise OSError( - "No such file %s, no candidate parcellations " - "found in directory" % fname + f"No such file {fname}, no candidate parcellations " + "found in directory" ) else: raise OSError( @@ -2229,7 +2229,7 @@ def _get_annot_fname(annot_fname, subject, hemi, parc, subjects_dir): hemis = [hemi] subjects_dir = get_subjects_dir(subjects_dir, raise_error=True) - dst = str(subjects_dir / subject / "label" / ("%%s.%s.annot" % parc)) + dst = str(subjects_dir / subject / "label" / f"%s.{parc}.annot") annot_fname = [dst % hemi_ for hemi_ in hemis] return annot_fname, hemis @@ -2312,7 +2312,7 @@ def read_labels_from_annot( if regexp is not None: # allow for convenient substring match r_ = re.compile( - ".*%s.*" % regexp if regexp.replace("_", "").isalnum() else regexp + f".*{regexp}.*" if regexp.replace("_", "").isalnum() else regexp ) # now we are ready to create the labels @@ -2332,7 +2332,7 @@ def read_labels_from_annot( surf_name, hemi, len(annot), - extra="for annotation file %s" % fname, + extra=f"for annotation file {fname}", ) for label_id, label_name, label_rgba in zip( label_ids, label_names, label_rgbas @@ -2693,7 +2693,7 @@ def write_labels_to_annot( for fname in annot_fname: if op.exists(fname): raise ValueError( - 'File %s exists. Use "overwrite=True" to ' "overwrite it" % fname + f'File {fname} exists. Use "overwrite=True" to ' "overwrite it" ) # prepare container for data to save: @@ -2769,7 +2769,7 @@ def write_labels_to_annot( # find number of vertices in surface if subject is not None and subjects_dir is not None: - fpath = op.join(subjects_dir, subject, "surf", "%s.white" % hemi) + fpath = op.join(subjects_dir, subject, "surf", f"{hemi}.white") points, _ = read_surface(fpath) n_vertices = len(points) else: @@ -2817,7 +2817,7 @@ def write_labels_to_annot( # Assign unlabeled vertices to an "unknown" label unlabeled = annot == -1 if np.any(unlabeled): - msg = "Assigning %i unlabeled vertices to " "'unknown-%s'" % ( + msg = "Assigning %i unlabeled vertices to 'unknown-%s'" % ( unlabeled.sum(), hemi, ) diff --git a/mne/minimum_norm/_eloreta.py b/mne/minimum_norm/_eloreta.py index b49b0a4a338..0fd5240fd4a 100644 --- a/mne/minimum_norm/_eloreta.py +++ b/mne/minimum_norm/_eloreta.py @@ -36,7 +36,7 @@ def _compute_eloreta(inv, lambda2, options): # Reassemble the gain matrix (should be fast enough) if inv["eigen_leads_weighted"]: # We can probably relax this if we ever need to - raise RuntimeError("eLORETA cannot be computed with weighted eigen " "leads") + raise RuntimeError("eLORETA cannot be computed with weighted eigen leads") G = np.dot( inv["eigen_fields"]["data"].T * inv["sing"], inv["eigen_leads"]["data"].T ) @@ -128,7 +128,7 @@ def _compute_eloreta(inv, lambda2, options): ) break else: - warn("eLORETA weight fitting did not converge (>= %s)" % eps) + warn(f"eLORETA weight fitting did not converge (>= {eps})") del G_R_Gt logger.info(" Updating inverse with weighted eigen leads") G /= source_std # undo our biasing diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py index 387e341370b..63043757b9b 100644 --- a/mne/minimum_norm/inverse.py +++ b/mne/minimum_norm/inverse.py @@ -167,10 +167,10 @@ def _pick_channels_inverse_operator(ch_names, inv): except ValueError: raise ValueError( "The inverse operator was computed with " - "channel %s which is not present in " + f"channel {name} which is not present in " "the data. You should compute a new inverse " "operator restricted to the good data " - "channels." % name + "channels." ) return sel @@ -204,7 +204,7 @@ def read_inverse_operator(fname, *, verbose=None): # # Open the file, create directory # - logger.info("Reading inverse operator decomposition from %s..." % fname) + logger.info(f"Reading inverse operator decomposition from {fname}...") f, tree, _ = fiff_open(fname) with f as fid: # @@ -212,7 +212,7 @@ def read_inverse_operator(fname, *, verbose=None): # invs = dir_tree_find(tree, FIFF.FIFFB_MNE_INVERSE_SOLUTION) if invs is None or len(invs) < 1: - raise Exception("No inverse solutions in %s" % fname) + raise Exception(f"No inverse solutions in {fname}") invs = invs[0] # @@ -220,7 +220,7 @@ def read_inverse_operator(fname, *, verbose=None): # parent_mri = dir_tree_find(tree, FIFF.FIFFB_MNE_PARENT_MRI_FILE) if len(parent_mri) == 0: - raise Exception("No parent MRI information in %s" % fname) + raise Exception(f"No parent MRI information in {fname}") parent_mri = parent_mri[0] # take only first one logger.info(" Reading inverse operator info...") @@ -391,12 +391,12 @@ def read_inverse_operator(fname, *, verbose=None): inv["src"][k], inv["coord_frame"], mri_head_t ) except Exception as inst: - raise Exception("Could not transform source space (%s)" % inst) + raise Exception(f"Could not transform source space ({inst})") nuse += inv["src"][k]["nuse"] logger.info( - " Source spaces transformed to the inverse solution " "coordinate frame" + " Source spaces transformed to the inverse solution coordinate frame" ) # # Done! @@ -437,7 +437,7 @@ def write_inverse_operator(fname, inv, *, overwrite=False, verbose=None): # # Open the file, create directory # - logger.info("Write inverse operator decomposition in %s..." % fname) + logger.info(f"Write inverse operator decomposition in {fname}...") # Create the file and save the essentials with start_and_end_file(fname) as fid: @@ -585,7 +585,7 @@ def _check_ch_names(inv, info): if n_missing > 0: raise ValueError( "%d channels in inverse operator " % n_missing - + "are not present in the data (%s)" % missing_ch_names + + f"are not present in the data ({missing_ch_names})" ) _check_compensation_grade(inv["info"], info, "inverse") @@ -692,7 +692,7 @@ def prepare_inverse_operator( if ncomp > 0: logger.info(" Created an SSP operator (subspace dimension = %d)" % ncomp) else: - logger.info(" The projection vectors do not apply to these " "channels.") + logger.info(" The projection vectors do not apply to these channels.") # # Create the whitener @@ -709,7 +709,7 @@ def prepare_inverse_operator( if method == "eLORETA": _compute_eloreta(inv, lambda2, method_params) elif method != "MNE": - logger.info(" Computing noise-normalization factors (%s)..." % method) + logger.info(f" Computing noise-normalization factors ({method})...") # Here we have:: # # inv['reginv'] = sing / (sing ** 2 + lambda2) @@ -909,7 +909,7 @@ def _check_reference(inst, ch_names=None): "modeling, use the method set_eeg_reference(projection=True)" ) if _electrode_types(info) and info.get("custom_ref_applied", False): - raise ValueError("Custom EEG reference is not allowed for inverse " "modeling.") + raise ValueError("Custom EEG reference is not allowed for inverse modeling.") def _subject_from_inverse(inverse_operator): @@ -2017,7 +2017,7 @@ def make_inverse_operator( logger.info("Computing SVD of whitened and weighted lead field matrix.") eigen_fields, sing, eigen_leads = _safe_svd(gain, full_matrices=False) del gain - logger.info(" largest singular value = %g" % np.max(sing)) + logger.info(f" largest singular value = {np.max(sing):g}") logger.info( f" scaling factor to adjust the trace = {trace_GRGT:g} " f"(nchan = {eigen_fields.shape[0]} " diff --git a/mne/minimum_norm/resolution_matrix.py b/mne/minimum_norm/resolution_matrix.py index 655ca991914..dccb08b3e04 100644 --- a/mne/minimum_norm/resolution_matrix.py +++ b/mne/minimum_norm/resolution_matrix.py @@ -192,7 +192,7 @@ def _get_psf_ctf( def _check_get_psf_ctf_params(mode, n_comp, return_pca_vars): """Check input parameters of _get_psf_ctf() for consistency.""" if mode in [None, "sum", "mean"] and n_comp > 1: - msg = "n_comp must be 1 for mode=%s." % mode + msg = f"n_comp must be 1 for mode={mode}." raise ValueError(msg) if mode != "pca" and return_pca_vars: msg = "SVD variances can only be returned if mode=" "pca" "." @@ -513,7 +513,7 @@ def _get_matrix_from_inverse_operator( assert np.array_equal(v0o1, invmat[1]) assert np.array_equal(v3o2, invmat[11]) - logger.info("Dimension of Inverse Matrix: %s" % str(invmat.shape)) + logger.info(f"Dimension of Inverse Matrix: {invmat.shape}") return invmat diff --git a/mne/minimum_norm/spatial_resolution.py b/mne/minimum_norm/spatial_resolution.py index c9d28aef4d8..430ce6d4824 100644 --- a/mne/minimum_norm/spatial_resolution.py +++ b/mne/minimum_norm/spatial_resolution.py @@ -79,10 +79,10 @@ def resolution_metrics( # Check if input options are valid metrics = ("peak_err", "cog_err", "sd_ext", "maxrad_ext", "peak_amp", "sum_amp") if metric not in metrics: - raise ValueError('"%s" is not a recognized metric.' % metric) + raise ValueError(f'"{metric}" is not a recognized metric.') if function not in ["psf", "ctf"]: - raise ValueError("Not a recognised resolution function: %s." % function) + raise ValueError(f"Not a recognised resolution function: {function}.") if metric in ("peak_err", "cog_err"): resolution_metric = _localisation_error( diff --git a/mne/minimum_norm/tests/test_inverse.py b/mne/minimum_norm/tests/test_inverse.py index e3be18a3fc9..4dd41914664 100644 --- a/mne/minimum_norm/tests/test_inverse.py +++ b/mne/minimum_norm/tests/test_inverse.py @@ -345,7 +345,7 @@ def test_inverse_operator_channel_ordering(evoked, noise_cov): evoked.info, fwd_orig, noise_cov, loose=0.2, depth=depth, verbose=True ) log = log.getvalue() - assert "limit = 1/%s" % fwd_orig["nsource"] in log + assert f"limit = 1/{fwd_orig['nsource']}" in log stc_1 = apply_inverse(evoked, inv_orig, lambda2, "dSPM") # Assume that a raw reordering applies to both evoked and noise_cov, diff --git a/mne/morph.py b/mne/morph.py index 5b8bfba41a7..4c987263925 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -207,7 +207,7 @@ def compute_source_morph( "with surface source estimates." ) if sparse and kind != "surface": - raise ValueError("Only surface source estimates can compute a " "sparse morph.") + raise ValueError("Only surface source estimates can compute a sparse morph.") subjects_dir = str(get_subjects_dir(subjects_dir, raise_error=True)) shape = affine = pre_affine = sdr_morph = morph_mat = None @@ -223,7 +223,7 @@ def compute_source_morph( mri_subpath = op.join("mri", "brain.mgz") mri_path_from = op.join(subjects_dir, subject_from, mri_subpath) - logger.info(' Loading %s as "from" volume' % mri_path_from) + logger.info(f' Loading {mri_path_from} as "from" volume') with warnings.catch_warnings(): mri_from = nib.load(mri_path_from) @@ -231,8 +231,8 @@ def compute_source_morph( # let's KISS and use `brain.mgz`, too mri_path_to = op.join(subjects_dir, subject_to, mri_subpath) if not op.isfile(mri_path_to): - raise OSError("cannot read file: %s" % mri_path_to) - logger.info(' Loading %s as "to" volume' % mri_path_to) + raise OSError(f"cannot read file: {mri_path_to}") + logger.info(f' Loading {mri_path_to} as "to" volume') with warnings.catch_warnings(): mri_to = nib.load(mri_path_to) @@ -602,9 +602,7 @@ def compute_vol_morph_mat(self, *, verbose=None): """ if self.affine is None or self.vol_morph_mat is not None: return - logger.info( - "Computing sparse volumetric morph matrix " "(will take some time...)" - ) + logger.info("Computing sparse volumetric morph matrix (will take some time...)") self.vol_morph_mat = self._morph_vols(None, "Vertex") return self @@ -735,7 +733,7 @@ def _morph_vols(self, vols, mesg, subselect=True): return img_to def __repr__(self): # noqa: D105 - s = "%s" % self.kind + s = f"{self.kind}" s += f", {self.subject_from} -> {self.subject_to}" if self.kind == "volume": s += f", zooms : {self.zooms}" @@ -746,7 +744,7 @@ def __repr__(self): # noqa: D105 s += f", smooth : {self.smooth}" s += ", xhemi" if self.xhemi else "" - return "" % s + return f"" @verbose def save(self, fname, overwrite=False, verbose=None): diff --git a/mne/morph_map.py b/mne/morph_map.py index 643cacf8dea..618cacd3272 100644 --- a/mne/morph_map.py +++ b/mne/morph_map.py @@ -74,7 +74,7 @@ def read_morph_map( try: os.mkdir(mmap_dir) except Exception: - warn('Could not find or make morph map directory "%s"' % mmap_dir) + warn(f'Could not find or make morph map directory "{mmap_dir}"') # filename components if xhemi: @@ -102,7 +102,7 @@ def read_morph_map( return _read_morph_map(fname, subject_from, subject_to) # if file does not exist, make it logger.info( - 'Morph map "%s" does not exist, creating it and saving it to ' "disk" % fname + f'Morph map "{fname}" does not exist, creating it and saving it to ' "disk" ) logger.info(log_msg % (subject_from, subject_to)) mmap_1 = _make_morph_map(subject_from, subject_to, subjects_dir, xhemi) @@ -144,7 +144,7 @@ def _read_morph_map(fname, subject_from, subject_to): logger.info(" Right-hemisphere map read.") if left_map is None or right_map is None: - raise ValueError("Could not find both hemispheres in %s" % fname) + raise ValueError(f"Could not find both hemispheres in {fname}") return left_map, right_map diff --git a/mne/parallel.py b/mne/parallel.py index 8f314c07477..b20dd317b27 100644 --- a/mne/parallel.py +++ b/mne/parallel.py @@ -144,7 +144,7 @@ def _check_n_jobs(n_jobs): n_jobs = _ensure_int(n_jobs, "n_jobs", must_be="an int or None") if os.getenv("MNE_FORCE_SERIAL", "").lower() in ("true", "1") and n_jobs != 1: n_jobs = 1 - logger.info("... MNE_FORCE_SERIAL set. Processing in forced " "serial mode.") + logger.info("... MNE_FORCE_SERIAL set. Processing in forced serial mode.") elif n_jobs <= 0: n_cores = multiprocessing.cpu_count() n_jobs_orig = n_jobs diff --git a/mne/preprocessing/_csd.py b/mne/preprocessing/_csd.py index 544ac0364d2..271b97387a2 100644 --- a/mne/preprocessing/_csd.py +++ b/mne/preprocessing/_csd.py @@ -127,16 +127,16 @@ def compute_current_source_density( _validate_type(lambda2, "numeric", "lambda2") if not 0 <= lambda2 < 1: - raise ValueError("lambda2 must be between 0 and 1, got %s" % lambda2) + raise ValueError(f"lambda2 must be between 0 and 1, got {lambda2}") _validate_type(stiffness, "numeric", "stiffness") if stiffness < 0: - raise ValueError("stiffness must be non-negative got %s" % stiffness) + raise ValueError(f"stiffness must be non-negative got {stiffness}") n_legendre_terms = _ensure_int(n_legendre_terms, "n_legendre_terms") if n_legendre_terms < 1: raise ValueError( - "n_legendre_terms must be greater than 0, " "got %s" % n_legendre_terms + "n_legendre_terms must be greater than 0, " f"got {n_legendre_terms}" ) if isinstance(sphere, str) and sphere == "auto": @@ -155,7 +155,7 @@ def compute_current_source_density( _validate_type(z, "numeric", "z") _validate_type(radius, "numeric", "radius") if radius <= 0: - raise ValueError("sphere radius must be greater than 0, " "got %s" % radius) + raise ValueError("sphere radius must be greater than 0, " f"got {radius}") pos = np.array([inst.info["chs"][pick]["loc"][:3] for pick in picks]) if not np.isfinite(pos).all() or np.isclose(pos, 0.0).all(1).any(): @@ -267,9 +267,7 @@ def compute_bridged_electrodes( inst = inst.copy() # don't modify original picks = pick_types(inst.info, eeg=True) if len(picks) == 0: - raise RuntimeError( - "No EEG channels found, cannot compute " "electrode bridging" - ) + raise RuntimeError("No EEG channels found, cannot compute electrode bridging") # first, filter inst.filter(l_freq=l_freq, h_freq=h_freq, picks=picks, verbose=False) diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index 6b69bc9abca..a519f339ab9 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -541,9 +541,7 @@ def annotate_break( ) if not annotations: - raise ValueError( - "Could not find (or generate) any annotations in " "your data." - ) + raise ValueError("Could not find (or generate) any annotations in your data.") # Only keep annotations of interest and extract annotated time periods # Ignore case diff --git a/mne/preprocessing/bads.py b/mne/preprocessing/bads.py index 839f5774b80..39af59d8800 100644 --- a/mne/preprocessing/bads.py +++ b/mne/preprocessing/bads.py @@ -40,7 +40,7 @@ def _find_outliers(X, threshold=3.0, max_iter=2, tail=0): elif tail == -1: this_z = -zscore(X) else: - raise ValueError("Tail parameter %s not recognised." % tail) + raise ValueError(f"Tail parameter {tail} not recognised.") local_bad = this_z > threshold my_mask = np.max([my_mask, local_bad], 0) if not np.any(local_bad): diff --git a/mne/preprocessing/ecg.py b/mne/preprocessing/ecg.py index e36319316b1..2cdcd991fae 100644 --- a/mne/preprocessing/ecg.py +++ b/mne/preprocessing/ecg.py @@ -217,7 +217,7 @@ def find_ecg_events( del reject_by_annotation idx_ecg = _get_ecg_channel_index(ch_name, raw) if idx_ecg is not None: - logger.info("Using channel %s to identify heart beats." % raw.ch_names[idx_ecg]) + logger.info(f"Using channel {raw.ch_names[idx_ecg]} to identify heart beats.") ecg = raw.get_data(picks=idx_ecg) else: ecg, _ = _make_ecg(raw, start=None, stop=None) @@ -332,8 +332,7 @@ def _get_ecg_channel_index(ch_name, inst): if len(ecg_idx) > 1: warn( - "More than one ECG channel found. Using only %s." - % inst.ch_names[ecg_idx[0]] + f"More than one ECG channel found. Using only {inst.ch_names[ecg_idx[0]]}." ) return ecg_idx[0] diff --git a/mne/preprocessing/eog.py b/mne/preprocessing/eog.py index 2cd209a9b5f..aa7c15ad33a 100644 --- a/mne/preprocessing/eog.py +++ b/mne/preprocessing/eog.py @@ -70,7 +70,7 @@ def find_eog_events( # Getting EOG Channel eog_inds = _get_eog_channel_index(ch_name, raw) eog_names = np.array(raw.ch_names)[eog_inds] # for logging - logger.info("EOG channel index for this subject is: %s" % eog_inds) + logger.info(f"EOG channel index for this subject is: {eog_inds}") # Reject bad segments. reject_by_annotation = "omit" if reject_by_annotation else None diff --git a/mne/preprocessing/eyetracking/eyetracking.py b/mne/preprocessing/eyetracking/eyetracking.py index 883cf1934c6..0e66fdc0eb5 100644 --- a/mne/preprocessing/eyetracking/eyetracking.py +++ b/mne/preprocessing/eyetracking/eyetracking.py @@ -75,9 +75,7 @@ def set_channel_types_eyetrack(inst, mapping): # loop over channels for ch_name, ch_desc in mapping.items(): if ch_name not in ch_names: - raise ValueError( - "This channel name (%s) doesn't exist in " "info." % ch_name - ) + raise ValueError(f"This channel name ({ch_name}) doesn't exist in info.") c_ind = ch_names.index(ch_name) # set ch_type and unit diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 78d35119f29..6cdd95244ae 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -464,7 +464,7 @@ def __init__( ) if isinstance(val, int_like) and val == 1: raise ValueError( - f"Selecting one component with {kind}={val} is not " "supported" + f"Selecting one component with {kind}={val} is not supported" ) self.current_fit = "unfitted" @@ -1067,7 +1067,7 @@ def _get_picks(self, inst): elif isinstance(inst, Evoked): kind, do = "Evoked", "doesn't" else: - raise ValueError("Data input must be of Raw, Epochs or Evoked " "type") + raise ValueError("Data input must be of Raw, Epochs or Evoked type") raise RuntimeError( "%s %s match fitted data: %i channels " "fitted but %i channels supplied. \nPlease " @@ -1263,7 +1263,7 @@ def get_sources(self, inst, add_channels=None, start=None, stop=None): ) sources = self._sources_as_evoked(inst, add_channels) else: - raise ValueError("Data input must be of Raw, Epochs or Evoked " "type") + raise ValueError("Data input must be of Raw, Epochs or Evoked type") return sources def _sources_as_raw(self, raw, add_channels, start, stop): @@ -1448,14 +1448,14 @@ def score_sources( ) sources = self._transform_evoked(inst) else: - raise ValueError("Data input must be of Raw, Epochs or Evoked " "type") + raise ValueError("Data input must be of Raw, Epochs or Evoked type") if target is not None: # we can have univariate metrics without target target = self._check_target(target, inst, start, stop, reject_by_annotation) if sources.shape[-1] != target.shape[-1]: raise ValueError( - "Sources and target do not have the same " "number of time slices." + "Sources and target do not have the same number of time slices." ) # auto target selection if isinstance(inst, BaseRaw): @@ -1705,7 +1705,7 @@ def find_bads_ecg( if method == "ctps": if threshold == "auto": threshold = self._get_ctps_threshold() - logger.info("Using threshold: %.2f for CTPS ECG detection" % threshold) + logger.info(f"Using threshold: {threshold:.2f} for CTPS ECG detection") if isinstance(inst, BaseRaw): sources = self.get_sources( create_ecg_epochs( @@ -1726,9 +1726,7 @@ def find_bads_ecg( elif isinstance(inst, BaseEpochs): sources = self.get_sources(inst).get_data(copy=False) else: - raise ValueError( - "With `ctps` only Raw and Epochs input is " "supported" - ) + raise ValueError("With `ctps` only Raw and Epochs input is supported") _, p_vals, _ = ctps(sources) scores = p_vals.max(-1) ecg_idx = np.where(scores >= threshold)[0] @@ -1738,7 +1736,7 @@ def find_bads_ecg( self.labels_["ecg"] = list(ecg_idx) if ch_name is None: ch_name = "ECG-MAG" - self.labels_["ecg/%s" % ch_name] = list(ecg_idx) + self.labels_[f"ecg/{ch_name}"] = list(ecg_idx) elif method == "correlation": if threshold == "auto" and measure == "zscore": threshold = 3.0 @@ -1914,7 +1912,7 @@ def find_bads_ref( ref_picks = pick_types(self.info, meg=False, ref_meg=True) if not any(meg_picks) or not any(ref_picks): raise ValueError( - "ICA solution must contain both reference and" " MEG channels." + "ICA solution must contain both reference and MEG channels." ) weights = self.get_components() # take norm of component weights on reference channels for each @@ -2423,7 +2421,7 @@ def save(self, fname, *, overwrite=False, verbose=None): ) fname = _check_fname(fname, overwrite=overwrite) - logger.info("Writing ICA solution to %s..." % fname) + logger.info(f"Writing ICA solution to {fname}...") with start_and_end_file(fname) as fid: _write_ica(fid, self) return self @@ -2797,7 +2795,7 @@ def _find_sources(sources, target, score_func): score_func = get_score_funcs().get(score_func, score_func) if not callable(score_func): - raise ValueError("%s is not a valid score_func." % score_func) + raise ValueError(f"{score_func} is not a valid score_func.") scores = ( score_func(sources, target) if target is not None else score_func(sources, 1) @@ -2830,7 +2828,7 @@ def _ica_explained_variance(ica, inst, normalize=False): raise TypeError("first argument must be an instance of ICA.") if not isinstance(inst, (BaseRaw, BaseEpochs, Evoked)): raise TypeError( - "second argument must an instance of either Raw, " "Epochs or Evoked." + "second argument must an instance of either Raw, Epochs or Evoked." ) source_data = _get_inst_data(ica.get_sources(inst)) @@ -3007,7 +3005,7 @@ def read_ica(fname, verbose=None): """ check_fname(fname, "ICA", ("-ica.fif", "-ica.fif.gz", "_ica.fif", "_ica.fif.gz")) - logger.info("Reading %s ..." % fname) + logger.info(f"Reading {fname} ...") fid, tree, _ = fiff_open(fname) try: @@ -3344,7 +3342,7 @@ def corrmap( template_fig, labelled_ics = None, None if plot is True: if is_subject: # plotting from an ICA object - ttl = f"Template from subj. {str(template[0])}" + ttl = f"Template from subj. {template[0]}" template_fig = icas[template[0]].plot_components( picks=template[1], ch_type=ch_type, @@ -3397,7 +3395,7 @@ def corrmap( _, median_corr, _, max_corrs = paths[np.argmax([path[1] for path in paths])] allmaps, indices, subjs, nones = (list() for _ in range(4)) - logger.info("Median correlation with constructed map: %0.3f" % median_corr) + logger.info(f"Median correlation with constructed map: {median_corr:0.3f}") del median_corr if plot is True: logger.info("Displaying selected ICs per subject.") diff --git a/mne/preprocessing/infomax_.py b/mne/preprocessing/infomax_.py index 0f873c9d0bd..354df38ba8f 100644 --- a/mne/preprocessing/infomax_.py +++ b/mne/preprocessing/infomax_.py @@ -320,8 +320,8 @@ def infomax( if l_rate > min_l_rate: if verbose: logger.info( - "... lowering learning rate to %g" - "\n... re-starting..." % l_rate + f"... lowering learning rate to {l_rate:g}" + "\n... re-starting..." ) else: raise ValueError( diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 8f4f5c64521..1a925dba528 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -447,7 +447,7 @@ def _prep_maxwell_filter( _check_regularize(regularize) st_correlation = float(st_correlation) if st_correlation <= 0.0 or st_correlation > 1.0: - raise ValueError("Need 0 < st_correlation <= 1., got %s" % st_correlation) + raise ValueError(f"Need 0 < st_correlation <= 1., got {st_correlation}") _check_option("coord_frame", coord_frame, ["head", "meg"]) head_frame = True if coord_frame == "head" else False recon_trans = _check_destination(destination, raw.info, head_frame) @@ -570,7 +570,7 @@ def _prep_maxwell_filter( if dist > 25.0: warn( f'Head position change is over 25 mm ' - f'({", ".join("%0.1f" % x for x in diff)}) = {dist:0.1f} mm' + f'({", ".join(f"{x:0.1f}" for x in diff)}) = {dist:0.1f} mm' ) # Reconstruct raw file object with spatiotemporal processed data @@ -584,7 +584,7 @@ def _prep_maxwell_filter( job=job, subspcorr=st_correlation, buflen=st_duration / info["sfreq"] ) logger.info( - " Processing data using tSSS with st_duration=%s" % max_st["buflen"] + f" Processing data using tSSS with st_duration={max_st['buflen']}" ) st_when = "before" if st_fixed else "after" # relative to movecomp else: @@ -879,14 +879,12 @@ def _get_coil_scale(meg_picks, mag_picks, grad_picks, mag_scale, info): """Get the magnetometer scale factor.""" if isinstance(mag_scale, str): if mag_scale != "auto": - raise ValueError( - 'mag_scale must be a float or "auto", got "%s"' % mag_scale - ) + raise ValueError(f'mag_scale must be a float or "auto", got "{mag_scale}"') if len(mag_picks) in (0, len(meg_picks)): mag_scale = 100.0 # only one coil type, doesn't matter logger.info( - " Setting mag_scale=%0.2f because only one " - "coil type is present" % mag_scale + f" Setting mag_scale={mag_scale:0.2f} because only one " + "coil type is present" ) else: # Find our physical distance between gradiometer pickup loops @@ -899,7 +897,7 @@ def _get_coil_scale(meg_picks, mag_picks, grad_picks, mag_scale, info): raise RuntimeError( "Could not automatically determine " "mag_scale, could not find one " - "proper gradiometer distance from: %s" % list(grad_base) + f"proper gradiometer distance from: {list(grad_base)}" ) grad_base = list(grad_base)[0] mag_scale = 1.0 / grad_base @@ -946,7 +944,7 @@ def _check_destination(destination, info, head_frame): return info["dev_head_t"] if not head_frame: raise RuntimeError( - "destination can only be set if using the " "head coordinate frame" + "destination can only be set if using the head coordinate frame" ) if isinstance(destination, (str, Path)): recon_trans = _get_trans(destination, "meg", "head")[0] @@ -955,7 +953,7 @@ def _check_destination(destination, info, head_frame): else: destination = np.array(destination, float) if destination.shape != (3,): - raise ValueError("destination must be a 3-element vector, " "str, or None") + raise ValueError("destination must be a 3-element vector, str, or None") recon_trans = np.eye(4) recon_trans[:3, 3] = destination recon_trans = Transform("meg", "head", recon_trans) @@ -1057,7 +1055,7 @@ def _do_tSSS( np.asarray_chkfinite(resid) t_proj = _overlap_projector(orig_in_data, resid, st_correlation) # Apply projector according to Eq. 12 in :footcite:`TauluSimola2006` - msg = " Projecting %2d intersecting tSSS component%s " "for %s" % ( + msg = " Projecting %2d intersecting tSSS component%s for %s" % ( t_proj.shape[1], _pl(t_proj.shape[1], " "), t_str, @@ -1254,7 +1252,7 @@ def _get_decomp( pS_decomp, sing = _col_norm_pinv(S_decomp.copy()) cond = sing[0] / sing[-1] if bad_condition != "ignore" and cond >= 1000.0: - msg = "Matrix is badly conditioned: %0.0f >= 1000" % cond + msg = f"Matrix is badly conditioned: {cond:0.0f} >= 1000" if bad_condition == "error": raise RuntimeError(msg) elif bad_condition == "warning": @@ -1298,7 +1296,7 @@ def _regularize( int_order, ext_order = exp["int_order"], exp["ext_order"] n_in = _get_n_moments(int_order) n_out = S_decomp.shape[1] - n_in - t_str = "%8.3f" % t + t_str = f"{t:8.3f}" if regularize is not None: # regularize='in' in_removes, out_removes = _regularize_in( int_order, ext_order, S_decomp, mag_or_fine, extended_remove @@ -1344,12 +1342,12 @@ def _get_mf_picks_fix_mags(info, int_order, ext_order, ignore_ref=False, verbose n_bases = _get_n_moments([int_order, ext_order]).sum() if n_bases > good_mask.sum(): raise ValueError( - f"Number of requested bases ({str(n_bases)}) exceeds number of " + f"Number of requested bases ({n_bases}) exceeds number of " f"good sensors ({good_mask.sum()})" ) recons = [ch for ch in meg_info["bads"]] if len(recons) > 0: - msg = " Bad MEG channels being reconstructed: %s" % recons + msg = f" Bad MEG channels being reconstructed: {recons}" else: msg = " No bad MEG channels" logger.info(msg) @@ -1379,7 +1377,7 @@ def _get_mf_picks_fix_mags(info, int_order, ext_order, ignore_ref=False, verbose ) n_kit = len(mag_picks) - mag_or_fine.sum() if n_kit > 0: - msg += " (of which %s are actually KIT gradiometers)" % n_kit + msg += f" (of which {n_kit} are actually KIT gradiometers)" logger.info(msg) return meg_picks, mag_picks, grad_picks, good_mask, mag_or_fine @@ -1396,7 +1394,7 @@ def _check_usable(inst, ignore_ref): """Ensure our data are clean.""" if inst.proj: raise RuntimeError( - "Projectors cannot be applied to data during " "Maxwell filtering." + "Projectors cannot be applied to data during Maxwell filtering." ) current_comp = inst.compensation_grade if current_comp not in (0, None) and ignore_ref: @@ -1924,8 +1922,8 @@ def _check_info(info, sss=True, tsss=True, calibration=True, ctc=True): continue if len(ent["max_info"][key]) > 0: raise RuntimeError( - "Maxwell filtering %s step has already " - "been applied, cannot reapply" % msg + f"Maxwell filtering {msg} step has already " + "been applied, cannot reapply" ) @@ -2007,7 +2005,7 @@ def _update_sss_info( max_info=max_info_dict, block_id=block_id, date=DATE_NONE, - creator="mne-python v%s" % __version__, + creator=f"mne-python v{__version__}", experimenter="", ), ) @@ -2102,11 +2100,9 @@ def _prep_fine_cal(info, fine_cal): info_to_cal[oi] = ci meg_picks = pick_types(info, meg=True, exclude=[]) if len(info_to_cal) != len(meg_picks): + bad = sorted({ch_names[pick] for pick in meg_picks} - set(fine_cal["ch_names"])) raise RuntimeError( - "Not all MEG channels found in fine calibration file, missing:\n%s" - % sorted( - list({ch_names[pick] for pick in meg_picks} - set(fine_cal["ch_names"])) - ) + f"Not all MEG channels found in fine calibration file, missing:\n{bad}" ) if len(missing): warn(f"Found cal channel{_pl(missing)} not in data: {missing}") @@ -2808,12 +2804,10 @@ def _read_cross_talk(cross_talk, ch_names): ch_names = _clean_names(ch_names, remove_whitespace=True) missing = sorted(list(set(ch_names) - set(ctc_chs))) if len(missing) != 0: - raise RuntimeError( - "Missing MEG channels in cross-talk matrix:\n%s" % missing - ) + raise RuntimeError(f"Missing MEG channels in cross-talk matrix:\n{missing}") missing = sorted(list(set(ctc_chs) - set(ch_names))) if len(missing) > 0: - warn("Not all cross-talk channels in raw:\n%s" % missing) + warn(f"Not all cross-talk channels in raw:\n{missing}") ctc_picks = [ctc_chs.index(name) for name in ch_names] ctc = sss_ctc["decoupler"][ctc_picks][:, ctc_picks] # I have no idea why, but MF transposes this for storage.. diff --git a/mne/preprocessing/nirs/_tddr.py b/mne/preprocessing/nirs/_tddr.py index a7d0af9a305..20c18ea01e8 100644 --- a/mne/preprocessing/nirs/_tddr.py +++ b/mne/preprocessing/nirs/_tddr.py @@ -51,9 +51,7 @@ def temporal_derivative_distribution_repair(raw, *, verbose=None): picks = _validate_nirs_info(raw.info) if not len(picks): - raise RuntimeError( - "TDDR should be run on optical density or " "hemoglobin data." - ) + raise RuntimeError("TDDR should be run on optical density or hemoglobin data.") for pick in picks: raw._data[pick] = _TDDR(raw._data[pick], raw.info["sfreq"]) diff --git a/mne/preprocessing/otp.py b/mne/preprocessing/otp.py index 572e99ec7e2..1d1f15c350b 100644 --- a/mne/preprocessing/otp.py +++ b/mne/preprocessing/otp.py @@ -114,7 +114,7 @@ def oversampled_temporal_projection(raw, duration=10.0, picks=None, verbose=None def _otp(data, picks_good, picks_bad): """Perform OTP on one segment of data.""" if not np.isfinite(data).all(): - raise RuntimeError("non-finite data (inf or nan) found in raw " "instance") + raise RuntimeError("non-finite data (inf or nan) found in raw instance") # demean our data data_means = np.mean(data, axis=-1, keepdims=True) data -= data_means diff --git a/mne/preprocessing/ssp.py b/mne/preprocessing/ssp.py index 271f9195416..b7eef3cbbd2 100644 --- a/mne/preprocessing/ssp.py +++ b/mne/preprocessing/ssp.py @@ -79,7 +79,7 @@ def _compute_exg_proj( raw_event = raw assert mode in ("ECG", "EOG") # internal function - logger.info("Running %s SSP computation" % mode) + logger.info(f"Running {mode} SSP computation") if mode == "ECG": events, _, _ = find_ecg_events( raw_event, diff --git a/mne/preprocessing/stim.py b/mne/preprocessing/stim.py index 2a095b73809..9b9a6a2db78 100644 --- a/mne/preprocessing/stim.py +++ b/mne/preprocessing/stim.py @@ -109,7 +109,7 @@ def fix_stim_artifact( elif isinstance(inst, BaseEpochs): if inst.reject is not None: raise RuntimeError( - "Reject is already applied. Use reject=None " "in the constructor." + "Reject is already applied. Use reject=None in the constructor." ) e_start = int(np.ceil(inst.info["sfreq"] * inst.tmin)) first_samp = s_start - e_start @@ -125,6 +125,6 @@ def fix_stim_artifact( _fix_artifact(data, window, picks, first_samp, last_samp, mode) else: - raise TypeError("Not a Raw or Epochs or Evoked (got %s)." % type(inst)) + raise TypeError(f"Not a Raw or Epochs or Evoked (got {type(inst)}).") return inst diff --git a/mne/preprocessing/tests/test_annotate_amplitude.py b/mne/preprocessing/tests/test_annotate_amplitude.py index 3618e480657..ced337f5610 100644 --- a/mne/preprocessing/tests/test_annotate_amplitude.py +++ b/mne/preprocessing/tests/test_annotate_amplitude.py @@ -323,12 +323,12 @@ def test_invalid_arguments(): # negative floats PTP with pytest.raises( ValueError, - match="Argument 'flat' should define a positive " "threshold. Provided: '-1'.", + match="Argument 'flat' should define a positive threshold. Provided: '-1'.", ): annotate_amplitude(raw, peak=None, flat=-1) with pytest.raises( ValueError, - match="Argument 'peak' should define a positive " "threshold. Provided: '-1'.", + match="Argument 'peak' should define a positive threshold. Provided: '-1'.", ): annotate_amplitude(raw, peak=-1, flat=None) @@ -351,7 +351,7 @@ def test_invalid_arguments(): # test both PTP set to None with pytest.raises( ValueError, - match="At least one of the arguments 'peak' or 'flat' " "must not be None.", + match="At least one of the arguments 'peak' or 'flat' must not be None.", ): annotate_amplitude(raw, peak=None, flat=None) diff --git a/mne/preprocessing/tests/test_csd.py b/mne/preprocessing/tests/test_csd.py index 1c9be1a86cf..cff4b834c76 100644 --- a/mne/preprocessing/tests/test_csd.py +++ b/mne/preprocessing/tests/test_csd.py @@ -73,7 +73,7 @@ def test_csd_matlab(evoked_csd_sphere): assert_allclose(evoked_csd_data, csd, atol=2e-7) with pytest.raises( - ValueError, match=("CSD already applied, " "should not be reapplied") + ValueError, match=("CSD already applied, should not be reapplied") ): compute_current_source_density(evoked_csd, sphere=sphere) @@ -124,15 +124,13 @@ def test_csd_degenerate(evoked_csd_sphere): with pytest.raises(TypeError, match="n_legendre_terms must be"): compute_current_source_density(evoked, n_legendre_terms=0.1, sphere=sphere) - with pytest.raises( - ValueError, match=("n_legendre_terms must be " "greater than 0") - ): + with pytest.raises(ValueError, match=("n_legendre_terms must be greater than 0")): compute_current_source_density(evoked, n_legendre_terms=0, sphere=sphere) with pytest.raises(ValueError, match="sphere must be"): compute_current_source_density(evoked, sphere=-0.1) - with pytest.raises(ValueError, match=("sphere radius must be " "greater than 0")): + with pytest.raises(ValueError, match=("sphere radius must be greater than 0")): compute_current_source_density(evoked, sphere=(-0.1, 0.0, 0.0, -1.0)) with pytest.raises(TypeError): diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index 6caac588229..8b0fbf25515 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -212,7 +212,7 @@ def test_warnings(): ica.fit(epochs) epochs.baseline = (epochs.tmin, 0) - with pytest.warns(RuntimeWarning, match="consider baseline-correcting.*" "again"): + with pytest.warns(RuntimeWarning, match="consider baseline-correcting.*again"): ica.apply(epochs) diff --git a/mne/preprocessing/xdawn.py b/mne/preprocessing/xdawn.py index a332da6f3a8..4a8677718c9 100644 --- a/mne/preprocessing/xdawn.py +++ b/mne/preprocessing/xdawn.py @@ -354,7 +354,7 @@ def _check_Xy(self, X, y=None): # Check data if not isinstance(X, np.ndarray) or X.ndim != 3: raise ValueError( - "X must be an array of shape (n_epochs, " "n_channels, n_samples)." + "X must be an array of shape (n_epochs, n_channels, n_samples)." ) if y is None: y = np.ones(len(X)) @@ -464,9 +464,7 @@ def fit(self, epochs, y=None): correct_overlap = isi.min() < window if epochs.baseline and correct_overlap: - raise ValueError( - "Cannot apply correct_overlap if epochs" " were baselined." - ) + raise ValueError("Cannot apply correct_overlap if epochs were baselined.") events, tmin, sfreq = None, 0.0, 1.0 if correct_overlap: diff --git a/mne/proj.py b/mne/proj.py index d72bbd27e06..92414cf2dd8 100644 --- a/mne/proj.py +++ b/mne/proj.py @@ -463,7 +463,7 @@ def sensitivity_map( ) # can only run the last couple methods if there are projectors elif mode in residual_types: - raise ValueError("No projectors used, cannot compute %s" % mode) + raise ValueError(f"No projectors used, cannot compute {mode}") _, n_dipoles = gain.shape n_locations = n_dipoles // 3 @@ -495,7 +495,7 @@ def sensitivity_map( elif mode == "dampening": sensitivity_map[k] = 1.0 - p / gz else: - raise ValueError("Unknown mode type (got %s)" % mode) + raise ValueError(f"Unknown mode type (got {mode})") # only normalize fixed and free methods if mode in ["fixed", "free"]: diff --git a/mne/rank.py b/mne/rank.py index a176a1f5431..0b3f122c202 100644 --- a/mne/rank.py +++ b/mne/rank.py @@ -294,7 +294,7 @@ def _get_rank_sss( or "in_order" not in proc_info[0]["max_info"]["sss_info"] ): raise ValueError( - "Could not find Maxfilter information in " 'info["proc_history"]. %s' % msg + f'Could not find Maxfilter information in info["proc_history"]. {msg}' ) proc_info = proc_info[0] max_info = proc_info["max_info"] diff --git a/mne/report/report.py b/mne/report/report.py index e9ed4379e8f..1786bb38078 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -632,11 +632,11 @@ def open_report(fname, **params): state = read_hdf5(fname, title="mnepython") for param in params.keys(): if param not in state: - raise ValueError("The loaded report has no attribute %s" % param) + raise ValueError(f"The loaded report has no attribute {param}") if params[param] != state[param]: raise ValueError( - "Attribute '%s' of loaded report does not " - "match the given parameter." % param + f"Attribute '{param}' of loaded report does not " + "match the given parameter." ) report = Report() report.__setstate__(state) @@ -2824,15 +2824,11 @@ def parse_folder( else: # only warn if relevant if any(_endswith(fname, "cov") for fname in fnames): - warn("`info_fname` not provided. Cannot render " "-cov.fif(.gz) files.") + warn("`info_fname` not provided. Cannot render -cov.fif(.gz) files.") if any(_endswith(fname, "trans") for fname in fnames): - warn( - "`info_fname` not provided. Cannot render " "-trans.fif(.gz) files." - ) + warn("`info_fname` not provided. Cannot render -trans.fif(.gz) files.") if any(_endswith(fname, "proj") for fname in fnames): - warn( - "`info_fname` not provided. Cannot render " "-proj.fif(.gz) files." - ) + warn("`info_fname` not provided. Cannot render -proj.fif(.gz) files.") info, sfreq = None, None cov = None diff --git a/mne/simulation/metrics/metrics.py b/mne/simulation/metrics/metrics.py index 745b0485d48..f8dddd055a8 100644 --- a/mne/simulation/metrics/metrics.py +++ b/mne/simulation/metrics/metrics.py @@ -179,7 +179,7 @@ def _check_threshold(threshold): if isinstance(threshold, str): if not threshold.endswith("%"): raise ValueError( - "Threshold if a string must end with " '"%%". Got %s.' % threshold + "Threshold if a string must end with " f'"%". Got {threshold}.' ) threshold = float(threshold[:-1]) / 100.0 threshold = float(threshold) diff --git a/mne/simulation/raw.py b/mne/simulation/raw.py index b1c3428f9df..584d64c3fdd 100644 --- a/mne/simulation/raw.py +++ b/mne/simulation/raw.py @@ -119,11 +119,11 @@ def _check_head_pos(head_pos, info, first_samp, times=None): ts.sort() dev_head_ts = [head_pos[float(tt)] for tt in ts] else: - raise TypeError("unknown head_pos type %s" % type(head_pos)) + raise TypeError(f"unknown head_pos type {type(head_pos)}") bad = ts < 0 if bad.any(): raise RuntimeError( - f"All position times must be >= 0, found {bad.sum()}/{len(bad)}" "< 0" + f"All position times must be >= 0, found {bad.sum()}/{len(bad)}< 0" ) if times is not None: bad = ts > times[-1] @@ -379,7 +379,7 @@ def simulate_raw( break del fwd else: - raise RuntimeError("Maximum number of STC iterations (%d) " "exceeded" % (n,)) + raise RuntimeError("Maximum number of STC iterations (%d) exceeded" % (n,)) raw_data = np.concatenate(raw_datas, axis=-1) raw = RawArray(raw_data, info, first_samp=first_samp, verbose=False) raw.set_annotations(raw.annotations) @@ -544,7 +544,7 @@ def _add_exg(raw, kind, head_pos, interp, n_jobs, random_state): else: if len(meg_picks) == 0: raise RuntimeError( - "Can only add ECG artifacts if MEG data " "channels are present" + "Can only add ECG artifacts if MEG data channels are present" ) exg_rr = np.array([[-R, 0, -3 * R]]) max_beats = int(np.ceil(times[-1] * 80.0 / 60.0)) @@ -582,7 +582,7 @@ def _add_exg(raw, kind, head_pos, interp, n_jobs, random_state): else: ch = None src = setup_volume_source_space(pos=dict(rr=exg_rr, nn=nn), sphere_units="mm") - _log_ch("%s simulated and trace" % kind, info, ch) + _log_ch(f"{kind} simulated and trace", info, ch) del ch, nn, noise used = np.zeros(len(raw.times), bool) diff --git a/mne/simulation/source.py b/mne/simulation/source.py index 42c88c47a46..e50575e62d5 100644 --- a/mne/simulation/source.py +++ b/mne/simulation/source.py @@ -20,6 +20,7 @@ _check_option, _ensure_events, _ensure_int, + _validate_type, check_random_state, fill_doc, warn, @@ -196,14 +197,14 @@ def simulate_sparse_stc( datas = data elif n_dipoles > len(labels): raise ValueError( - "Number of labels (%d) smaller than n_dipoles (%d) " - "is not allowed." % (len(labels), n_dipoles) + f"Number of labels ({len(labels)}) smaller than n_dipoles ({n_dipoles:d}) " + "is not allowed." ) else: if n_dipoles != len(labels): warn( "The number of labels is different from the number of " - "dipoles. %s dipole(s) will be generated." % min(n_dipoles, len(labels)) + f"dipoles. {min(n_dipoles, len(labels))} dipole(s) will be generated." ) labels = labels[:n_dipoles] if n_dipoles < len(labels) else labels @@ -429,8 +430,7 @@ def add_data(self, label, waveform, events): Events associated to the waveform(s) to specify when the activity should occur. """ - if not isinstance(label, Label): - raise ValueError("label must be a Label," "not %s" % type(label)) + _validate_type(label, Label, "label") # If it is not a list then make it one if not isinstance(waveform, list) and np.ndim(waveform) == 2: diff --git a/mne/simulation/tests/test_source.py b/mne/simulation/tests/test_source.py index d8cc42fec3b..43bf5654a8b 100644 --- a/mne/simulation/tests/test_source.py +++ b/mne/simulation/tests/test_source.py @@ -444,11 +444,9 @@ def test_source_simulator(_get_fwd_labels): ss = SourceSimulator(src) with pytest.raises(ValueError, match="No simulation parameters"): ss.get_stc() - with pytest.raises(ValueError, match="label must be a Label"): + with pytest.raises(TypeError, match="must be an instance of Label"): ss.add_data(1, wfs, events) - with pytest.raises( - ValueError, match="Number of waveforms and events " "should match" - ): + with pytest.raises(ValueError, match="Number of waveforms and events should match"): ss.add_data(mylabels[0], wfs[:2], events) with pytest.raises(ValueError, match="duration must be None or"): ss = SourceSimulator(src, tstep, tstep / 2) diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 481ae84efab..4888441bac8 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -296,8 +296,8 @@ def read_source_estimate(fname, subject=None): fname = fname[:-7] else: err = ( - "Invalid .stc filename: %r; needs to end with " - "hemisphere tag ('...-lh.stc' or '...-rh.stc')" % fname + f"Invalid .stc filename: {fname!r}; needs to end with " + "hemisphere tag ('...-lh.stc' or '...-rh.stc')" ) raise OSError(err) elif fname.endswith(".w"): @@ -306,15 +306,15 @@ def read_source_estimate(fname, subject=None): fname = fname[:-5] else: err = ( - "Invalid .w filename: %r; needs to end with " - "hemisphere tag ('...-lh.w' or '...-rh.w')" % fname + f"Invalid .w filename: {fname!r}; needs to end with " + "hemisphere tag ('...-lh.w' or '...-rh.w')" ) raise OSError(err) elif fname.endswith(".h5"): ftype = "h5" fname = fname[:-3] else: - raise RuntimeError("Unknown extension for file %s" % fname_arg) + raise RuntimeError(f"Unknown extension for file {fname_arg}") if ftype != "volume": stc_exist = [op.exists(f) for f in [fname + "-rh.stc", fname + "-lh.stc"]] @@ -329,9 +329,9 @@ def read_source_estimate(fname, subject=None): ftype = "h5" fname += "-stc" elif any(stc_exist) or any(w_exist): - raise OSError("Hemisphere missing for %r" % fname_arg) + raise OSError(f"Hemisphere missing for {fname_arg!r}") else: - raise OSError("SourceEstimate File(s) not found for: %r" % fname_arg) + raise OSError(f"SourceEstimate File(s) not found for: {fname_arg!r}") # read the files if ftype == "volume": # volume source space @@ -453,7 +453,7 @@ def guess_src_type(): Klass = MixedVectorSourceEstimate if vector else MixedSourceEstimate else: raise ValueError( - "vertices has to be either a list with one or more " "arrays or an array" + "vertices has to be either a list with one or more arrays or an array" ) # Rotate back for vector source estimates @@ -568,7 +568,7 @@ def __init__(self, data, vertices, tmin, tstep, subject=None, verbose=None): def __repr__(self): # noqa: D105 s = "%d vertices" % (sum(len(v) for v in self.vertices),) if self.subject is not None: - s += ", subject : %s" % self.subject + s += f", subject : {self.subject}" s += ", tmin : %s (ms)" % (1e3 * self.tmin) s += ", tmax : %s (ms)" % (1e3 * self.times[-1]) s += ", tstep : %s (ms)" % (1e3 * self.tstep) @@ -2504,7 +2504,7 @@ def in_label(self, label, mri, src, *, verbose=None): if isinstance(label, str): volume_label = [label] else: - volume_label = {"Volume ID %s" % (label): _ensure_int(label)} + volume_label = {f"Volume ID {label}": _ensure_int(label)} label = _volume_labels(src, (mri, volume_label), mri_resolution=False) assert len(label) == 1 label = label[0] @@ -2689,7 +2689,7 @@ def save(self, fname, ftype="stc", *, overwrite=False, verbose=None): ) if ftype != "h5" and self.data.dtype == "complex": raise ValueError( - "Can only write non-complex data to .stc or .w" ", use .h5 instead" + "Can only write non-complex data to .stc or .w, use .h5 instead" ) if ftype == "stc": logger.info("Writing STC to disk...") @@ -3078,10 +3078,10 @@ def _spatio_temporal_src_adjacency_surf(src, n_times): missing = 100 * float(len(masks) - np.sum(masks)) / len(masks) if missing: warn( - "%0.1f%% of original source space vertices have been" + f"{missing:0.1f}% of original source space vertices have been" " omitted, tri-based adjacency will have holes.\n" "Consider using distance-based adjacency or " - "morphing data to all source space vertices." % missing + "morphing data to all source space vertices." ) masks = np.tile(masks, n_times) masks = np.where(masks)[0] @@ -3491,7 +3491,7 @@ def _prepare_label_extraction(stc, labels, src, mode, allow_empty, use_sparse): this_vertices = np.intersect1d(vertno[1], slabel.vertices) vertidx = nvert[0] + np.searchsorted(vertno[1], this_vertices) else: - raise ValueError("label %s has invalid hemi" % label.name) + raise ValueError(f"label {label.name} has invalid hemi") this_vertidx.append(vertidx) # convert it to an array @@ -3575,9 +3575,7 @@ def _volume_labels(src, labels, mri_resolution): if atlas_values.dtype.kind == "f": # MGZ will be 'i' atlas_values = atlas_values[np.isfinite(atlas_values)] if not (atlas_values == np.round(atlas_values)).all(): - raise RuntimeError( - "Non-integer values present in atlas, cannot " "labelize" - ) + raise RuntimeError("Non-integer values present in atlas, cannot labelize") atlas_values = np.round(atlas_values).astype(np.int64) if infer_labels: labels = { @@ -3597,7 +3595,7 @@ def _volume_labels(src, labels, mri_resolution): vox_mri_t, want = vox_mri_t["trans"], want["trans"] if not np.allclose(vox_mri_t, want, atol=1e-6): raise RuntimeError( - "atlas vox_mri_t does not match that used to create the source " "space" + "atlas vox_mri_t does not match that used to create the source space" ) src_shape = tuple(src[0]["mri_" + k] for k in ("width", "height", "depth")) atlas_shape = atlas_data.shape diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index 7f2910cbaad..87ec81a5ec7 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -453,8 +453,8 @@ def __repr__(self): # noqa: D105 r += " (%s), n_vertices=%i" % (_get_hemi(ss)[0], ss["np"]) r += ", n_used=%i" % (ss["nuse"],) if si == 0: - extra += ["%s coords" % (_coord_frame_name(int(ss["coord_frame"])))] - ss_repr.append("<%s>" % r) + extra += [_coord_frame_name(int(ss["coord_frame"])) + " coords"] + ss_repr.append(f"<{r}>") subj = self._subject if subj is not None: extra += [f"subject {repr(subj)}"] @@ -636,7 +636,7 @@ def export_volume( elif src["type"] in ("surf", "discrete"): src_types["surface_discrete"].append(src) else: - raise ValueError("Unrecognized source type: %s." % src["type"]) + raise ValueError(f"Unrecognized source type: {src['type']}.") # Raise error if there are no volume source spaces if len(src_types["volume"]) == 0: @@ -682,7 +682,7 @@ def export_volume( # read the lookup table value for segmented volume if "seg_name" not in vs: raise ValueError( - "Volume sources should be segments, " "not the entire volume." + "Volume sources should be segments, not the entire volume." ) # find the color value for this volume use_id = 1.0 @@ -1091,7 +1091,7 @@ def _read_one_source_space(fid, this): res["inuse"] = tag.data.astype(np.int64).T if len(res["inuse"]) != res["np"]: - raise ValueError("Incorrect number of entries in source space " "selection") + raise ValueError("Incorrect number of entries in source space selection") res["vertno"] = np.where(res["inuse"])[0] @@ -1326,7 +1326,7 @@ def _write_one_source_space(fid, this, verbose=None): elif this["type"] == "discrete": src_type = FIFF.FIFFV_MNE_SPACE_DISCRETE else: - raise ValueError("Unknown source space type (%s)" % this["type"]) + raise ValueError(f"Unknown source space type ({this['type']})") write_int(fid, FIFF.FIFF_MNE_SOURCE_SPACE_TYPE, src_type) if this["id"] >= 0: write_int(fid, FIFF.FIFF_MNE_SOURCE_SPACE_ID, this["id"]) @@ -1458,14 +1458,14 @@ def _check_spacing(spacing, verbose=None): else: src_type_str = f"{stype} = {sval}" if stype == "ico": - logger.info("Icosahedron subdivision grade %s" % sval) + logger.info(f"Icosahedron subdivision grade {sval}") ico_surf = _get_ico_surface(sval) elif stype == "oct": - logger.info("Octahedron subdivision grade %s" % sval) + logger.info(f"Octahedron subdivision grade {sval}") ico_surf = _tessellate_sphere_surf(sval) else: assert stype == "spacing" - logger.info("Approximate spacing %s mm" % sval) + logger.info(f"Approximate spacing {sval} mm") ico_surf = sval return stype, sval, ico_surf, src_type_str @@ -1531,9 +1531,9 @@ def setup_source_space( raise OSError(f"Could not find the {hemi} surface {surf}") logger.info("Setting up the source space with the following parameters:\n") - logger.info("SUBJECTS_DIR = %s" % subjects_dir) - logger.info("Subject = %s" % subject) - logger.info("Surface = %s" % surface) + logger.info(f"SUBJECTS_DIR = {subjects_dir}") + logger.info(f"Subject = {subject}") + logger.info(f"Surface = {surface}") stype, sval, ico_surf, src_type_str = _check_spacing(spacing) logger.info("") del spacing @@ -1549,7 +1549,7 @@ def setup_source_space( f'Doing the {dict(ico="icosa", oct="octa")[stype]}hedral vertex picking...' ) for hemi, surf in zip(["lh", "rh"], surfs): - logger.info("Loading %s..." % surf) + logger.info(f"Loading {surf}...") # Setup the surface spacing in the MRI coord frame if stype != "all": logger.info("Mapping %s %s -> %s (%d) ..." % (hemi, subject, stype, sval)) @@ -1814,7 +1814,7 @@ def setup_volume_source_space( surf_extra = "dict()" else: if not op.isfile(surface): - raise OSError('surface file "%s" not found' % surface) + raise OSError(f'surface file "{surface}" not found') surf_extra = surface logger.info("Boundary surface file : %s", surf_extra) else: @@ -1834,7 +1834,7 @@ def setup_volume_source_space( pos = float(pos) except (TypeError, ValueError): raise ValueError( - "pos must be a dict, or something that can be " "cast to float()" + "pos must be a dict, or something that can be cast to float()" ) if not isinstance(pos, float): logger.info("Source location file : %s", pos_extra) @@ -1842,16 +1842,16 @@ def setup_volume_source_space( logger.info("Assuming input in MRI coordinates") if isinstance(pos, float): - logger.info("grid : %.1f mm" % pos) - logger.info("mindist : %.1f mm" % mindist) + logger.info(f"grid : {pos:.1f} mm") + logger.info(f"mindist : {mindist:.1f} mm") pos /= 1000.0 # convert pos from m to mm if exclude > 0.0: - logger.info("Exclude : %.1f mm" % exclude) + logger.info(f"Exclude : {exclude:.1f} mm") vol_info = dict() if mri is not None: - logger.info("MRI volume : %s" % mri) + logger.info(f"MRI volume : {mri}") logger.info("") - logger.info("Reading %s..." % mri) + logger.info(f"Reading {mri}...") vol_info = _get_mri_info_data(mri, data=volume_label is not None) exclude /= 1000.0 # convert exclude from m to mm @@ -1883,7 +1883,7 @@ def setup_volume_source_space( f"BEM is not in MRI coordinates, got " f"{_coord_frame_name(surf['coord_frame'])}" ) - logger.info("Taking inner skull from %s" % bem) + logger.info(f"Taking inner skull from {bem}") elif surface is not None: if isinstance(surface, str): # read the surface in the MRI coordinate frame @@ -2121,7 +2121,7 @@ def _make_volume_source_space( sp["inuse"][bads] = False sp["nuse"] -= len(bads) logger.info( - "%d sources after omitting infeasible sources not within " "%0.1f - %0.1f mm.", + "%d sources after omitting infeasible sources not within %0.1f - %0.1f mm.", sp["nuse"], 1000 * exclude, 1000 * maxdist, @@ -2151,7 +2151,7 @@ def _make_volume_source_space( else: if not do_neighbors: raise RuntimeError( - "volume_label cannot be None unless " "do_neighbors is True" + "volume_label cannot be None unless do_neighbors is True" ) sps = list() orig_sp = sp @@ -2540,7 +2540,7 @@ def _filter_source_spaces(surf, limit, mri_head_t, src, n_jobs=None, verbose=Non logger.info(out_str) out_str = "Checking that the sources are inside the surface" if limit > 0.0: - out_str += " and at least %6.1f mm away" % (limit) + out_str += f" and at least {limit:6.1f} mm away" logger.info(out_str + " (will take a few...)") # fit a sphere to a surf quickly @@ -2625,8 +2625,8 @@ def _ensure_src(src, kind=None, extra="", verbose=None): if _path_like(src): src = str(src) if not op.isfile(src): - raise OSError('Source space file "%s" not found' % src) - logger.info("Reading %s..." % src) + raise OSError(f'Source space file "{src}" not found') + logger.info(f"Reading {src}...") src = read_source_spaces(src, verbose=False) if not isinstance(src, SourceSpaces): raise ValueError(f"{msg}, got {src} (type {type(src)})") @@ -2646,7 +2646,7 @@ def _ensure_src_subject(src, subject): if subject is None: subject = src_subject if subject is None: - raise ValueError("source space is too old, subject must be " "provided") + raise ValueError("source space is too old, subject must be provided") elif src_subject is not None and subject != src_subject: raise ValueError( f'Mismatch between provided subject "{subject}" and subject ' @@ -2704,7 +2704,7 @@ def add_source_space_distances(src, dist_limit=np.inf, n_jobs=None, *, verbose=N raise ValueError(f"dist_limit must be non-negative, got {dist_limit}") patch_only = dist_limit == 0 if src.kind != "surface": - raise RuntimeError("Currently all source spaces must be of surface " "type") + raise RuntimeError("Currently all source spaces must be of surface type") parallel, p_fun, n_jobs = parallel_func(_do_src_distances, n_jobs) min_dists = list() @@ -2867,7 +2867,7 @@ def _get_hemi(s): elif s["id"] == FIFF.FIFFV_MNE_SURF_RIGHT_HEMI: return "rh", 1, s["id"] else: - raise ValueError("unknown surface ID %s" % s["id"]) + raise ValueError(f"unknown surface ID {s['id']}") def _get_vertex_map_nn( @@ -3055,7 +3055,7 @@ def _get_morph_src_reordering( ): raise RuntimeError( "Could not map vertices, perhaps the wrong " - 'subject "%s" was provided?' % subject_from + f'subject "{subject_from}" was provided?' ) # And our data have been implicitly remapped by the forced ascending @@ -3086,7 +3086,7 @@ def _compare_source_spaces(src0, src1, mode="exact", nearest=True, dist_tol=1.5e ) if mode != "exact" and "approx" not in mode: # 'nointerp' can be appended - raise RuntimeError("unknown mode %s" % mode) + raise RuntimeError(f"unknown mode {mode}") for si, (s0, s1) in enumerate(zip(src0, src1)): # first check the keys @@ -3162,7 +3162,7 @@ def _compare_source_spaces(src0, src1, mode="exact", nearest=True, dist_tol=1.5e ) assert_equal(len(s0["vertno"]), len(s1["vertno"])) agreement = np.mean(s0["inuse"] == s1["inuse"]) - assert_(agreement >= 0.99, "%s < 0.99" % agreement) + assert_(agreement >= 0.99, f"{agreement} < 0.99") if agreement < 1.0: # make sure mismatched vertno are within 1.5mm v0 = np.setdiff1d(s0["vertno"], s1["vertno"]) @@ -3186,9 +3186,9 @@ def _compare_source_spaces(src0, src1, mode="exact", nearest=True, dist_tol=1.5e assert_equal(src0.info[name], src1.info[name]) else: # 'approx' in mode: if name in src0.info: - assert_(name in src1.info, '"%s" missing' % name) + assert_(name in src1.info, f'"{name}" missing') else: - assert_(name not in src1.info, '"%s" should not exist' % name) + assert_(name not in src1.info, f'"{name}" should not exist') def _set_source_space_vertices(src, vertices): diff --git a/mne/stats/cluster_level.py b/mne/stats/cluster_level.py index 32243eeeff0..835c0d85427 100644 --- a/mne/stats/cluster_level.py +++ b/mne/stats/cluster_level.py @@ -556,7 +556,7 @@ def _find_clusters_1dir(x, x_in, adjacency, max_step, t_power, ndimage): else: if x.ndim > 1: raise Exception( - "Data should be 1D when using a adjacency " "to define clusters." + "Data should be 1D when using a adjacency to define clusters." ) if isinstance(adjacency, sparse.spmatrix) or adjacency is False: clusters = _get_components(x_in, adjacency) @@ -619,7 +619,7 @@ def _pval_from_histogram(T, H0, tail): def _setup_adjacency(adjacency, n_tests, n_times): if not sparse.issparse(adjacency): raise ValueError( - "If adjacency matrix is given, it must be a " "SciPy sparse matrix." + "If adjacency matrix is given, it must be a SciPy sparse matrix." ) if adjacency.shape[0] == n_tests: # use global algorithm adjacency = adjacency.tocoo() diff --git a/mne/stats/regression.py b/mne/stats/regression.py index c9c6c63a5dc..e8d4e977884 100644 --- a/mne/stats/regression.py +++ b/mne/stats/regression.py @@ -75,7 +75,7 @@ def linear_regression(inst, design_matrix, names=None): exclude=["bads"], ) if [inst.ch_names[p] for p in picks] != inst.ch_names: - warn("Fitting linear model to non-data or bad channels. " "Check picking") + warn("Fitting linear model to non-data or bad channels. Check picking") msg = "Fitting linear model to epochs" data = inst.get_data(copy=False) out = EvokedArray(np.zeros(data.shape[1:]), inst.info, inst.tmin) @@ -88,7 +88,7 @@ def linear_regression(inst, design_matrix, names=None): out = inst[0] data = np.array([i.data for i in inst]) else: - raise ValueError("Input must be epochs or iterable of source " "estimates") + raise ValueError("Input must be epochs or iterable of source estimates") logger.info(msg + f", ({np.prod(data.shape[1:])} targets, {len(names)} regressors)") lm_params = _fit_lm(data, design_matrix, names) lm = namedtuple("lm", "beta stderr t_val p_val mlog10_p_val") @@ -116,7 +116,7 @@ def _fit_lm(data, design_matrix, names): if n_samples != n_rows: raise ValueError( - "Number of rows in design matrix must be equal " "to number of observations" + "Number of rows in design matrix must be equal to number of observations" ) if n_predictors != len(names): raise ValueError( diff --git a/mne/surface.py b/mne/surface.py index 62e689b6dc6..24279e58f2c 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -107,15 +107,7 @@ def _get_head_surface(subject, source, subjects_dir, on_defects, raise_error=Tru # Load the head surface from the BEM subjects_dir = str(get_subjects_dir(subjects_dir, raise_error=True)) - if not isinstance(subject, str): - raise TypeError( - "subject must be a string, not %s." - % ( - type( - subject, - ) - ) - ) + _validate_type(subject, str, "subject") # use realpath to allow for linked surfaces (c.f. MNE manual 196-197) if isinstance(source, str): source = [source] @@ -136,7 +128,7 @@ def _get_head_surface(subject, source, subjects_dir, on_defects, raise_error=Tru # let's do a more sophisticated search path = op.join(subjects_dir, subject, "bem") if not op.isdir(path): - raise OSError('Subject bem directory "%s" does not exist.' % path) + raise OSError(f'Subject bem directory "{path}" does not exist.') files = sorted(glob(op.join(path, f"{subject}*{this_source}.fif"))) for this_head in files: try: @@ -162,7 +154,7 @@ def _get_head_surface(subject, source, subjects_dir, on_defects, raise_error=Tru ) else: return surf - logger.info("Using surface from %s." % this_head) + logger.info(f"Using surface from {this_head}.") return surf @@ -212,7 +204,7 @@ def get_meg_helmet_surf(info, trans=None, *, verbose=None): system, have_helmet = _get_meg_system(info) if have_helmet: - logger.info("Getting helmet for system %s" % system) + logger.info(f"Getting helmet for system {system}") fname = _helmet_path / f"{system}.fif.gz" surf = read_bem_surfaces( fname, False, FIFF.FIFFV_MNE_SURF_MEG_HELMET, verbose=False @@ -516,7 +508,7 @@ def complete_surface_info( surf["tri_area"] = _normalize_vectors(surf["tri_nn"]) / 2.0 zidx = np.where(surf["tri_area"] == 0)[0] if len(zidx) > 0: - logger.info(" Warning: zero size triangles: %s" % zidx) + logger.info(f" Warning: zero size triangles: {zidx}") # Find neighboring triangles, accumulate vertex normals, normalize logger.info(" Triangle neighbors and vertex normals...") @@ -538,13 +530,14 @@ def complete_surface_info( surf["neighbor_tri"][ni] = np.array([], int) if len(zero) > 0: logger.info( - " Vertices do not have any neighboring " - "triangles: [%s]" % ", ".join(str(z) for z in zero) + " Vertices do not have any neighboring triangles: " + f"[{', '.join(str(z) for z in zero)}]" ) if len(fewer) > 0: + fewer = ", ".join(str(f) for f in fewer) logger.info( - " Vertices have fewer than three neighboring " - "triangles, removing neighbors: [%s]" % ", ".join(str(f) for f in fewer) + " Vertices have fewer than three neighboring triangles, removing " + f"neighbors: [{fewer}]" ) # Determine the neighboring vertices and fix errors @@ -1224,7 +1217,7 @@ def _create_surf_spacing(surf, hemi, subject, stype, ico_surf, subjects_dir): else: # ico or oct # ## from mne_ico_downsample.c ## # surf_name = subjects_dir / subject / "surf" / f"{hemi}.sphere" - logger.info("Loading geometry from %s..." % surf_name) + logger.info(f"Loading geometry from {surf_name}...") from_surf = read_surface(surf_name, return_dict=True)[-1] _normalize_vectors(from_surf["rr"]) if from_surf["np"] != surf["np"]: @@ -1246,7 +1239,7 @@ def _create_surf_spacing(surf, hemi, subject, stype, ico_surf, subjects_dir): inds = np.where(np.logical_not(surf["inuse"][neigh]))[0] if len(inds) == 0: raise RuntimeError( - "Could not find neighbor for vertex " "%d / %d" % (k, nmap) + "Could not find neighbor for vertex %d / %d" % (k, nmap) ) else: mmap[k] = neigh[inds[-1]] @@ -1265,7 +1258,7 @@ def _create_surf_spacing(surf, hemi, subject, stype, ico_surf, subjects_dir): ) surf["inuse"][mmap[k]] = True - logger.info("Setting up the triangulation for the decimated " "surface...") + logger.info("Setting up the triangulation for the decimated surface...") surf["use_tris"] = np.array([mmap[ist] for ist in ico_surf["tris"]], np.int32) if surf["use_tris"] is not None: surf["nuse_tri"] = len(surf["use_tris"]) @@ -1411,10 +1404,10 @@ def _decimate_surface_vtk(points, triangles, n_triangles): from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkPolyData from vtkmodules.vtkFiltersCore import vtkQuadricDecimation except ImportError: - raise ValueError("This function requires the VTK package to be " "installed") + raise ValueError("This function requires the VTK package to be installed") if triangles.max() > len(points) - 1: raise ValueError( - "The triangles refer to undefined points. " "Please check your mesh." + "The triangles refer to undefined points. Please check your mesh." ) src = vtkPolyData() vtkpoints = vtkPoints() diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index c968f639e22..3a95f4c75f5 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -791,7 +791,7 @@ def test_events_from_annot_in_raw_objects(): assert isinstance(event_id, dict) assert len(event_id) > 0 for kind in ("BAD", "EDGE"): - assert "%s boundary" % kind in raw_concat.annotations.description + assert f"{kind} boundary" in raw_concat.annotations.description for key in event_id.keys(): assert kind not in key @@ -1049,7 +1049,7 @@ def test_broken_csv(tmp_path): @pytest.fixture(scope="function", params=("ch_names",)) def dummy_annotation_txt_file(tmp_path_factory, ch_names): """Create txt file for testing.""" - content = "3.14, 42, AA \n" "6.28, 48, BB" + content = "3.14, 42, AA \n6.28, 48, BB" if ch_names: content = content.splitlines() content[0] = content[0].strip() + "," @@ -1142,7 +1142,7 @@ def test_read_annotation_txt_one_segment(tmp_path): def test_read_annotation_txt_empty(tmp_path): """Test empty TXT input/output.""" - content = "# MNE-Annotations\n" "# onset, duration, description\n" + content = "# MNE-Annotations\n# onset, duration, description\n" fname = tmp_path / "empty-annotations.txt" with open(fname, "w") as f: f.write(content) diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py index 3217205ba9f..0d3d821c0ef 100644 --- a/mne/tests/test_bem.py +++ b/mne/tests/test_bem.py @@ -70,7 +70,7 @@ def _compare_bem_surfaces(surfs_1, surfs_2): s1[name], rtol=1e-3, atol=1e-6, - err_msg='Mismatch: "%s"' % name, + err_msg=f'Mismatch: "{name}"', ) @@ -94,7 +94,7 @@ def _compare_bem_solutions(sol_a, sol_b): assert sol_a["solver"] == sol_b["solver"] for key in names[:-1]: assert_allclose( - sol_a[key], sol_b[key], rtol=1e-3, atol=1e-5, err_msg="Mismatch: %s" % key + sol_a[key], sol_b[key], rtol=1e-3, atol=1e-5, err_msg=f"Mismatch: {key}" ) @@ -308,7 +308,7 @@ def test_bem_solution(tmp_path, cond, fname): pytest.importorskip( "openmeeg", "2.5", - reason="OpenMEEG required to fully test BEM " "solution computation", + reason="OpenMEEG required to fully test BEM solution computation", ) with catch_logging() as log: solution = make_bem_solution(model, solver="openmeeg", verbose=True) diff --git a/mne/tests/test_coreg.py b/mne/tests/test_coreg.py index 5f4c58fa8a5..f1988a329a8 100644 --- a/mne/tests/test_coreg.py +++ b/mne/tests/test_coreg.py @@ -346,9 +346,7 @@ def test_fit_matched_points(): src_pts = apply_trans(trans, tgt_pts) trans_est = fit_matched_points(src_pts, tgt_pts, translate=False, out="trans") est_pts = apply_trans(trans_est, src_pts) - assert_array_almost_equal( - tgt_pts, est_pts, 2, "fit_matched_points with " "rotation" - ) + assert_array_almost_equal(tgt_pts, est_pts, 2, "fit_matched_points with rotation") # rotation & translation trans = np.dot(translation(2, -6, 3), rotation(2, 6, 3)) @@ -356,7 +354,7 @@ def test_fit_matched_points(): trans_est = fit_matched_points(src_pts, tgt_pts, out="trans") est_pts = apply_trans(trans_est, src_pts) assert_array_almost_equal( - tgt_pts, est_pts, 2, "fit_matched_points with " "rotation and translation." + tgt_pts, est_pts, 2, "fit_matched_points with rotation and translation." ) # rotation & translation & scaling @@ -370,7 +368,7 @@ def test_fit_matched_points(): tgt_pts, est_pts, 2, - "fit_matched_points with " "rotation, translation and scaling.", + "fit_matched_points with rotation, translation and scaling.", ) # test exceeding tolerance diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py index d23452a6a0b..6c23e5321f3 100644 --- a/mne/tests/test_cov.py +++ b/mne/tests/test_cov.py @@ -620,8 +620,9 @@ def get_data(n_samples, n_features, rank, sigma): X = get_data(n_samples=n_samples, n_features=n_features, rank=rank, sigma=sigma) method_params = {"iter_n_components": [n_features + 5]} msg = ( - "You are trying to estimate %i components on matrix " "with %i features." - ) % (n_features + 5, n_features) + f"You are trying to estimate {n_features + 5} components on matrix with " + f"{n_features} features." + ) with pytest.warns(RuntimeWarning, match=msg): _auto_low_rank_model( X, mode=mode, n_jobs=n_jobs, method_params=method_params, cv=cv diff --git a/mne/tests/test_dipole.py b/mne/tests/test_dipole.py index 8f7c9508024..8b4f398b2b0 100644 --- a/mne/tests/test_dipole.py +++ b/mne/tests/test_dipole.py @@ -244,12 +244,12 @@ def test_dipole_fitting(tmp_path): # XXX possibly some OpenBLAS numerical differences make # things slightly worse for us factor = 0.7 - assert dists[0] / factor >= dists[1], "dists: %s" % dists - assert corrs[0] * factor <= corrs[1], "corrs: %s" % corrs - assert gc_dists[0] / factor >= gc_dists[1] * 0.8, "gc-dists (ori): %s" % gc_dists - assert amp_errs[0] / factor >= amp_errs[1], "amplitude errors: %s" % amp_errs + assert dists[0] / factor >= dists[1], f"dists: {dists}" + assert corrs[0] * factor <= corrs[1], f"corrs: {corrs}" + assert gc_dists[0] / factor >= gc_dists[1] * 0.8, f"gc-dists (ori): {gc_dists}" + assert amp_errs[0] / factor >= amp_errs[1], f"amplitude errors: {amp_errs}" # This one is weird because our cov/sim/picking is weird - assert gofs[0] * factor <= gofs[1] * 2, "gof: %s" % gofs + assert gofs[0] * factor <= gofs[1] * 2, f"gof: {gofs}" @testing.requires_testing_data diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index 9e59c7302e7..d5c4e6366f6 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -219,8 +219,8 @@ def test_tabs(): continue source = inspect.getsource(mod) assert "\t" not in source, ( - '"%s" has tabs, please remove them ' - "or add it to the ignore list" % modname + f'"{modname}" has tabs, please remove them ' + "or add it to the ignore list" ) @@ -286,7 +286,7 @@ def test_documented(): doc_dir = (Path(__file__).parents[2] / "doc" / "api").absolute() doc_file = doc_dir / "python_reference.rst" if not doc_file.is_file(): - pytest.skip("Documentation file not found: %s" % doc_file) + pytest.skip(f"Documentation file not found: {doc_file}") api_files = ( "covariance", "creating_from_arrays", diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index a4e9319f3e2..8e5e1f488f3 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -589,7 +589,7 @@ def my_reject_2(epoch_data): ) # Check if callable returns a tuple with reasons - bad_types = [my_reject_2, ("Hi" "Hi"), (1, 1), None] + bad_types = [my_reject_2, ("HiHi"), (1, 1), None] for val in bad_types: # protect against bad types for kwarg in ("reject", "flat"): with pytest.raises( @@ -5180,7 +5180,7 @@ def test_epochs_saving_with_annotations(tmp_path): # if metadata is added already, then an error will be raised epochs.add_annotations_to_metadata() - with pytest.raises(RuntimeError, match="Metadata for Epochs " "already contains"): + with pytest.raises(RuntimeError, match="Metadata for Epochs already contains"): epochs.add_annotations_to_metadata() # no error is raised if overwrite is True epochs.add_annotations_to_metadata(overwrite=True) diff --git a/mne/tests/test_event.py b/mne/tests/test_event.py index 7d899291232..c51b4eaed44 100644 --- a/mne/tests/test_event.py +++ b/mne/tests/test_event.py @@ -222,7 +222,7 @@ def test_find_events(): raw = read_raw_fif(raw_fname, preload=True) # let's test the defaulting behavior while we're at it extra_ends = ["", "_1"] - orig_envs = [os.getenv("MNE_STIM_CHANNEL%s" % s) for s in extra_ends] + orig_envs = [os.getenv(f"MNE_STIM_CHANNEL{s}") for s in extra_ends] os.environ["MNE_STIM_CHANNEL"] = "STI 014" if "MNE_STIM_CHANNEL_1" in os.environ: del os.environ["MNE_STIM_CHANNEL_1"] @@ -373,7 +373,7 @@ def test_find_events(): # put back the env vars we trampled on for s, o in zip(extra_ends, orig_envs): if o is not None: - os.environ["MNE_STIM_CHANNEL%s" % s] = o + os.environ[f"MNE_STIM_CHANNEL{s}"] = o # Test with list of stim channels raw._data[stim_channel_idx, 1:101] = np.zeros(100) diff --git a/mne/tests/test_line_endings.py b/mne/tests/test_line_endings.py index 8ee4f604c9f..c055ef41667 100644 --- a/mne/tests/test_line_endings.py +++ b/mne/tests/test_line_endings.py @@ -64,7 +64,7 @@ def _assert_line_endings(dir_): with open(filename, "rb") as fid: text = fid.read().decode("utf-8") except UnicodeDecodeError: - report.append("In %s found non-decodable bytes" % relfilename) + report.append(f"In {relfilename} found non-decodable bytes") else: crcount = text.count("\r") if crcount: diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index 08e08761ced..af638effc57 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -240,7 +240,7 @@ def test_volume_stc(tmp_path): stc.save(fname_vol, ftype="whatever", overwrite=True) for ftype in ["w", "h5"]: for _ in range(2): - fname_temp = tmp_path / ("temp-vol.%s" % ftype) + fname_temp = tmp_path / f"temp-vol.{ftype}" stc_new.save(fname_temp, ftype=ftype, overwrite=True) stc_new = read_source_estimate(fname_temp) assert isinstance(stc_new, VolSourceEstimate) diff --git a/mne/time_frequency/_stft.py b/mne/time_frequency/_stft.py index 50599947b90..1b4a0df89c0 100644 --- a/mne/time_frequency/_stft.py +++ b/mne/time_frequency/_stft.py @@ -62,9 +62,7 @@ def stft(x, wsize, tstep=None, verbose=None): ) if tstep > wsize / 2: - raise ValueError( - "The step size must be smaller than half the " "window length." - ) + raise ValueError("The step size must be smaller than half the window length.") n_step = int(ceil(T / float(tstep))) n_freq = wsize // 2 + 1 diff --git a/mne/time_frequency/csd.py b/mne/time_frequency/csd.py index e2ea5ac1ba7..327d6a2aa68 100644 --- a/mne/time_frequency/csd.py +++ b/mne/time_frequency/csd.py @@ -248,7 +248,7 @@ def sum(self, fmin=None, fmax=None): if any(fmin_ > fmax_ for fmin_, fmax_ in zip(fmin, fmax)): raise ValueError( - "Some lower bounds are higher than the " "corresponding upper bounds." + "Some lower bounds are higher than the corresponding upper bounds." ) # Find the index of the lower bound of each frequency bin @@ -256,7 +256,7 @@ def sum(self, fmin=None, fmax=None): fmax_inds = [self._get_frequency_index(f) + 1 for f in fmax] if len(fmin_inds) != len(fmax_inds): - raise ValueError("The length of fmin does not match the " "length of fmax.") + raise ValueError("The length of fmin does not match the length of fmax.") # Sum across each frequency bin n_bins = len(fmin_inds) @@ -330,7 +330,7 @@ def _get_frequency_index(self, freq): index = np.argmin(distance) min_dist = distance[index] if min_dist > 1: - raise IndexError("Frequency %f is not available." % freq) + raise IndexError(f"Frequency {freq:f} is not available.") return index def pick_frequency(self, freq=None, index=None): @@ -1247,9 +1247,9 @@ def _prepare_csd(epochs, tmin=None, tmax=None, picks=None, projs=None): """ tstep = epochs.times[1] - epochs.times[0] if tmin is not None and tmin < epochs.times[0] - tstep: - raise ValueError("tmin should be larger than the smallest data time " "point") + raise ValueError("tmin should be larger than the smallest data time point") if tmax is not None and tmax > epochs.times[-1] + tstep: - raise ValueError("tmax should be smaller than the largest data time " "point") + raise ValueError("tmax should be smaller than the largest data time point") if tmax is not None and tmin is not None: if tmax < tmin: raise ValueError("tmax must be larger than tmin") @@ -1289,9 +1289,9 @@ def _prepare_csd_array(X, sfreq, t0, tmin, tmax, fmin=None, fmax=None): if tmax <= tmin: raise ValueError("tmax must be larger than tmin") if tmin < times[0] - tstep: - raise ValueError("tmin should be larger than the smallest data time " "point") + raise ValueError("tmin should be larger than the smallest data time point") if tmax > times[-1] + tstep: - raise ValueError("tmax should be smaller than the largest data time " "point") + raise ValueError("tmax should be smaller than the largest data time point") # Check fmin and fmax if fmax is not None and fmin is not None and fmax <= fmin: diff --git a/mne/time_frequency/psd.py b/mne/time_frequency/psd.py index b2083c22229..fdf546b0be2 100644 --- a/mne/time_frequency/psd.py +++ b/mne/time_frequency/psd.py @@ -200,7 +200,7 @@ def psd_array_welch( # Prep the PSD n_fft, n_per_seg, n_overlap = _check_nfft(n_times, n_fft, n_per_seg, n_overlap) win_size = n_fft / float(sfreq) - logger.info("Effective window size : %0.3f (s)" % win_size) + logger.info(f"Effective window size : {win_size:0.3f} (s)") freqs = np.arange(n_fft // 2 + 1, dtype=float) * (sfreq / n_fft) freq_mask = (freqs >= fmin) & (freqs <= fmax) if not freq_mask.any(): diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 571f9683e75..d93ed6cb67d 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -165,10 +165,10 @@ def morlet(sfreq, freqs, n_cycles=7.0, sigma=None, zero_mean=False): freqs = np.array(freqs, float) if np.any(freqs <= 0): - raise ValueError("all frequencies in 'freqs' must be " "greater than 0.") + raise ValueError("all frequencies in 'freqs' must be greater than 0.") if (n_cycles.size != 1) and (n_cycles.size != len(freqs)): - raise ValueError("n_cycles should be fixed or defined for " "each frequency.") + raise ValueError("n_cycles should be fixed or defined for each frequency.") _check_option("freqs.ndim", freqs.ndim, [0, 1]) singleton = freqs.ndim == 0 if singleton: @@ -273,7 +273,7 @@ def _make_dpss( freqs = np.array(freqs) if np.any(freqs <= 0): - raise ValueError("all frequencies in 'freqs' must be " "greater than 0.") + raise ValueError("all frequencies in 'freqs' must be greater than 0.") if time_bandwidth < 2.0: raise ValueError("time_bandwidth should be >= 2.0 for good tapers") @@ -281,7 +281,7 @@ def _make_dpss( n_cycles = np.atleast_1d(n_cycles) if n_cycles.size != 1 and n_cycles.size != len(freqs): - raise ValueError("n_cycles should be fixed or defined for " "each frequency.") + raise ValueError("n_cycles should be fixed or defined for each frequency.") for m in range(n_taps): Wm = list() @@ -598,28 +598,24 @@ def _check_tfr_param( """Aux. function to _compute_tfr to check the params validity.""" # Check freqs if not isinstance(freqs, (list, np.ndarray)): - raise ValueError( - "freqs must be an array-like, got %s " "instead." % type(freqs) - ) + raise ValueError(f"freqs must be an array-like, got {type(freqs)} instead.") freqs = np.asarray(freqs, dtype=float) if freqs.ndim != 1: raise ValueError( - "freqs must be of shape (n_freqs,), got %s " - "instead." % np.array(freqs.shape) + f"freqs must be of shape (n_freqs,), got {np.array(freqs.shape)} " + "instead." ) # Check sfreq if not isinstance(sfreq, (float, int)): - raise ValueError( - "sfreq must be a float or an int, got %s " "instead." % type(sfreq) - ) + raise ValueError(f"sfreq must be a float or an int, got {type(sfreq)} instead.") sfreq = float(sfreq) # Default zero_mean = True if multitaper else False zero_mean = method == "multitaper" if zero_mean is None else zero_mean if not isinstance(zero_mean, bool): raise ValueError( - "zero_mean should be of type bool, got %s. instead" % type(zero_mean) + f"zero_mean should be of type bool, got {type(zero_mean)}. instead" ) freqs = np.asarray(freqs) @@ -635,7 +631,7 @@ def _check_tfr_param( ) else: raise ValueError( - "n_cycles must be a float or an array, got %s " "instead." % type(n_cycles) + f"n_cycles must be a float or an array, got {type(n_cycles)} instead." ) # Check time_bandwidth @@ -646,15 +642,13 @@ def _check_tfr_param( # Check use_fft if not isinstance(use_fft, bool): - raise ValueError( - "use_fft must be a boolean, got %s " "instead." % type(use_fft) - ) + raise ValueError(f"use_fft must be a boolean, got {type(use_fft)} instead.") # Check decim if isinstance(decim, int): decim = slice(None, None, decim) if not isinstance(decim, slice): raise ValueError( - "decim must be an integer or a slice, " "got %s instead." % type(decim) + "decim must be an integer or a slice, " f"got {type(decim)} instead." ) # Check output diff --git a/mne/transforms.py b/mne/transforms.py index 7a3875ef56c..b27d0ce6055 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -704,7 +704,7 @@ def get_ras_to_neuromag_trans(nasion, lpa, rpa): for pt in (nasion, lpa, rpa): if pt.ndim != 1 or len(pt) != 3: raise ValueError( - "Points have to be provided as one dimensional " "arrays of length 3." + "Points have to be provided as one dimensional arrays of length 3." ) right = rpa - lpa @@ -1159,7 +1159,7 @@ def fit( del match_rr # 2. Compute spherical harmonic coefficients for all points logger.info( - " Computing spherical harmonic approximation with " "order %s" % order + " Computing spherical harmonic approximation with " f"order {order}" ) src_sph = _compute_sph_harm(order, *src_rad_az_pol[1:]) dest_sph = _compute_sph_harm(order, *dest_rad_az_pol[1:]) @@ -1569,7 +1569,7 @@ def _read_fs_xfm(fname): """Read a Freesurfer transform from a .xfm file.""" assert fname.endswith(".xfm") with open(fname) as fid: - logger.debug("Reading FreeSurfer talairach.xfm file:\n%s" % fname) + logger.debug(f"Reading FreeSurfer talairach.xfm file:\n{fname}") # read lines until we get the string 'Linear_Transform', which precedes # the data transformation matrix @@ -1583,7 +1583,7 @@ def _read_fs_xfm(fname): break else: raise ValueError( - 'Failed to find "Linear_Transform" string in ' "xfm file:\n%s" % fname + 'Failed to find "Linear_Transform" string in ' f"xfm file:\n{fname}" ) xfm = list() @@ -1606,7 +1606,7 @@ def _write_fs_xfm(fname, xfm, kind): fid.write((kind + "\n\nTtransform_Type = Linear;\n").encode("ascii")) fid.write("Linear_Transform =\n".encode("ascii")) for li, line in enumerate(xfm[:-1]): - line = " ".join(["%0.6f" % part for part in line]) + line = " ".join([f"{part:0.6f}" for part in line]) line += "\n" if li < 2 else ";\n" fid.write(line.encode("ascii")) diff --git a/mne/utils/_bunch.py b/mne/utils/_bunch.py index 26cc4e6b17a..5795be4ad5c 100644 --- a/mne/utils/_bunch.py +++ b/mne/utils/_bunch.py @@ -63,7 +63,7 @@ def __new__(cls, name, val): # noqa: D102,D105 return out def __str__(self): # noqa: D105 - return f"{str(self.__class__.mro()[-2](self))} ({self._name})" + return f"{self.__class__.mro()[-2](self)} ({self._name})" __repr__ = __str__ diff --git a/mne/utils/_logging.py b/mne/utils/_logging.py index f4546e5e7d8..02beae6224d 100644 --- a/mne/utils/_logging.py +++ b/mne/utils/_logging.py @@ -359,7 +359,7 @@ def __getattr__(self, name): # noqa: D105 if hasattr(sys.stdout, name): return getattr(sys.stdout, name) else: - raise AttributeError("'file' object has not attribute '%s'" % name) + raise AttributeError(f"'file' object has not attribute '{name}'") _verbose_dec_re = re.compile("^$") diff --git a/mne/utils/_testing.py b/mne/utils/_testing.py index f0e76c70e8a..b60c3d0df05 100644 --- a/mne/utils/_testing.py +++ b/mne/utils/_testing.py @@ -192,7 +192,7 @@ def assert_and_remove_boundary_annot(annotations, n=1): if isinstance(annotations, BaseRaw): # allow either input annotations = annotations.annotations for key in ("EDGE", "BAD"): - idx = np.where(annotations.description == "%s boundary" % key)[0] + idx = np.where(annotations.description == f"{key} boundary")[0] assert len(idx) == n annotations.delete(idx) @@ -242,7 +242,7 @@ def _check_snr(actual, desired, picks, min_tol, med_tol, msg, kind="MEG"): # min tol snr = snrs.min() bad_count = (snrs < min_tol).sum() - msg = " (%s)" % msg if msg != "" else msg + msg = f" ({msg})" if msg != "" else msg assert bad_count == 0, ( f"SNR (worst {snr:0.2f}) < {min_tol:0.2f} " f"for {bad_count}/{len(picks)} channels{msg}" diff --git a/mne/utils/check.py b/mne/utils/check.py index 89d54b3386b..9538ed12c3e 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -193,7 +193,7 @@ def check_random_state(seed): if isinstance(seed, np.random.Generator): return seed raise ValueError( - "%r cannot be used to seed a " "numpy.random.mtrand.RandomState instance" % seed + f"{seed!r} cannot be used to seed a numpy.random.mtrand.RandomState instance" ) @@ -206,12 +206,10 @@ def _check_event_id(event_id, events): for key in event_id.keys(): _validate_type(key, str, "Event names") event_id = { - key: _ensure_int(val, "event_id[%s]" % key) for key, val in event_id.items() + key: _ensure_int(val, f"event_id[{key}]") for key, val in event_id.items() } elif isinstance(event_id, list): - event_id = [ - _ensure_int(v, "event_id[%s]" % vi) for vi, v in enumerate(event_id) - ] + event_id = [_ensure_int(v, f"event_id[{vi}]") for vi, v in enumerate(event_id)] event_id = dict(zip((str(i) for i in event_id), event_id)) else: event_id = _ensure_int(event_id, "event_id") @@ -299,9 +297,7 @@ def _check_subject( _validate_type(first, "str", f"Either {second_kind} subject or {first_kind}") return first elif raise_error is True: - raise ValueError( - f"Neither {second_kind} subject nor {first_kind} " "was a string" - ) + raise ValueError(f"Neither {second_kind} subject nor {first_kind} was a string") return None @@ -746,10 +742,10 @@ def _check_channels_spatial_filter(ch_names, filters): for ch_name in filters["ch_names"]: if ch_name not in ch_names: raise ValueError( - "The spatial filter was computed with channel %s " + f"The spatial filter was computed with channel {ch_name} " "which is not present in the data. You should " "compute a new spatial filter restricted to the " - "good data channels." % ch_name + "good data channels." ) # then compare list of channels and get selection based on data: sel = [ii for ii, ch_name in enumerate(ch_names) if ch_name in filters["ch_names"]] @@ -1054,7 +1050,7 @@ def _check_sphere(sphere, info=None, sphere_units="m"): f"{ch_name}" ) if ch_name == "Fpz": - msg += ", and was unable to approximate its location " "from Oz" + msg += ", and was unable to approximate its location from Oz" raise ValueError(msg) # Calculate the radius from: T7<->T8, Fpz<->Oz diff --git a/mne/utils/config.py b/mne/utils/config.py index e432e8c00f6..271d55b35a3 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -44,7 +44,7 @@ def set_cache_dir(cache_dir): temporary file storage. """ if cache_dir is not None and not op.exists(cache_dir): - raise OSError("Directory %s does not exist" % cache_dir) + raise OSError(f"Directory {cache_dir} does not exist") set_config("MNE_CACHE_DIR", cache_dir, set_env=False) @@ -223,8 +223,8 @@ def _load_config(config_path, raise_error=False): except ValueError: # No JSON object could be decoded --> corrupt file? msg = ( - "The MNE-Python config file (%s) is not a valid JSON " - "file and might be corrupted" % config_path + f"The MNE-Python config file ({config_path}) is not a valid JSON " + "file and might be corrupted" ) if raise_error: raise RuntimeError(msg) @@ -314,18 +314,17 @@ def get_config(key=None, default=None, raise_error=False, home_dir=None, use_env elif raise_error is True and key not in config: loc_env = "the environment or in the " if use_env else "" meth_env = ( - ('either os.environ["%s"] = VALUE for a temporary ' "solution, or " % key) + (f'either os.environ["{key}"] = VALUE for a temporary ' "solution, or ") if use_env else "" ) extra_env = ( - " You can also set the environment variable before " "running python." + " You can also set the environment variable before running python." if use_env else "" ) meth_file = ( - 'mne.utils.set_config("%s", VALUE, set_env=True) ' - "for a permanent one" % key + f'mne.utils.set_config("{key}", VALUE, set_env=True) ' "for a permanent one" ) raise KeyError( f'Key "{key}" not found in {loc_env}' @@ -367,7 +366,7 @@ def set_config(key, value, home_dir=None, set_env=True): if key not in _known_config_types and not any( key.startswith(k) for k in _known_config_wildcards ): - warn('Setting non-standard config type: "%s"' % key) + warn(f'Setting non-standard config type: "{key}"') # Read all previous values config_path = get_config_path(home_dir=home_dir) @@ -376,8 +375,7 @@ def set_config(key, value, home_dir=None, set_env=True): else: config = dict() logger.info( - "Attempting to create new mne-python configuration " - "file:\n%s" % config_path + f"Attempting to create new mne-python configuration file:\n{config_path}" ) if value is None: config.pop(key, None) @@ -837,7 +835,7 @@ def _get_latest_version(timeout): elif "timed out" in str(err): return f"timeout after {timeout} sec" else: - return f"unknown error: {str(err)}" + return f"unknown error: {err}" else: return response["tag_name"].lstrip("v") or "version unknown" diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 78e6ae7d3b5..17eb07552d8 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -3353,7 +3353,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): _picks_types = "str | array-like | slice | None" _picks_header = f"picks : {_picks_types}" _picks_desc = "Channels to include." -_picks_int = "Slices and lists of integers will be interpreted as channel " "indices." +_picks_int = "Slices and lists of integers will be interpreted as channel indices." _picks_str_types = """channel *type* strings (e.g., ``['meg', 'eeg']``) will pick channels of those types,""" _picks_str_names = """channel *name* strings (e.g., ``['MEG0111', 'MEG2623']`` @@ -5009,7 +5009,7 @@ def fill_doc(f): except (TypeError, ValueError, KeyError) as exp: funcname = f.__name__ funcname = docstring.split("\n")[0] if funcname is None else funcname - raise RuntimeError(f"Error documenting {funcname}:\n{str(exp)}") + raise RuntimeError(f"Error documenting {funcname}:\n{exp}") return f @@ -5310,7 +5310,7 @@ def linkcode_resolve(domain, info): if "dev" in mne.__version__: kind = "main" else: - kind = "maint/%s" % (".".join(mne.__version__.split(".")[:2])) + kind = "maint/" + ".".join(mne.__version__.split(".")[:2]) return f"http://github.com/mne-tools/mne-python/blob/{kind}/mne/{fn}{linespec}" @@ -5537,7 +5537,7 @@ def _docformat(docstring, docdict=None, funcname=None): try: return docstring % indented except (TypeError, ValueError, KeyError) as exp: - raise RuntimeError(f"Error documenting {funcname}:\n{str(exp)}") + raise RuntimeError(f"Error documenting {funcname}:\n{exp}") def _indentcount_lines(lines): diff --git a/mne/utils/misc.py b/mne/utils/misc.py index a86688ca2a7..88a7d7f1f80 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -246,7 +246,7 @@ def running_subprocess(command, after="wait", verbose=None, *args, **kwargs): else: command = [str(s) for s in command] command_str = " ".join(s for s in command) - logger.info("Running subprocess: %s" % command_str) + logger.info(f"Running subprocess: {command_str}") try: p = subprocess.Popen(command, *args, **kwargs) except Exception: @@ -254,7 +254,7 @@ def running_subprocess(command, after="wait", verbose=None, *args, **kwargs): command_name = command.split()[0] else: command_name = command[0] - logger.error("Command not found: %s" % command_name) + logger.error(f"Command not found: {command_name}") raise try: with ExitStack() as stack: @@ -340,7 +340,7 @@ def sizeof_fmt(num): quotient = float(num) / 1024**exponent unit = units[exponent] num_decimals = decimals[exponent] - format_string = "{0:.%sf} {1}" % (num_decimals) + format_string = f"{{0:.{num_decimals}f}} {{1}}" return format_string.format(quotient, unit) if num == 0: return "0 bytes" diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 793e399a69f..02b7eaffd17 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -69,7 +69,7 @@ def __hash__(self): _check_preload(self, "Hashing ") return object_hash(dict(info=self.info, data=self._data)) else: - raise RuntimeError("Hashing unknown object type: %s" % type(self)) + raise RuntimeError(f"Hashing unknown object type: {type(self)}") class GetEpochsMixin: diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py index 2f09689917b..508c019983b 100644 --- a/mne/utils/numerics.py +++ b/mne/utils/numerics.py @@ -178,7 +178,7 @@ def _reg_pinv(x, reg=0, rank="full", rcond=1e-15): # Warn the user if both all parameters were kept at their defaults and the # matrix is rank deficient. if (rank_after < n).any() and reg == 0 and rank == "full" and rcond == 1e-15: - warn("Covariance matrix is rank-deficient and no regularization is " "done.") + warn("Covariance matrix is rank-deficient and no regularization is done.") elif isinstance(rank, int) and rank > n: raise ValueError( "Invalid value for the rank parameter (%d) given " @@ -373,7 +373,7 @@ def _apply_scaling_cov(data, picks_list, scalings): scales[idx] = scalings[ch_t] elif isinstance(scalings, np.ndarray): if len(scalings) != len(data): - raise ValueError("Scaling factors and data are of incompatible " "shape") + raise ValueError("Scaling factors and data are of incompatible shape") scales = scalings elif scalings is None: pass @@ -404,9 +404,7 @@ def _check_scaling_inputs(data, picks_list, scalings): elif scalings is None: pass else: - raise NotImplementedError( - "No way! That's not a rescaling " "option: %s" % scalings - ) + raise NotImplementedError(f"Not a valid rescaling option: {scalings}") return scalings_ @@ -798,20 +796,20 @@ def object_diff(a, b, pre="", *, allclose=False): k2s = _sort_keys(b) m1 = set(k2s) - set(k1s) if len(m1): - out += pre + " left missing keys %s\n" % (m1) + out += pre + f" left missing keys {m1}\n" for key in k1s: if key not in k2s: - out += pre + " right missing key %s\n" % key + out += pre + f" right missing key {key}\n" else: out += object_diff( - a[key], b[key], pre=(pre + "[%s]" % repr(key)), allclose=allclose + a[key], b[key], pre=(pre + f"[{repr(key)}]"), allclose=allclose ) elif isinstance(a, (list, tuple)): if len(a) != len(b): out += pre + f" length mismatch ({len(a)}, {len(b)})\n" else: for ii, (xx1, xx2) in enumerate(zip(a, b)): - out += object_diff(xx1, xx2, pre + "[%s]" % ii, allclose=allclose) + out += object_diff(xx1, xx2, pre + f"[{ii}]", allclose=allclose) elif isinstance(a, float): if not _array_equal_nan(a, b, allclose): out += pre + f" value mismatch ({a}, {b})\n" @@ -820,7 +818,7 @@ def object_diff(a, b, pre="", *, allclose=False): out += pre + f" value mismatch ({a}, {b})\n" elif a is None: if b is not None: - out += pre + " left is None, right is not (%s)\n" % (b) + out += pre + f" left is None, right is not ({b})\n" elif isinstance(a, np.ndarray): if not _array_equal_nan(a, b, allclose): out += pre + " array mismatch\n" @@ -840,7 +838,7 @@ def object_diff(a, b, pre="", *, allclose=False): c = a - b c.eliminate_zeros() if c.nnz > 0: - out += pre + (" sparse matrix a and b differ on %s " "elements" % c.nnz) + out += pre + (f" sparse matrix a and b differ on {c.nnz} elements") elif pd and isinstance(a, pd.DataFrame): try: pd.testing.assert_frame_equal(a, b) @@ -887,7 +885,7 @@ def _fit(self, X): if n_components == "mle": if n_samples < n_features: raise ValueError( - "n_components='mle' is only supported " "if n_samples >= n_features" + "n_components='mle' is only supported if n_samples >= n_features" ) elif not 0 <= n_components <= min(n_samples, n_features): raise ValueError( diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index eb23035eb63..e5cf7ed108f 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -659,7 +659,7 @@ def plot_alignment( user_alpha[key] = float(val) if not 0 <= user_alpha[key] <= 1: raise ValueError( - f"surfaces[{repr(key)}] ({val}) must be" " between 0 and 1" + f"surfaces[{repr(key)}] ({val}) must be between 0 and 1" ) else: user_alpha = {} @@ -763,7 +763,7 @@ def plot_alignment( head_keys = ("auto", "head", "outer_skin", "head-dense", "seghead") head = [s for s in surfaces if s in head_keys] if len(head) > 1: - raise ValueError("Can only supply one head-like surface name, " f"got {head}") + raise ValueError(f"Can only supply one head-like surface name, got {head}") head = head[0] if head else False if head is not False: surfaces.pop(surfaces.index(head)) @@ -1797,7 +1797,7 @@ def _process_clim(clim, colormap, transparent, data=0.0, allow_pos_lims=True): if ("lims" in clim) + ("pos_lims" in clim) != 1: raise ValueError( - "Exactly one of lims and pos_lims must be specified " f"in clim, got {clim}" + f"Exactly one of lims and pos_lims must be specified in clim, got {clim}" ) if "pos_lims" in clim and not allow_pos_lims: raise ValueError('Cannot use "pos_lims" for clim, use "lims" ' "instead") @@ -2031,10 +2031,7 @@ def _plot_mpl_stc( from ..morph import _get_subject_sphere_tris from ..source_space._source_space import _check_spacing, _create_surf_spacing - if hemi not in ["lh", "rh"]: - raise ValueError( - "hemi must be 'lh' or 'rh' when using matplotlib. " "Got %s." % hemi - ) + _check_option("hemi", hemi, ("lh", "rh"), extra="when using matplotlib") lh_kwargs = { "lat": {"elev": 0, "azim": 180}, "med": {"elev": 0, "azim": 0}, @@ -2879,7 +2876,7 @@ def _update_timeslice(idx, params): ax_y.clear() ax_z.clear() params.update({"img_idx": index_img(img, idx)}) - params.update({"title": "Activation (t=%.3f s.)" % params["stc"].times[idx]}) + params.update({"title": f"Activation (t={params['stc'].times[idx]:.3f} s.)"}) plot_map_callback(params["img_idx"], title="", cut_coords=cut_coords) def _update_vertlabel(loc_idx): @@ -3108,7 +3105,7 @@ def _check_views(surf, views, hemi, stc=None, backend=None): if backend is not None: if backend not in ("pyvistaqt", "notebook"): raise RuntimeError( - "The PyVista 3D backend must be used to " "plot a flatmap" + "The PyVista 3D backend must be used to plot a flatmap" ) if (views == ["flat"]) ^ (surf == "flat"): # exactly only one of the two raise ValueError( @@ -3743,7 +3740,7 @@ def snapshot_brain_montage(fig, montage, hide_sensors=True): ch_names, xyz = zip(*[(ich, ixyz) for ich, ixyz in montage.items()]) else: raise TypeError( - "montage must be an instance of `DigMontage`, `Info`," " or `dict`" + "montage must be an instance of `DigMontage`, `Info`, or `dict`" ) # initialize figure diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index f4bccfc5447..95ae75dc8d8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1575,7 +1575,7 @@ def plot_time_course(self, hemi, vertex_id, color, update=True): except Exception: mni = None if mni is not None: - mni = " MNI: " + ", ".join("%5.1f" % m for m in mni) + mni = " MNI: " + ", ".join(f"{m:5.1f}" for m in mni) else: mni = "" label = f"{hemi_str}:{str(vertex_id).ljust(6)}{mni}" @@ -1893,10 +1893,10 @@ def add_data( if self._n_times is None: self._times = time elif len(time) != self._n_times: - raise ValueError("New n_times is different from previous " "n_times") + raise ValueError("New n_times is different from previous n_times") elif not np.array_equal(time, self._times): raise ValueError( - "Not all time values are consistent with " "previously set times." + "Not all time values are consistent with previously set times." ) # initial time @@ -2248,7 +2248,7 @@ def add_label( self._subjects_dir, self._subject, "label", subdir, label_fname ) if not os.path.exists(filepath): - raise ValueError("Label file %s does not exist" % filepath) + raise ValueError(f"Label file {filepath} does not exist") label = read_label(filepath) ids = label.vertices scalars = label.values @@ -3033,7 +3033,7 @@ def add_annotation( ".".join([hemi, annot, "annot"]), ) if not os.path.exists(filepath): - raise ValueError("Annotation file %s does not exist" % filepath) + raise ValueError(f"Annotation file {filepath} does not exist") filepaths += [filepath] annots = [] for hemi, filepath in zip(hemis, filepaths): @@ -3794,7 +3794,7 @@ def _save_movie_tv( def frame_callback(frame, n_frames): if frame == n_frames: # On the ImageIO step - self.status_msg.set_value("Saving with ImageIO: %s" % filename) + self.status_msg.set_value(f"Saving with ImageIO: {filename}") self.status_msg.show() self.status_progress.hide() self._renderer._status_bar_update() @@ -4028,7 +4028,7 @@ def _check_hemi(self, hemi, extras=()): if hemi is None: if self._hemi not in ["lh", "rh"]: raise ValueError( - "hemi must not be None when both " "hemispheres are displayed" + "hemi must not be None when both hemispheres are displayed" ) hemi = self._hemi _check_option("hemi", hemi, ("lh", "rh") + tuple(extras)) diff --git a/mne/viz/_brain/surface.py b/mne/viz/_brain/surface.py index 7f17cebf718..272123fa687 100644 --- a/mne/viz/_brain/surface.py +++ b/mne/viz/_brain/surface.py @@ -174,7 +174,7 @@ def z(self): def load_curvature(self): """Load in curvature values from the ?h.curv file.""" - curv_path = path.join(self.data_path, "surf", "%s.curv" % self.hemi) + curv_path = path.join(self.data_path, "surf", f"{self.hemi}.curv") if path.isfile(curv_path): self.curv = read_curvature(curv_path, binary=False) self.bin_curv = np.array(self.curv > 0, np.int64) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index c8252070a32..d58682bb5f3 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -1042,8 +1042,8 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, brain subject=brain._subject, subjects_dir=brain._subjects_dir, ) - label = "{}:{} MNI: {}".format( - hemi_prefix, str(vertex_id).ljust(6), ", ".join("%5.1f" % m for m in mni) + label = f"{hemi_prefix}:{str(vertex_id).ljust(6)} MNI: " + ", ".join( + f"{m:5.1f}" for m in mni ) assert line.get_label() == label diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index faa209454e1..510d8b99fc4 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -44,7 +44,7 @@ def _reload_backend(backend_name): backend = importlib.import_module( name=_backend_name_map[backend_name], package="mne.viz.backends" ) - logger.info("Using %s 3d backend.\n" % backend_name) + logger.info(f"Using {backend_name} 3d backend.") def _get_backend(): diff --git a/mne/viz/circle.py b/mne/viz/circle.py index b19130b3bff..2e9578cf4c9 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -171,7 +171,7 @@ def _plot_connectivity_circle( if node_angles is not None: if len(node_angles) != n_nodes: - raise ValueError("node_angles has to be the same length " "as node_names") + raise ValueError("node_angles has to be the same length as node_names") # convert it to radians node_angles = node_angles * np.pi / 180 else: diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index af6ef0f6786..472874e6062 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -708,7 +708,7 @@ def plot_drop_log( percent = _drop_log_stats(drop_log, ignore) if percent < threshold: logger.info( - "Percent dropped epochs < supplied threshold; not " "plotting drop log." + "Percent dropped epochs < supplied threshold; not plotting drop log." ) return absolute = len([x for x in drop_log if len(x) if not any(y in ignore for y in x)]) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 7a1be5eb586..dad723d6c5a 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -137,9 +137,7 @@ def _line_plot_onselect( ch_types = [type_ for type_ in ch_types if type_ in ("eeg", "grad", "mag")] if len(ch_types) == 0: - raise ValueError( - "Interactive topomaps only allowed for EEG " "and MEG channels." - ) + raise ValueError("Interactive topomaps only allowed for EEG and MEG channels.") if ( "grad" in ch_types and len(_pair_grad_sensors(info, topomap_coords=False, raise_error=False)) < 2 @@ -336,14 +334,14 @@ def _plot_evoked( axes[sel] = plt.axes() if not isinstance(axes, dict): raise ValueError( - "If `group_by` is a dict, `axes` must be " "a dict of axes or None." + "If `group_by` is a dict, `axes` must be a dict of axes or None." ) _validate_if_list_of_axes(list(axes.values())) remove_xlabels = any(ax.get_subplotspec().is_last_row() for ax in axes.values()) for sel in group_by: # ... we loop over selections if sel not in axes: raise ValueError( - sel + " present in `group_by`, but not " "found in `axes`" + sel + " present in `group_by`, but not found in `axes`" ) ax = axes[sel] # the unwieldy dict comp below defaults the title to the sel @@ -398,7 +396,7 @@ def _plot_evoked( return figs elif isinstance(axes, dict): raise ValueError( - "If `group_by` is not a dict, " "`axes` must not be a dict either." + "If `group_by` is not a dict, `axes` must not be a dict either." ) time_unit, times = _check_time_unit(time_unit, evoked.times) @@ -429,9 +427,7 @@ def _plot_evoked( if ylim is not None and not isinstance(ylim, dict): # The user called Evoked.plot_image() or plot_evoked_image(), the # clim parameters of those functions end up to be the ylim here. - raise ValueError( - "`clim` must be a dict. " "E.g. clim = dict(eeg=[-20, 20])" - ) + raise ValueError("`clim` must be a dict. E.g. clim = dict(eeg=[-20, 20])") picks = _picks_to_idx(info, picks, none="all", exclude=()) if len(picks) != len(set(picks)): @@ -668,9 +664,7 @@ def _plot_lines( # we need to use "is True" here _spat_col = _check_spatial_colors(info, idx, spatial_colors) if _spat_col is True and not _check_ch_locs(info=info, picks=idx): - warn( - "Channel locations not available. Disabling spatial " "colors." - ) + warn("Channel locations not available. Disabling spatial colors.") _spat_col = selectable = False if _spat_col is True and len(idx) != 1: x, y, z = locs3d.T @@ -1271,7 +1265,7 @@ def plot_evoked_topo( if isinstance(color, (tuple, list)): if len(color) != len(evoked): raise ValueError( - "Lists of evoked objects and colors" " must have the same length" + "Lists of evoked objects and colors must have the same length" ) elif color is None: if dark_background: @@ -1596,7 +1590,7 @@ def plot_evoked_white( ) if has_sss: logger.info( - "SSS has been applied to data. Showing mag and grad " "whitening jointly." + "SSS has been applied to data. Showing mag and grad whitening jointly." ) # get one whitened evoked per cov @@ -1641,11 +1635,10 @@ def whitened_gfp(x, rank=None): raise ValueError(f"axes must have shape {want_shape}, got {axes.shape}.") fig = axes.flat[0].figure if n_columns > 1: + suptitle = noise_cov[0].get("method", "empirical") suptitle = ( - 'Whitened evoked (left, best estimator = "%s")\n' - "and global field power " - "(right, comparison of estimators)" - % noise_cov[0].get("method", "empirical") + f'Whitened evoked (left, best estimator = "{suptitle}")\n' + "and global field power (right, comparison of estimators)" ) fig.suptitle(suptitle) @@ -1701,7 +1694,7 @@ def whitened_gfp(x, rank=None): ax = ax_gfp[i] ax.set_title( - title if n_columns > 1 else 'Whitened GFP, method = "%s"' % label + title if n_columns > 1 else f'Whitened GFP, method = "{label}"' ) data = evoked_white.data[sub_picks] diff --git a/mne/viz/evoked_field.py b/mne/viz/evoked_field.py index 3ce9c6756e2..b247b3fc092 100644 --- a/mne/viz/evoked_field.py +++ b/mne/viz/evoked_field.py @@ -126,7 +126,7 @@ def __init__( time = np.mean([evoked.get_peak(ch_type=t)[1] for t in types]) self._current_time = time if not evoked.times[0] <= time <= evoked.times[-1]: - raise ValueError("`time` (%0.3f) must be inside `evoked.times`" % time) + raise ValueError(f"`time` ({time:0.3f}) must be inside `evoked.times`") self._time_label = time_label self._vmax = _validate_type(vmax, (None, "numeric", dict), "vmax") @@ -258,10 +258,10 @@ def _prepare_surf_map(self, surf_map, color, alpha): message = ["Channels in map and data do not match."] diff = map_ch_names - evoked_ch_names if len(diff): - message += ["%s not in data file. " % list(diff)] + message += [f"{list(diff)} not in data file. "] diff = evoked_ch_names - map_ch_names if len(diff): - message += ["%s not in map file." % list(diff)] + message += [f"{list(diff)} not in map file."] raise RuntimeError(" ".join(message)) data = surf_map["data"] @ self._evoked.data[pick] diff --git a/mne/viz/ica.py b/mne/viz/ica.py index 1f53d1f1d22..75cca731fe6 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -339,7 +339,7 @@ def _set_scale(ax, scale): _set_scale(spec_ax, "log") # epoch variance - var_ax_title = "Dropped segments: %.2f %%" % var_percent + var_ax_title = f"Dropped segments: {var_percent:.2f} %" set_title_and_labels(var_ax, var_ax_title, kind, "Variance (AU)") hist_ax.set_ylabel("") @@ -563,7 +563,7 @@ def _fast_plot_ica_properties( fig, axes = _create_properties_layout(figsize=figsize) else: if len(picks) > 1: - raise ValueError("Only a single pick can be drawn " "to a set of axes.") + raise ValueError("Only a single pick can be drawn to a set of axes.") from .utils import _validate_if_list_of_axes _validate_if_list_of_axes(axes, obligatory_len=5) @@ -1017,7 +1017,7 @@ def plot_ica_scores( for label, this_scores, ax in zip(labels, scores, axes): if len(my_range) != len(this_scores): raise ValueError( - "The length of `scores` must equal the " "number of ICA components." + "The length of `scores` must equal the number of ICA components." ) ax.bar(my_range, this_scores, color="gray", edgecolor="k") for excl in exclude: @@ -1035,7 +1035,7 @@ def plot_ica_scores( label = ", ".join([split[0], split[2]]) elif "/" in label: label = ", ".join(label.split("/")) - ax.set_title("(%s)" % label) + ax.set_title(f"({label})") ax.set_xlabel("ICA components") ax.set_xlim(-0.6, len(this_scores) - 0.4) fig.canvas.draw() @@ -1110,7 +1110,7 @@ def plot_ica_overlay( if exclude is None: exclude = ica.exclude if not isinstance(exclude, (np.ndarray, list)): - raise TypeError("exclude must be of type list. Got %s" % type(exclude)) + raise TypeError(f"exclude must be of type list. Got {type(exclude)}") if isinstance(inst, BaseRaw): start = 0.0 if start is None else start stop = 3.0 if stop is None else stop diff --git a/mne/viz/misc.py b/mne/viz/misc.py index 49b01ed6b16..b072e4ff183 100644 --- a/mne/viz/misc.py +++ b/mne/viz/misc.py @@ -81,7 +81,7 @@ def _index_info_cov(info, cov, exclude): idx_names = [ ( idx_by_type[key], - "%s covariance" % DEFAULTS["titles"][key], + f"{DEFAULTS['titles'][key]} covariance", DEFAULTS["units"][key], DEFAULTS["scalings"][key], key, @@ -162,12 +162,10 @@ def plot_cov( P, ncomp, _ = make_projector(projs, ch_names) if ncomp > 0: - logger.info( - " Created an SSP operator (subspace dimension" " = %d)" % ncomp - ) + logger.info(f" Created an SSP operator (subspace dimension = {ncomp:d})") C = np.dot(P, np.dot(C, P.T)) else: - logger.info(" The projection vectors do not apply to these " "channels.") + logger.info(" The projection vectors do not apply to these channels.") if np.iscomplexobj(C): C = np.sqrt((C * C.conj()).real) @@ -225,7 +223,7 @@ def plot_cov( axes[0, k].text( this_rank - 1, axes[0, k].get_ylim()[1], - "rank ≈ %d" % (this_rank,), + f"rank ≈ {this_rank:d}", ha="right", va="top", color="r", @@ -233,7 +231,7 @@ def plot_cov( zorder=4, ) axes[0, k].set( - ylabel="Noise σ (%s)" % unit, + ylabel=f"Noise σ ({unit})", yscale="log", xlabel="Eigenvalue index", title=name, @@ -282,7 +280,7 @@ def plot_source_spectrogram( stc = stcs[0] if tmin is not None and tmin < stc.times[0]: raise ValueError( - "tmin cannot be smaller than the first time point " "provided in stcs" + "tmin cannot be smaller than the first time point provided in stcs" ) if tmax is not None and tmax > stc.times[-1] + stc.tstep: raise ValueError( @@ -432,7 +430,7 @@ def _plot_mri_contours( raise ValueError( "slices must be a sorted 1D array of int with unique " "elements, at least one element, and no elements " - "greater than %d, got %s" % (n_slices - 1, slices) + f"greater than {n_slices - 1:d}, got {slices}" ) # create of list of surfaces @@ -702,7 +700,7 @@ def plot_bem( if surf_fname.exists(): surfaces.append((surf_fname, "#00DD00")) else: - raise OSError("Surface %s does not exist." % surf_fname) + raise OSError(f"Surface {surf_fname} does not exist.") # TODO: Refactor with / improve _ensure_src to do this if isinstance(src, (str, Path, os.PathLike)): @@ -717,7 +715,7 @@ def plot_bem( elif src is not None and not isinstance(src, SourceSpaces): raise TypeError( "src needs to be None, path-like or SourceSpaces instance, " - "not %s" % repr(src) + f"not {repr(src)}" ) if len(surfaces) == 0: @@ -751,7 +749,7 @@ def _get_bem_plotting_surfaces(bem_path): surf_fname = glob(op.join(bem_path, surf_name + ".surf")) if len(surf_fname) > 0: surf_fname = surf_fname[0] - logger.info("Using surface: %s" % surf_fname) + logger.info(f"Using surface: {surf_fname}") surfaces.append((surf_fname, color)) return surfaces @@ -841,7 +839,7 @@ def plot_events( for this_event in unique_events: if this_event not in unique_events_id: - warn("event %s missing from event_id will be ignored" % this_event) + warn(f"event {this_event} missing from event_id will be ignored") else: unique_events_id = unique_events @@ -871,7 +869,7 @@ def plot_events( if event_id is not None: event_label = f"{event_id_rev[ev]} ({count})" else: - event_label = "N=%d" % (count,) + event_label = f"N={count:d}" labels.append(event_label) kwargs = {} if ev in color: @@ -1016,9 +1014,9 @@ def _get_flim(flim, fscale, freq, sfreq=None): flim += [freq[-1]] if fscale == "log": if flim[0] <= 0: - raise ValueError("flim[0] must be positive, got %s" % flim[0]) + raise ValueError(f"flim[0] must be positive, got {flim[0]}") elif flim[0] < 0: - raise ValueError("flim[0] must be non-negative, got %s" % flim[0]) + raise ValueError(f"flim[0] must be non-negative, got {flim[0]}") return flim @@ -1127,7 +1125,7 @@ def plot_filter( if isinstance(plot, str): plot = [plot] for xi, x in enumerate(plot): - _check_option("plot[%d]" % xi, x, ("magnitude", "delay", "time")) + _check_option(f"plot[{xi}]", x, ("magnitude", "delay", "time")) flim = _get_flim(flim, fscale, freq, sfreq) if fscale == "log": @@ -1203,8 +1201,8 @@ def plot_filter( fig = axes[0].get_figure() if len(axes) != len(plot): raise ValueError( - "Length of axes (%d) must be the same as number of " - "requested filter properties (%d)" % (len(axes), len(plot)) + f"Length of axes ({len(axes)}) must be the same as number of " + f"requested filter properties ({len(plot)})" ) t = np.arange(len(h)) @@ -1403,8 +1401,8 @@ def _handle_event_colors(color_dict, unique_events, event_id): custom_colors[event_id[key]] = color else: # key not a valid event, warn and ignore warn( - "Event ID %s is in the color dict but is not " - "present in events or event_id." % str(key) + f"Event ID {key} is in the color dict but is not " + "present in events or event_id." ) # warn if color_dict is missing any entries unassigned = sorted(set(unique_events) - set(custom_colors)) @@ -1537,7 +1535,7 @@ def plot_csd( if csd._is_sum: ax.set_title(f"{np.min(freq):.1f}-{np.max(freq):.1f} Hz.") else: - ax.set_title("%.1f Hz." % freq) + ax.set_title(f"{freq:.1f} Hz.") plt.suptitle(title) if colorbar: @@ -1545,7 +1543,7 @@ def plot_csd( if mode == "csd": label = "CSD" if ch_type in units: - label += " (%s)" % units[ch_type] + label += f" ({units[ch_type]})" cb.set_label(label) elif mode == "coh": cb.set_label("Coherence") diff --git a/mne/viz/tests/test_3d_mpl.py b/mne/viz/tests/test_3d_mpl.py index b006a421494..a96d41fefb8 100644 --- a/mne/viz/tests/test_3d_mpl.py +++ b/mne/viz/tests/test_3d_mpl.py @@ -87,7 +87,7 @@ def test_plot_volume_source_estimates( verbose=True, ) log = log.getvalue() - want_str = "t = %0.3f s" % want_t + want_str = f"t = {want_t:0.3f} s" assert want_str in log, (want_str, init_t) want_str = f"({want_p[0]:0.1f}, {want_p[1]:0.1f}, {want_p[2]:0.1f}) mm" assert want_str in log, (want_str, init_p) diff --git a/mne/viz/tests/test_circle.py b/mne/viz/tests/test_circle.py index cf831768291..357a140456d 100644 --- a/mne/viz/tests/test_circle.py +++ b/mne/viz/tests/test_circle.py @@ -13,7 +13,7 @@ @pytest.mark.filterwarnings( - "ignore:invalid value encountered in greater_equal" ":RuntimeWarning" + "ignore:invalid value encountered in greater_equal:RuntimeWarning" ) def test_plot_channel_labels_circle(): """Test plotting channel labels in a circle.""" diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py index 9679a787277..7de511affe7 100644 --- a/mne/viz/tests/test_epochs.py +++ b/mne/viz/tests/test_epochs.py @@ -419,7 +419,7 @@ def test_plot_psd_epochs(epochs): # test support for single-bin bands and old-style list-of-tuple input fig = spectrum.plot_topomap(bands=[(20, "20 Hz"), (15, 25, "15-25 Hz")]) # test with a flat channel - err_str = "for channel %s" % epochs.ch_names[2] + err_str = f"for channel {epochs.ch_names[2]}" epochs.get_data(copy=False)[0, 2, :] = 0 for dB in [True, False]: with _record_warnings(), pytest.warns(UserWarning, match=err_str): diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 52a5193f2e0..13319cc586c 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -164,14 +164,12 @@ def format_coord_unified(x, y, pos=None, ch_names=None): else: in_box = False return ( - ("%s (click to magnify)" % ch_names[closest]) - if in_box - else "No channel here" + f"{ch_names[closest]} (click to magnify)" if in_box else "No channel here" ) def format_coord_multiaxis(x, y, ch_name=None): """Update status bar with channel name under cursor.""" - return "%s (click to magnify)" % ch_name + return f"{ch_name} (click to magnify)" fig.set_facecolor(fig_facecolor) if layout is None: @@ -557,7 +555,7 @@ def _format_coord(x, y, labels, ax): ) timestr = f"{x:6.3f} {xunit}: " if not nearby: - return "%s Nothing here" % timestr + return f"{timestr} Nothing here" labels = [""] * len(nearby) if labels is None else labels nearby_data = [(data[n], labels[n], times[n]) for n in nearby] ylabel = ax.get_ylabel() @@ -577,7 +575,7 @@ def _format_coord(x, y, labels, ax): s += f"{data_[ch_idx, idx]:7.2f} {yunit}" if trunc_labels: label = label if len(label) <= 10 else f"{label[:6]}..{label[-2:]}" - s += " [%s] " % label if label else " " + s += f" [{label}] " if label else " " return s ax.format_coord = lambda x, y: _format_coord(x, y, labels=labels, ax=ax) @@ -972,7 +970,7 @@ def _plot_evoked_topo( picks = new_picks types_used = ["grad"] unit = _handle_default("units")["grad"] if noise_cov is None else "NA" - y_label = "RMS amplitude (%s)" % unit + y_label = f"RMS amplitude ({unit})" if layout is None: layout = find_layout(info, exclude=exclude) @@ -1033,7 +1031,7 @@ def _plot_evoked_topo( unit = _handle_default("units")[channel_type(info, ch_idx)] else: unit = "NA" - y_label.append("Amplitude (%s)" % unit) + y_label.append(f"Amplitude ({unit})") if ylim is None: # find minima and maxima over all evoked data for each channel pick @@ -1053,7 +1051,7 @@ def _plot_evoked_topo( if is_meg or is_nirs: ylim_ = list(map(list, zip(*ylim_))) else: - raise TypeError("ylim must be None or a dict. Got %s." % type(ylim)) + raise TypeError(f"ylim must be None or a dict. Got {type(ylim)}.") data = [e.data for e in evoked] comments = [e.comment for e in evoked] diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 5a6eac4f1ab..20efbe79ab8 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -164,7 +164,7 @@ def _prepare_topomap_plot(inst, ch_type, sphere=None): picks = pick_types(info, meg=ch_type, ref_meg=False, exclude="bads") if len(picks) == 0: - raise ValueError("No channels of type %r" % ch_type) + raise ValueError(f"No channels of type {ch_type!r}") pos = _find_topomap_coords(info, picks, sphere=sphere) @@ -664,7 +664,7 @@ def _make_head_outlines(sphere, pos, outlines, clip_origin): elif isinstance(outlines, dict): if "mask_pos" not in outlines: - raise ValueError("You must specify the coordinates of the image " "mask.") + raise ValueError("You must specify the coordinates of the image mask.") else: raise ValueError("Invalid value for `outlines`.") @@ -1212,9 +1212,7 @@ def _plot_topomap( # check if there is only 1 channel type, and n_chans matches the data ch_type = pos.get_channel_types(picks=None, unique=True) - info_help = ( - "Pick Info with e.g. mne.pick_info and " "mne.channel_indices_by_type." - ) + info_help = "Pick Info with e.g. mne.pick_info and mne.channel_indices_by_type." if len(ch_type) > 1: raise ValueError("Multiple channel types in Info structure. " + info_help) elif len(pos["chs"]) != data.shape[0]: @@ -1238,8 +1236,7 @@ def _plot_topomap( extrapolate = _check_extrapolate(extrapolate, ch_type) if data.ndim > 1: raise ValueError( - "Data needs to be array of shape (n_sensors,); got " - "shape %s." % str(data.shape) + f"Data needs to be array of shape (n_sensors,); got shape {data.shape}." ) # Give a helpful error message for common mistakes regarding the position @@ -1420,7 +1417,7 @@ def _plot_ica_topomap( if not isinstance(axes, Axes): raise ValueError( "axis has to be an instance of matplotlib Axes, " - "got %s instead." % type(axes) + f"got {type(axes)} instead." ) ch_type = _get_plot_ch_type(ica, ch_type, allow_ref_meg=ica.allow_ref_meg) if ch_type == "ref_meg": @@ -2156,7 +2153,7 @@ def plot_evoked_topomap( axes_given = axes is not None interactive = isinstance(times, str) and times == "interactive" if interactive and axes_given: - raise ValueError("User-provided axes not allowed when " "times='interactive'.") + raise ValueError("User-provided axes not allowed when times='interactive'.") # units, scalings key = "grad" if ch_type.startswith("planar") else ch_type default_scaling = _handle_default("scalings", None)[key] @@ -3027,7 +3024,7 @@ def _prepare_topomap(pos, ax, check_nonzero=True): _hide_frame(ax) if check_nonzero and not pos.any(): raise RuntimeError( - "No position information found, cannot compute " "geometries for topomap." + "No position information found, cannot compute geometries for topomap." ) @@ -3600,8 +3597,8 @@ def plot_arrowmap( if ch_type not in ("mag", "grad"): raise ValueError( - "Channel type '%s' not supported. Supported channel " - "types are 'mag' and 'grad'." % ch_type + f"Channel type '{ch_type}' not supported. Supported channel " + "types are 'mag' and 'grad'." ) if info_to is None and ch_type == "mag": @@ -3614,9 +3611,7 @@ def plot_arrowmap( ch_type = ch_type[0][0] if ch_type != "mag": - raise ValueError( - "only 'mag' channel type is supported. " "Got %s" % ch_type - ) + raise ValueError("only 'mag' channel type is supported. " f"Got {ch_type}") if info_to is not info_from: info_to = pick_info(info_to, pick_types(info_to, meg=True)) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index cb4c6e85249..b524ec800b8 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1149,7 +1149,7 @@ def plot_sensors( if pick in value: colors[pick_idx] = color_vals[ind] break - title = "Sensor positions (%s)" % ch_type if title is None else title + title = f"Sensor positions ({ch_type})" if title is None else title fig = _plot_sensors_2d( pos, info, @@ -2138,7 +2138,7 @@ def _check_time_unit(time_unit, times): elif time_unit == "ms": times = 1e3 * times else: - raise ValueError("time_unit must be 's' or 'ms', got %r" % time_unit) + raise ValueError(f"time_unit must be 's' or 'ms', got {time_unit!r}") return time_unit, times diff --git a/pyproject.toml b/pyproject.toml index ee89321df6f..9abefee7ca9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,7 @@ doc = [ "sphinx>=6", "numpydoc", "pydata_sphinx_theme==0.15.2", - "sphinx-gallery", + "sphinx-gallery>=0.16", "sphinxcontrib-bibtex>=2.5", "sphinxcontrib-towncrier", "memory_profiler", diff --git a/tutorials/epochs/40_autogenerate_metadata.py b/tutorials/epochs/40_autogenerate_metadata.py index 9e769a5ff5e..7da978373fc 100644 --- a/tutorials/epochs/40_autogenerate_metadata.py +++ b/tutorials/epochs/40_autogenerate_metadata.py @@ -304,7 +304,7 @@ # responses and a response time greater than 0.5 seconds # (i.e., slow responses). vis_erp = epochs["response_correct"].average() -vis_erp_slow = epochs["(not response_correct) & " "(response > 0.3)"].average() +vis_erp_slow = epochs["(not response_correct) & (response > 0.3)"].average() fig, ax = plt.subplots(2, figsize=(6, 6), layout="constrained") vis_erp.plot(gfp=True, spatial_colors=True, axes=ax[0]) diff --git a/tutorials/inverse/30_mne_dspm_loreta.py b/tutorials/inverse/30_mne_dspm_loreta.py index 1f7af45eec3..58102912675 100644 --- a/tutorials/inverse/30_mne_dspm_loreta.py +++ b/tutorials/inverse/30_mne_dspm_loreta.py @@ -129,7 +129,7 @@ fig, ax = plt.subplots() ax.plot(1e3 * stc.times, stc.data[::100, :].T) -ax.set(xlabel="time (ms)", ylabel="%s value" % method) +ax.set(xlabel="time (ms)", ylabel=f"{method} value") # %% # Examine the original data and the residual after fitting: diff --git a/tutorials/inverse/50_beamformer_lcmv.py b/tutorials/inverse/50_beamformer_lcmv.py index 9c7bc7d73be..d9027f32560 100644 --- a/tutorials/inverse/50_beamformer_lcmv.py +++ b/tutorials/inverse/50_beamformer_lcmv.py @@ -290,7 +290,7 @@ ori_labels = ["x", "y", "z"] fig, ax = plt.subplots(1) for ori, label in zip(stc_vec.data[peak_vox, :, :], ori_labels): - ax.plot(stc_vec.times, ori, label="%s component" % label) + ax.plot(stc_vec.times, ori, label=f"{label} component") ax.legend(loc="lower right") ax.set( title="Activity per orientation in the peak voxel", diff --git a/tutorials/inverse/85_brainstorm_phantom_ctf.py b/tutorials/inverse/85_brainstorm_phantom_ctf.py index 8ef188487d6..ecd0b1479d3 100644 --- a/tutorials/inverse/85_brainstorm_phantom_ctf.py +++ b/tutorials/inverse/85_brainstorm_phantom_ctf.py @@ -121,8 +121,8 @@ expected_pos = np.array([18.0, 0.0, 49.0]) diff = np.sqrt(np.sum((dip.pos[0] * 1000 - expected_pos) ** 2)) -print("Actual pos: %s mm" % np.array_str(expected_pos, precision=1)) -print("Estimated pos: %s mm" % np.array_str(dip.pos[0] * 1000, precision=1)) -print("Difference: %0.1f mm" % diff) +print(f"Actual pos: {np.array_str(expected_pos, precision=1)} mm") +print(f"Estimated pos: {np.array_str(dip.pos[0] * 1000, precision=1)} mm") +print(f"Difference: {diff:0.1f} mm") print("Amplitude: %0.1f nAm" % (1e9 * dip.amplitude[0])) -print("GOF: %0.1f %%" % dip.gof[0]) +print(f"GOF: {dip.gof[0]:0.1f} %") diff --git a/tutorials/inverse/90_phantom_4DBTi.py b/tutorials/inverse/90_phantom_4DBTi.py index 69cb4a85bd6..c4064dd0307 100644 --- a/tutorials/inverse/90_phantom_4DBTi.py +++ b/tutorials/inverse/90_phantom_4DBTi.py @@ -66,7 +66,7 @@ actual_pos = np.dot(actual_pos, [[0, 1, 0], [-1, 0, 0], [0, 0, 1]]) errors = 1e3 * np.linalg.norm(actual_pos - pos, axis=1) -print("errors (mm) : %s" % errors) +print(f"errors (mm) : {errors}") # %% # Plot the dipoles in 3D diff --git a/tutorials/machine-learning/30_strf.py b/tutorials/machine-learning/30_strf.py index 4d8acad03c2..eda8f90c41f 100644 --- a/tutorials/machine-learning/30_strf.py +++ b/tutorials/machine-learning/30_strf.py @@ -238,7 +238,7 @@ (ix_best_alpha, scores[ix_best_alpha] - 0.1), arrowprops={"arrowstyle": "->"}, ) -plt.xticks(np.arange(len(alphas)), ["%.0e" % ii for ii in alphas]) +plt.xticks(np.arange(len(alphas)), [f"{ii:.0e}" for ii in alphas]) ax.set( xlabel="Ridge regularization value", ylabel="Score ($R^2$)", @@ -321,7 +321,7 @@ (ix_best_alpha, scores[ix_best_alpha] - 0.1), arrowprops={"arrowstyle": "->"}, ) -plt.xticks(np.arange(len(alphas)), ["%.0e" % ii for ii in alphas]) +plt.xticks(np.arange(len(alphas)), [f"{ii:.0e}" for ii in alphas]) ax.set( xlabel="Laplacian regularization value", ylabel="Score ($R^2$)", diff --git a/tutorials/preprocessing/25_background_filtering.py b/tutorials/preprocessing/25_background_filtering.py index c0f56098bad..105f360cead 100644 --- a/tutorials/preprocessing/25_background_filtering.py +++ b/tutorials/preprocessing/25_background_filtering.py @@ -170,7 +170,7 @@ third_height = np.array(plt.rcParams["figure.figsize"]) * [1, 1.0 / 3.0] ax = plt.subplots(1, figsize=third_height, layout="constrained")[1] -plot_ideal_filter(freq, gain, ax, title="Ideal %s Hz lowpass" % f_p, flim=flim) +plot_ideal_filter(freq, gain, ax, title=f"Ideal {f_p} Hz lowpass", flim=flim) # %% # This filter hypothetically achieves zero ripple in the frequency domain, @@ -425,7 +425,7 @@ l_freq=None, h_freq=f_p, h_trans_bandwidth=transition_band, - filter_length="%ss" % filter_dur, + filter_length=f"{filter_dur}s", fir_design="firwin2", verbose=True, ) @@ -844,7 +844,7 @@ def baseline_plot(x): if ri == 0: ax.set(title=("No " if ci == 0 else "") + "Baseline Correction") ax.set(xticks=tticks, ylim=ylim, xlim=xlim, xlabel=xlabel) - ax.set_ylabel("%0.1f Hz" % freq, rotation=0, horizontalalignment="right") + ax.set_ylabel(f"{freq:0.1f} Hz", rotation=0, horizontalalignment="right") fig.suptitle(title) plt.show() diff --git a/tutorials/stats-sensor-space/10_background_stats.py b/tutorials/stats-sensor-space/10_background_stats.py index 9d6912a20cb..d8f08b432a2 100644 --- a/tutorials/stats-sensor-space/10_background_stats.py +++ b/tutorials/stats-sensor-space/10_background_stats.py @@ -160,7 +160,7 @@ def plot_t_p(t, p, title, mcc, axes=None): mappable=surf, ) cbar.set_ticks(t_lims) - cbar.set_ticklabels(["%0.1f" % t_lim for t_lim in t_lims]) + cbar.set_ticklabels([f"{t_lim:0.1f}" for t_lim in t_lims]) cbar.set_label("t-value") cbar.ax.get_xaxis().set_label_coords(0.5, -0.3) if not show: @@ -182,7 +182,7 @@ def plot_t_p(t, p, title, mcc, axes=None): mappable=img, ) cbar.set_ticks(p_lims) - cbar.set_ticklabels(["%0.1f" % p_lim for p_lim in p_lims]) + cbar.set_ticklabels([f"{p_lim:0.1f}" for p_lim in p_lims]) cbar.set_label(r"$-\log_{10}(p)$") cbar.ax.get_xaxis().set_label_coords(0.5, -0.3) if show: From 7fd22d63e8e478db23e9f114abc9b1b5228ed822 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 1 May 2024 11:53:27 -0400 Subject: [PATCH 027/153] MAINT: Fix faulty links (#12592) --- .github/workflows/autofix.yml | 2 +- doc/conf.py | 1 + examples/preprocessing/eeg_bridging.py | 3 +-- mne/preprocessing/_csd.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 2c0b693750e..79c8ee528a0 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -18,4 +18,4 @@ jobs: python-version: '3.12' - run: pip install --upgrade towncrier pygithub - run: python ./.github/actions/rename_towncrier/rename_towncrier.py - - uses: autofix-ci/action@ea32e3a12414e6d3183163c3424a7d7a8631ad84 + - uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc diff --git a/doc/conf.py b/doc/conf.py index ad4a158c1f9..204a2ccac1d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -655,6 +655,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): "https://www.dtu.dk/english/service/phonebook/person", # SSL problems sometimes "http://ilabs.washington.edu", + "https://psychophysiology.cpmc.columbia.edu", ] linkcheck_anchors = False # saves a bit of time linkcheck_timeout = 15 # some can be quite slow diff --git a/examples/preprocessing/eeg_bridging.py b/examples/preprocessing/eeg_bridging.py index 87e1d8621f0..b0eb50a039d 100644 --- a/examples/preprocessing/eeg_bridging.py +++ b/examples/preprocessing/eeg_bridging.py @@ -22,8 +22,7 @@ effect and exclude subjects with bridging that might effect the outcome of a study. Preventing electrode bridging is ideal but awareness of the problem at least will mitigate its potential as a confound to a study. This tutorial -follows -https://psychophysiology.cpmc.columbia.edu/software/eBridge/tutorial.html. +follows the eBridge tutorial from https://psychophysiology.cpmc.columbia.edu. .. _electrodes.tsv: https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/03-electroencephalography.html#electrodes-description-_electrodestsv """ # noqa: E501 diff --git a/mne/preprocessing/_csd.py b/mne/preprocessing/_csd.py index 271b97387a2..6edb254ea49 100644 --- a/mne/preprocessing/_csd.py +++ b/mne/preprocessing/_csd.py @@ -219,7 +219,7 @@ def compute_bridged_electrodes( Based on :footcite:`TenkeKayser2001,GreischarEtAl2004,DelormeMakeig2004` and the `EEGLAB implementation - `_. + `__. Parameters ---------- From 356e8546890b8f798a14777991ba7de6cd9ab9bb Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Wed, 1 May 2024 17:01:13 -0700 Subject: [PATCH 028/153] BUG: Fix epochs interpolation for sEEG and don't interpolate over spans in electrodes and don't include stray contacts from other electrodes circumstantially in a line with an electrode (#12593) --- doc/changes/devel/12593.bugfix.rst | 1 + mne/channels/interpolation.py | 100 +++++++++++++++++------ mne/channels/tests/test_interpolation.py | 25 +++++- 3 files changed, 101 insertions(+), 25 deletions(-) create mode 100644 doc/changes/devel/12593.bugfix.rst diff --git a/doc/changes/devel/12593.bugfix.rst b/doc/changes/devel/12593.bugfix.rst new file mode 100644 index 00000000000..e43d6110716 --- /dev/null +++ b/doc/changes/devel/12593.bugfix.rst @@ -0,0 +1 @@ +Fix error causing :meth:`mne.Epochs.interpolate_bads` not to work for ``seeg`` channels and fix a single contact on neighboring shafts sometimes being included in interpolation, by `Alex Rockhill`_ \ No newline at end of file diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index 6c5042d1d04..28a5058b3ac 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -292,14 +292,16 @@ def _interpolate_bads_nirs(inst, exclude=(), verbose=None): return inst -def _find_seeg_electrode_shaft(pos, tol=2e-3): +def _find_seeg_electrode_shaft(pos, tol_shaft=0.002, tol_spacing=1): # 1) find nearest neighbor to define the electrode shaft line # 2) find all contacts on the same line + # 3) remove contacts with large distances dist = squareform(pdist(pos)) np.fill_diagonal(dist, np.inf) shafts = list() + shaft_ts = list() for i, n1 in enumerate(pos): if any([i in shaft for shaft in shafts]): continue @@ -308,12 +310,59 @@ def _find_seeg_electrode_shaft(pos, tol=2e-3): shaft_dists = np.linalg.norm( np.cross((pos - n1), (pos - n2)), axis=1 ) / np.linalg.norm(n2 - n1) - shafts.append(np.where(shaft_dists < tol)[0]) # 2 - return shafts + shaft = np.where(shaft_dists < tol_shaft)[0] # 2 + shaft_prev = None + for _ in range(10): # avoid potential cycles + if np.array_equal(shaft, shaft_prev): + break + shaft_prev = shaft + # compute median shaft line + v = np.median( + [ + pos[i] - pos[j] + for idx, i in enumerate(shaft) + for j in shaft[idx + 1 :] + ], + axis=0, + ) + c = np.median(pos[shaft], axis=0) + # recompute distances + shaft_dists = np.linalg.norm( + np.cross((pos - c), (pos - c + v)), axis=1 + ) / np.linalg.norm(v) + shaft = np.where(shaft_dists < tol_shaft)[0] + ts = np.array([np.dot(c - n0, v) / np.linalg.norm(v) ** 2 for n0 in pos[shaft]]) + shaft_order = np.argsort(ts) + shaft = shaft[shaft_order] + ts = ts[shaft_order] + + # only include the largest group with spacing with the error tolerance + # avoid interpolating across spans between contacts + t_diffs = np.diff(ts) + t_diff_med = np.median(t_diffs) + spacing_errors = (t_diffs - t_diff_med) / t_diff_med + groups = list() + group = [shaft[0]] + for j in range(len(shaft) - 1): + if spacing_errors[j] > tol_spacing: + groups.append(group) + group = [shaft[j + 1]] + else: + group.append(shaft[j + 1]) + groups.append(group) + group = [group for group in groups if i in group][0] + ts = ts[np.isin(shaft, group)] + shaft = np.array(group, dtype=int) + + shafts.append(shaft) + shaft_ts.append(ts) + return shafts, shaft_ts @verbose -def _interpolate_bads_seeg(inst, exclude=None, tol=2e-3, verbose=None): +def _interpolate_bads_seeg( + inst, exclude=None, tol_shaft=0.002, tol_spacing=1, verbose=None +): if exclude is None: exclude = list() picks = pick_types(inst.info, meg=False, seeg=True, exclude=exclude) @@ -328,21 +377,25 @@ def _interpolate_bads_seeg(inst, exclude=None, tol=2e-3, verbose=None): # Make sure only sEEG are used bads_idx_pos = bads_idx[picks] - shafts = _find_seeg_electrode_shaft(pos, tol=tol) + shafts, shaft_ts = _find_seeg_electrode_shaft( + pos, tol_shaft=tol_shaft, tol_spacing=tol_spacing + ) # interpolate the bad contacts picks_bad = list(np.where(bads_idx_pos)[0]) - for shaft in shafts: + for shaft, ts in zip(shafts, shaft_ts): bads_shaft = np.array([idx for idx in picks_bad if idx in shaft]) if bads_shaft.size == 0: continue goods_shaft = shaft[np.isin(shaft, bads_shaft, invert=True)] - if goods_shaft.size < 2: + if goods_shaft.size < 4: # cubic spline requires 3 channels + msg = "No shaft" if shaft.size < 4 else "Not enough good channels" + no_shaft_chs = " and ".join(np.array(inst.ch_names)[bads_shaft]) raise RuntimeError( - f"{goods_shaft.size} good contact(s) found in a line " - f" with {np.array(inst.ch_names)[bads_shaft]}, " - "at least 2 are required for interpolation. " - "Dropping this channel/these channels is recommended." + f"{msg} found in a line with {no_shaft_chs} " + "at least 3 good channels on the same line " + f"are required for interpolation, {goods_shaft.size} found. " + f"Dropping {no_shaft_chs} is recommended." ) logger.debug( f"Interpolating {np.array(inst.ch_names)[bads_shaft]} using " @@ -350,16 +403,17 @@ def _interpolate_bads_seeg(inst, exclude=None, tol=2e-3, verbose=None): ) bads_shaft_idx = np.where(np.isin(shaft, bads_shaft))[0] goods_shaft_idx = np.where(~np.isin(shaft, bads_shaft))[0] - n1, n2 = pos[shaft][:2] - ts = np.array( - [ - -np.dot(n1 - n0, n2 - n1) / np.linalg.norm(n2 - n1) ** 2 - for n0 in pos[shaft] - ] + + z = inst._data[..., goods_shaft, :] + is_epochs = z.ndim == 3 + if is_epochs: + z = z.swapaxes(0, 1) + z = z.reshape(z.shape[0], -1) + y = np.arange(z.shape[-1]) + out = RectBivariateSpline(x=ts[goods_shaft_idx], y=y, z=z)( + x=ts[bads_shaft_idx], y=y ) - if np.any(np.diff(ts) < 0): - ts *= -1 - y = np.arange(inst._data.shape[-1]) - inst._data[bads_shaft] = RectBivariateSpline( - x=ts[goods_shaft_idx], y=y, z=inst._data[goods_shaft] - )(x=ts[bads_shaft_idx], y=y) # 3 + if is_epochs: + out = out.reshape(bads_shaft.size, inst._data.shape[0], -1) + out = out.swapaxes(0, 1) + inst._data[..., bads_shaft, :] = out diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 7e282562955..31315343ddc 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -364,8 +364,6 @@ def test_interpolation_seeg(): # check that interpolation changes the data in raw raw_seeg = RawArray(data=epochs_seeg._data[0], info=epochs_seeg.info) raw_before = raw_seeg.copy() - with pytest.raises(RuntimeError, match="1 good contact"): - raw_seeg.interpolate_bads(method=dict(seeg="spline")) montage = raw_seeg.get_montage() pos = montage.get_positions() ch_pos = pos.pop("ch_pos") @@ -378,6 +376,29 @@ def test_interpolation_seeg(): assert not np.all(raw_before._data[bads_mask] == raw_after._data[bads_mask]) assert_array_equal(raw_before._data[~bads_mask], raw_after._data[~bads_mask]) + # check interpolation on epochs + epochs_seeg.set_montage(make_dig_montage(ch_pos, **pos)) + epochs_before = epochs_seeg.copy() + epochs_after = epochs_seeg.interpolate_bads(method=dict(seeg="spline")) + assert not np.all( + epochs_before._data[:, bads_mask] == epochs_after._data[:, bads_mask] + ) + assert_array_equal( + epochs_before._data[:, ~bads_mask], epochs_after._data[:, ~bads_mask] + ) + + # test shaft all bad + epochs_seeg.info["bads"] = epochs_seeg.ch_names + with pytest.raises(RuntimeError, match="Not enough good channels"): + epochs_seeg.interpolate_bads(method=dict(seeg="spline")) + + # test bad not on shaft + ch_pos[bads[0]] = np.array([10, 10, 10]) + epochs_seeg.info["bads"] = bads + epochs_seeg.set_montage(make_dig_montage(ch_pos, **pos)) + with pytest.raises(RuntimeError, match="No shaft found"): + epochs_seeg.interpolate_bads(method=dict(seeg="spline")) + def test_nan_interpolation(raw): """Test 'nan' method for interpolating bads.""" From 537faea66da9a0f2bb4b09efe39b90627c941d68 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Thu, 2 May 2024 17:42:45 +0200 Subject: [PATCH 029/153] Fix docstring of argument 'phase' in construct_iir_filter (#12598) Co-authored-by: Daniel McCloy --- mne/filter.py | 10 +++++++++- mne/time_frequency/spectrum.py | 3 ++- mne/time_frequency/tfr.py | 7 ++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/mne/filter.py b/mne/filter.py index d78987fdcdd..d872379c2b2 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -724,7 +724,15 @@ def construct_iir_filter( ``iir_params`` will be set inplace (if they weren't already). Otherwise, a new ``iir_params`` instance will be created and returned with these entries. - %(phase)s + phase : str + Phase of the filter. + ``phase='zero'`` (default) or equivalently ``'zero-double'`` constructs and + applies IIR filter twice, once forward, and once backward (making it non-causal) + using :func:`~scipy.signal.filtfilt`; ``phase='forward'`` will apply + the filter once in the forward (causal) direction using + :func:`~scipy.signal.lfilter`. + + .. versionadded:: 0.13 %(verbose)s Returns diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index 1441e339d41..45dadf9741a 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -1513,7 +1513,8 @@ def read_spectrum(fname): Parameters ---------- fname : path-like - Path to a spectrum file in HDF5 format. + Path to a spectrum file in HDF5 format, which should end with ``.h5`` or + ``.hdf5``. Returns ------- diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index d93ed6cb67d..4a36c78ad0f 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -2612,13 +2612,13 @@ def save(self, fname, *, overwrite=False, verbose=None): Parameters ---------- fname : path-like - Path of file to save to. + Path of file to save to, which should end with ``-tfr.h5`` or ``-tfr.hdf5``. %(overwrite)s %(verbose)s See Also -------- - mne.time_frequency.read_spectrum + mne.time_frequency.read_tfrs """ _, write_hdf5 = _import_h5io_funcs() check_fname(fname, "time-frequency object", (".h5", ".hdf5")) @@ -4134,7 +4134,8 @@ def read_tfrs(fname, condition=None, *, verbose=None): Parameters ---------- fname : path-like - Path to a TFR file in HDF5 format. + Path to a TFR file in HDF5 format, which should end with ``-tfr.h5`` or + ``-tfr.hdf5``. condition : int or str | list of int or str | None The condition to load. If ``None``, all conditions will be returned. Defaults to ``None``. From e28cba92d80d64ae6bceae0eac4447ea01b74144 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Thu, 2 May 2024 10:51:53 -0500 Subject: [PATCH 030/153] use SVG logo; shrink avatars; update intersphinx URL; adjust pin (#12595) Co-authored-by: Eric Larson --- doc/_static/funding/cds-dark.svg | 26 ++++++++++++++++++++++++++ doc/_static/funding/cds.png | Bin 65248 -> 0 bytes doc/_static/funding/cds.svg | 27 +++++++++++++++++++++++++++ doc/_static/style.css | 2 +- doc/conf.py | 15 +++++++++++++-- doc/funding.rst | 7 +++++-- pyproject.toml | 2 +- 7 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 doc/_static/funding/cds-dark.svg delete mode 100644 doc/_static/funding/cds.png create mode 100644 doc/_static/funding/cds.svg diff --git a/doc/_static/funding/cds-dark.svg b/doc/_static/funding/cds-dark.svg new file mode 100644 index 00000000000..940d66b5680 --- /dev/null +++ b/doc/_static/funding/cds-dark.svg @@ -0,0 +1,26 @@ + +image/svg+xml \ No newline at end of file diff --git a/doc/_static/funding/cds.png b/doc/_static/funding/cds.png deleted file mode 100644 index d726b8daeb1ceee6d80744d0555d38b76f08db90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65248 zcmeHw30RX?_I_(?*;*qa(hf*muvipRe~v{4NEB<4QpE*}EkZ_}mRgGuQAr@=s|un} zjY1_=kW?w+QjOLHkdRMVL_vubEh>-@AVg%z24w%f{|!3{;4&`s|I9p{r+q|{d(S<~ zd){;Ixx1Gv_Il6!ALeho@y2`J3qM`<#v5<$)BoS}9q=bV!k^rHq8)zu`T9h`=!7@d&7JTcSY4T)e2}#{>O7wL!T7^fjnajMyUiC& zKe?pf_>zL%PHUE0&pf$n=>j`HOIxS0=T3N9#ygFj=`=RcduRQK)ndWKfcyiYQ{qKHe z9lB(}4`6ANmxUhfvzV2B3x0B1<7a8IqF|@_f*-*0kD4#|$$UY|@uTC$AXZFNi28vPU6f=8vw_PN`^!0ss5q?V4w}qInnXWZaB;Z((GGU~q%nkE9_uxp z$D6W?Z#BMIE?jHqll$oY5P{?JsxzCyB5&#P$NQcb*dC2G>h)XAr$Yu3#|u-;9y2(tP6S;U zis#GgnW9^K`K-C>VtMe?l;Z}@tgqf1gq1#9cp2u)`RS5_U^Y`ziEWSNsVxx&)XJkO z8Ti5yE+@gk+BnShSq=d8Z%fkIGOa7?#1o*wD|O;E zxOF~bg5-rb+HPiqm?b&plFP_Mufxsql4+{~17n^L3=O>6%Z4TM0um&La8!XWxe-H) zCTT8(%XjbLS?($0kK&l%j8QaLrhY8W; z)Rr(}^(_*O0Eq*5w``C{bh1zt){OxNKWO8R>p^laA=E6?sRF-5DME1z)AA$V~ zEzm;XX0eEbB)dkUI+7waCgl7QqPK-U&X>oR2;(|q<##0(ENDoKcwvh79WR2qf>P_k zNz6oUbLBf3a6;1ZuEb@CoeqkJUVN-RddXp)uE3GTAZd4o*J+^YV2XaXC5#orN%^OL zr+3o_eNh_Rut!|Ogt(FNVma&I3py~kQOtjNsm%3LH8JQ9z+vz;xrCYuPMeo%ub7I# z5unAU9SU-)zk=xDBXiLhL`jFQ^HDx+y9=$%jZ8E1_xIN$z#wja+?YNDR#E<6hfh_6 zqjp5y3Q{cw$;Y-oIB#LzlT{B3k7^a-owpt&7R37X9CZj)Ihk31K*XH}eEJhp`5`Rg zMTaPQheF+{?Rm1NI*`d;0yeGbmpSGwog4VPsLFNJ_0O|0+`}Wy=2a@pnJ=R9xmO1l7{37}_BgauQo# zMAPztC`BnC>l-{-=t1|dETIN0SUL3`pJt0zyU|h@%-m1Mlnyr+a3nle&QLvMMY#bm z8cbHHVsIJ5?4*x7L$xgQ4YY`$`zVOk{*wdMb+|=Pf^rA8-j;))HCAZLdH2Yz3&wC# zdU@6%h!Mbs55|SQSUUMYG+>?xB}5dqJ5Cz}FX_R|H;_Et!nOcLnvULu1TxIcQuuu_KYfQWnxtK5T}MjK9RlbMJgfG1-{ ze>tpTx&_FVU`;r>0!2{uT5SW9HWMUWO|MHQP^`D|Fc%?cFT%1Yc5^dUAOs8(KvH3h z66?tQt|)g$twzSYVhBK#`~tmAYSVKq*5Myx)F zhqf*#7@R;n*S8w{iX83LB7A#=_7*JFfkG0L4)Q&-g?%a8A=1wmOvtd}Y!EANWh_bk z5l2_S&leFCBhQ@4v%s+>LQv1{ELD011d(+J7y#Pb#OJAB*4L#VwU&*bAJ|h-$RkuhjRl=b4zsLXl&Vj;fd)BRS+qLfd&~>vd z7mob=>s6)GWkts<*PficGj3kup)+3xw>$h}$E1%BpRR_dTuoY8{P~AF{(V+C_f*T7 zYrA$iPcOYN?qGFpaOuqqaar~!@~RuEKg!!Si{%kB{VeBOpIf?MdZ)R50dgBqp&q}Y z;5Vl=$0zM{T9X#H68yom-`04=O!^S`9q{+|I;~k|-TjMHa{!ur<$GZH4()Zib`GnG zz0^|g@)H453Xq8SNaE;%8M-o=`zpz_)CvB?T zm}mVg8(8{CurVPV)*PnFUC?jw2l}Z2AUu8noeQAp!|miQ?-d107QDNn;4|xUzj|8w zUk?jyMj6pe^$DhMJzSrox@v;%%B*z%Gsz{#!97a-Q?RaF`v+J11*gu3S7-y>$WgMn zktQq{`G*V1evaLU==G!^Gj?-`iz*#%o%LPGMPKN|eNY$j1~e{6$kV`A>%R_kmiSr1 zKZ-RRjQkcSC0P{&I*)S;iC{W@=A|q zzg{12W7TgWtxWYq8Y1FX*VRRL0_Oe)T$7&4B-Xh~G&H`A^d3>CVmAgdACwaz=N~{t zHFC%W=qvvN>9>KoJ-xkaNATno_$x%!G$=N?=q>Mo{f0{^fk%|OimyW`2Pe#=N^{6X zZb4on|4eeYIq+|Edzv9sOi#ZKU?Fj=q+vWrA{p!POj>B!zHe}^%U z$V_uXk$0G=7}|g;naa}?MGk>pE4`BbB-0(xAU`>|Yh)*qG*U>Qy+CI|WBi(cAo?FP zuw?LyZXZ9Vqt;*XRE%C|A*cd zhY*yW93I364Y83dPp7TGbr1B5(*Rn^3AdLa3PLqX(?;hDqJ4~a&b}*`$Ft>f-qmnk zB!;Mr5&r;cY^zQ_LqsrC(IFJYe_`QXbKo3}V;OmYg&8%!9Q}sV$VK0Gblf-4V&A=Y zUUUQa??&YxIL+*oloUTp=8`8x$RU<9#BmI+x1s4WCaob;Ud}=D*~0qwCaeP60nARn zU6E_GL zR0K+CcWiVg?M69XSg_}rAj}I<*sQ#8o`z51V^wb=Dgp{+ht@{>8hcQ+QZ?y_X)1_50SO&}61bZG9*i08WNU?)qw#yt>SAmt$3P#AJ8A30p z6|T47R5KJC=YX2VOI&PSHaXEJuNzf`B0Uz4*EVA{!If;vj?;!{37EkGxYmuIsNg8dL*@1FJ#*gig5=6q2N14`)z>gG z1wv=mkloLMDt~`zky4n7$yj8E>YiBrO-f3%9%w%iHaw>-TMw?O(zmecSS^Cqvt*!1 z`)*O~HY2HiR}#QiCn04}yR)hgR-V3ae9zP5G%wt5`%@)5AO=ab1KI=Hu8S(OLXEW5 z3Q#qrEKM7;0u%KJE^cJOp9S1~*4*<2>3~TK`aV<#Yu=>E9MLH*8&ZsPz|6*mOW`(% z6cwCi_<99zYobqAe)EdmHoK+*t)$C9Q}r&i5!HURp>ob@Lq}khijTVHqT9y5OVGwb zK0H~{WDvbykK4+mHS+7|=sCEwh^8in&KW?ozFel1P^mkutAaHy^%u)GZ0w*Ro|uWh zc&*jmAgJ;yRaS)Su<{_YcH;n|E#=~rWqdVL5w6{fPH-XdsyhglPHy1&Ry2gC7$PZU zkx>n_>wFG|-1f|fo&>t<@?T`H*u%6Mkgjp8d@64g24ik%gO?JV9hDnD)#Ov zf|kRvDTh6(rD={JIQfSAGV!%MT}4VN4@9%uN$bnc1O^-!Ea@Ir`yM=XZQ)U_KX~w# zJs~1burcpTLxwJeN!%z#np0DG#jjh_gE3_7BeU3iGb1Fqkw&|ktI3Rvh?oXq_iKwE zz1jjYr5_CWiaeESq@so2($MffW7HEvL-8s9Q_B7YhUaD~*){)DN~1!jBpT1a4ZV4?p-i2uR0|G~B43D;t_r?hqV0`%dl z|NhX8Y98Xfy{q!=KjG#yP(aoHga2yV*<3H;jfPP^UQ?ThY?9y{j5U31T= z71!EIWhp6m>lpOX_+Z?V%v(8f`v1$XAmY{)L%5`@Q;=Var*D9vCK4*i-F z>0!URyGLQ!f+o3QK@R^3g~;7po3hB)YmsR-Tx&6>QUEnSSKNKtJVe zLLRvSLXF%ed#_|u!PkaD%i?eZH_FbZrl!_yT$#08U%>B`Z8WNa)I?EN$b@8U~6_%{?#dycX) z7_tQyhaFV{9PKheo6jtE#7mp)MtRv`aUOVl@gH_G$C`fcGp6I@&VjibUA1}8#nE3~ zBkR7TTKMFt(nPCPx!2w+aj_d8L)t3NRp5P7Q$r&_!g}_05^1w{7MprC--MohIpWqI zcGCSQY07OG{le{}ii$sz0?aFAH+-p%1epeMBuK580#FLqsE%OO|KzuanCzv~=ir0p z{L36Z;*3?HjF+TFGTBHB*1hH4z4d$PjE{k77@g_jp31$gzN_ReSp25Q3J$-*4x5|L zKyC*sAC}X8C2AguVTH#>`b?G%HwGHLQ(phng2F!I{k-UEVeYi)Ii&Cffm=jJ=Dt>dq)3gT1?b`<3T zp-ZJ>KtIn9z5P6csqk^<5DKbm*KwFwWiFh^)U0KSnwp}ywDufzsaw%kN&BMtU@k`` z=sb7psnHvKZkCsmeqc*yRki<514^+TqMDiySV5fCU+rCa%0Mz^<>fB!l!(8|$`b8H zS%mAE+b)yR3veSvwLKPNuXy};c2SX!zTM_XPrFSYkJedH2ad0+JM8em38I8$`C2!$#aB{@Ll*O*6u-E6XO)2i0v9a)CMwr)#78exs@fDh zQ4y_amiA9v)Ip}|AipS_T9gBNhr%BoJ^ETAW2e`O+|qWawswlXm#tsi0#U8MIrjQS zwy>No{T!EdUHI__(*yZxp{?7A7ev zv2mwLvi$v7uK1ul;3azcg6RH*Vb}xG zCztd>Gk{~)8=_W$TD{ZJmm3igk&wSyuvp*e+lvgWX5fet$<^ARz}$Kz*$l|uC?^@h z0|5B=P$GvVleK$@Ivags`rCQ~1XeQ8LiK8*SU3!~;Tf`S;x;~-#xG*gbf>7Q41CdH z&|2#<2)r4VPFyeCX(8U#nP@v*E=Fbk$}p(KrxJ|1X&Gu88wxYeI`qlS?krV)tKdb7 z<#i1ofMg8dvk=v1jOb4L{{=u8pkbi`)G9_@o@&PIO5D;c7F`QjAW(mASyk3ZxpVtpa-h&GiF?$Cmxi=S2VT8nc1q zGlHNtp}9b{{#cn12kpH?t)mSyh~Gi_d~rF~*-U%=hN7LMeLSBdJHSuAROy{Z0(ZOz z^2aZYBYJKmtS7a`19^Kwtr#j6R=2V0Gib6ffa>9co%cfwG4)Ghik>;e?@q%zz}V&7 z5G!Rcn$4{v=>tK75A%q#_x)vq>E>{>FIp~8CY21U%GI8hGlj7n$wvUEXg~}YDDiww zZilXg4@3iMWb;^ByNGiIk};8)cwGbk^8oya;b>t1!aM@Ww5#1-m5Mk=Al6nv2kB8w z<2yNlfs(#qs-P#AMDrjtNk0Zo<$|8ZNi*%E_PBxhCM0mW1HtlPiMRBWw05$v{uose z4yC|atoEM}!i9EsVXgrcb=YNSUChAFOO?DNta2OwDoa-c{mi1Z>3v2P7$Y}uJR6Qa z1CU614uGkai3P9{hrZ2J-E;)V5mX|8-}oCOnR;*pn0iunZmw|SoO+wZ(9cC^8(e(& z((yt3-f&f?me8=U5=Vk+yjDm=%pm~|S*<6={#Lq_yY!PmM*U)ARqkQ zRLK_X08}w9vUC$56jqf1=GOXegvJ1&EKSbJ&K6oRmG7fGoKCJMT(c()3<8F$G0?0+ zvU8mYN|mp)t^wCt=S8y#nXFG||2{4WeLFK#xN)WO0n`wyR-g#ebHDmCaCj$ zJ52H2E^jdmRe(iSC@mmqi3$Vw#_!_ejOnc5nLc$?fG+38a3R5R6&AUQlQsB4jssL9 zhAl8HVDorMB(yzT7L4w}inJtj=>WC6VOM19$iO=(PLTcKHQC~N7&U>f2g7KN)89&a zJe^hdi$;Mf9&Ky2hha_8v#cIX_Q=Bg^-tgHY9u!`gZbc_uYTEbWWM?FsWG#@oU+jR zR=oMr_|K>8ed|NZT{AN#j*dUOFvjMiJOB58?;p23ciC0BJ?TGRRQM$P4-p-sDr!_mw7r4)L`m%>e?f)fFGl-WcjAB~vunSM? zb70)SC!Os$;o_un7@18iOvYzGEfskBB}>e*p7xw^-~|qv-I6X;J0kSTGRC1 zMWMA6d#LFIkFA;{Ds9dOiKul?VSY{Tadp;*`U|i3gf6DXcwu!}$ZfjTg)MlGWnr{e z`w7<5`bxHYG)at84W5Au6e*Se$XvYPJYT#L2&Lm}yJcpUkrAG?scj8??~J;Q_rs3q z>vWyx`h&hs=d!__TTA!2m+V5UtIUqlHy!j7PxnSUt1pA?ZL?wnMJXt6w`G!lVyPbT z^)&>Vp&T&#;;ef(+kQI?WtK3`gi)z`0vt0Rh{LA?{niV&=R2dz%G_3EN1XHWve`7J z$+e?xs83YvO%@i$^_6T_3klHFVc?E{ z%0@go5=P~TpwBrX63q;-J7D?!9oJjGtpQ`Oy9(C8P4@1uQY1@nw8Ii>|GE*jF(P`JK0fK9LL=^40j0kyse2QBp2Jp|ogZ0{(-V^FA zrmIP2Fe5=TqEz1~m2_x6BK3{(AZZt2Z|Dp?m;HqB!dP&{owNCHZSDHXe`Vj=V1vJ# z+xlk-(DMDj;e+)(lwFyX8EpeE& z-dVkFtZD}Jcw|F5BYRWZBw#Xg?Ak8~cLa{J$$@+gkV9En1jS6`8 ztPs_|8N=E{=O2h>5IMuo%V^*bA(9#w&g~j~K6_)#>q3$_U{o`6( zk*hjlTIKOs;R}&|B`y3XQ$q zrJtAT7n?$v;F+ypD%!ClEv?#HS|as+ggq%EKmeE$6K?L5PH07A#{CD9LM>T-A|Nq32G#h+Rz9$x!c zc?UeAQ#&3fNhiuYv}urXv-e~}K__9)v zCjhF>j0*k~41?Fo#X<~{;jc+V73a6Rqqgq)XI~ivy*rr@}5e>zd5O8IFzX5iB+LN5e-89y%)n^`2IuLWjb{i+L8~M0Tz6O{j5$<*}+` zj>ra7#iC6B-sGG?ke;;j5&$gt+F69%SmjlgVIorV<|cpY`#yPn$fd1<`Bgr++?#;L zaQ)ZiD}u8b&a8-gq-clA+(-r=5eL@T&;-#y!&Ud`QNM&Ef4YPobH@Jgw4d{{167kr z6a}D70m^$7*a_Q`FzdX(D*L4N228026+YG@4uwP?P=V(erTzqm_~#|VxhIs#p({2}+1|UcUxgp){cpoaQ zD>NT*WZd5#7QsipO!7(Vz8U(mKhWm`m)IEg0!( ze!Gea8i5yY`*`?Z0-!|fqk1jEH_n205Y_35gxf~`<=pM0a!&h|a$1#Gm<$UQHm3b4 zgqM{pJQX@YV($Jb4T#A3AEcndi-`76itA>K1Bu#Z8&{st5auHU&ca#XBG2<6@>TG|wWGfDLU za-ETTFfoxhMKGVYA+t6Udh94mBeY)xl+dD=!|Ol2Dv}~A76JA=hm5F*4I3C|4I|yS zhksP)nQ<}zb1-La_vqL*{N{L`3W~ ze_Z>cN1i}h9Vz3=K4p<^>b$@I?_UIe*~)WRo(Sf$+a~&&i$$lHhs9RPPpO;Q1@=8a;CPDi=G(KbD6vIoL*t7pdZ+`Kb>q40+f}rI);@t+S(Lji++DNB*ob<0PB(7F$?jeN%NJOdcxs3Ep~;KV!?nm{Atw+Pv`5z*$m zYbvdVQXNBe0V9=ZyL=!7t*#QQtp}??Kq?k>DXcAlg6$(7**&-Sc_|xE`$OQRl)rQst za8zPxJgn50l#L6z;VqRyf`*H0Wua|^pUZPwCccIpbkHc;X0$CsSq43ZL^d>J90;|T zEr8eaMStSw1;FScHqyodISI%$FF2%7y1d-|=Db)=w{;DTLU`)Gn8HT%zCzlbnHvu( zGDg+;?@Bf(mN21m7oi}QupJDq0Pm<6qKMaYk_2^{xcU_Gh+hM4gt!7%WmJ#^6CcP8 zi=oFE$ftPWa(rsl41=2F)26m7UWO);Ak88G9-}aX_O;^bBeA7M?H!5ujS(6AHa>cU z+Q6WV!+30NLBGKuYd!c@mpKHYC^ERcW*PlIst@;km~QJ@^x-?-e_w|Ey37w*_w8Bd zb-%4s-uZ3%C3Dka(#`P4Utsoq@BQ-+56zdpJ?>pX^dHlw9ZzmyeO>tJ$cKS`)pqV4 zf2Oxi|2FexQ)Q&+o@g^KS@YnQs)<8De+^gQ?Vnq2i+xM8=D;l*!p9C2znT<9tAfTG zn(u~vjx~uZ$I#lMu+?$`ij$XyBa<>o&C35s{`B!xRuWpjqKcC_Kl~I)GDIZ)8lVXM3zaJ*t7foT z&hg=K)~3&?^3S*RUfg65{C03PbaV^~68xm{%7^=-F<|yZzeW`Ad$B>}nItt2SA&5N zv7o(Kq#w9$6npzYus(f0ecC!aGaQ3gr4Yk2-OLAo+@sqyYiF=V83t~bL0cbIf=6I@ zk|~7iirfbApZJMpSTLlgUn5iENk&ja0v{D0mROOZ-eN^HH#^mwa5CbM25a!qUnc0P zo0uL>O$iO9RaLc29f19q+o>CdiFf4$-vY;Mhsqp9q83A<+F+Ge<|jyQ)D+s3 zU_*wpw=o0%RH47&#s@Z{@~Y;&0|H(h@=M97`G%cV_!K zrYES;81mas)4jF1!xwD{&q*5ywXB8-^|HrHCiH2W0AdK;F{rPyW=4u|5-X_L!yVF$ z8$j@;7s1=_sja@$HfSFSsdr=7&*p;1&U(A(dK$g@vo!`T55)E(Wr?$9l(KRv#>W_K z{Ps9gjcz8xKh`i$wXNH&bYMhha9ZC3CQjq}z9mfb9+T?+QRW7*vPG;Kffvj1$!TdF zzS_lSOls%(9!g7-?8bG5fbUAC4NjJLf%`(7<_g}`Ox=S_>3X<*-qxJSsbEk2-QS9W z5_L^H?i(0At-XSYnh8~7w521!@1~yY?Z|vu>fuFuKxraDB?rwY$opay0l{skdMNcX z?1a=)9uZpQlps{?!IvLH*|1oVi$AXS0X-e1uWS9G6cvr5jOP`&@uU{)`gZ4mQF82dgG_aiy5m;}(SOJBq2wrw z`be6HoMKAXsc>)|U(g5(!m_rkk3G5TD`STN`aFk-u8&&leu|ejwrurUZP;8e{ldsR z8gx^5&xo;lXV5NM7Z5^Q&<40P6IF#vwqe}=lj#P?Ib#c!{(+}y*Wkemzz6sm673zL zZZ?VskL6{I%mY0j)ak=uAK+01g=7z3@-0u3st9fT3%p`AVB;Ss^1f{f201ZHVQD0K zfQCTrk((4P@U;z@YnM?z{J}pJbREqSd?P&A)nE1dT=G}jB>T;yL%<)u6GCw^!-9EQ z{RzzHga?C`rq+`A8vGR-+EsaUJ@gbZs_+_cg7 zWuWVd^5JxM%Cfz5CTLAwz7pIC?O%hAezE9N8p;;<(DsRu5-7H6Zw9#g0^IKa1RNuX znqZ@e=Bsg0=B&u7^{S8}u3Vc@S@YNF$q5^G$iBFL#@(v^KyY3O*jix68vh~1z;K@M7f^JW! z;|XRcu$8M988t7zU^rO>{3u(xM_eBO3rUhwV$Iy}oY8}+%oAExr@KdJ_eI9$R6-kL zXB+kIKVJ-?ObTkVOpwqa0a{OI3rxrwH^h?5=PU{cYC8!nr9O)Bs&uOqB0JpN?bj#rdXFdAM30z$ztF;8ZkCB#!qn^O4C4=BP z^Ql?8-m64{JGHT0O8?Sh%Kc+Rog?%UXub?QB78LU=D?BiNsu_ki;mqS-pJ5kTF!!L zJuel+tXUS2&>BM06j7V~(Ul0oT_w{t2mBotp6satua}-uS)jH2nrxBhm~V}YsVA`Y z_QrNoMZNevFRciZ9iCO9GYNm`SnAxo1K{qnR8iLmw-hN=ObE-$*4fSa8z}a#pXb#U z;p*v7AzgNuFU`!_(zwfk>TVRb254+FrhQ(i2V8RG(P>f?j;NVUxE9INOzR+0e2tL8 z5D96|t-hBLvIkB3!0o_@hIB{jt#bB37n;WH!T1=v7<#&)Kj>BDA;Qrs_1^C}rS1lg zVbgXKR4WL@e2ocwQ=UnZCm+#fb@u|C`#YE_c*tG@sU20b(5ecuAck?jd9Sg9=#4W& zfS8woQB!yMFY?KY1Q{sj@igdX7;)j{12LEBX=<0Urvz#@tfVK`0iXvk9!b84-#U&5 z>CjCeE!VeI!(ZWB^Cov8mDKM`rUgDTefN%a92hMt(erj3sVy&$sEvnWD>C;k%<}>z zjQ;7}01fBHboVO_jo?<|y8ww)@R4}xJ%3~>L4Kam;XRC)h2dy~V`qX%dA>RoL9w`H zJg=Cfy%tMu$<|-p?3x$<6vd$4GkL`>dshs&<;+Bi3AMgawZ4)nzOJm2b8&J<^{7AF z@!AY<(3%5KdgWB0H{dRn4^@aomm*iWS}TBH>}xs;2W7b1!@rj@}5C%tv-RBe%1cwcfj=` z0)lo02Wh0GU?DkLdOE<%;l!@LgU!;xh&r+&2ilE8|G)}@Y3*p8sv_so=wY9=fjhz2 z`5;c+#h|yjGHW8@PvNtW^PKwju&KS168>9aLJKury+b zXvf>9Q{F)r{AZ1qx1{`N^7iWS+ut5xF~xk&gLD6MNWE|Mg`LG&Ym4N4|6Y_5z31S` zGxtAV~PxX5L=GTuKyDn@WIaRWL1Ia$Z-9FFoxz$e| zju33VT-}1K>M28ha#H_hBiVD%=kd0K$%=e^B|kT^4m`2quk?Wg({wxqPm)idzT1_c zKFiZ`*OD_?lu%k}i9h##FM*xbIXoo#%nVj`F6EJwY-;pC@OKu`WewYNG+{jz?E9ch z?l-l@j4~FhJ@`i$?rgPs<*65P8R39g(#QXGu|p*Um}r!^ftTvP;}$W_h*$sj<-uUVLb zzQtz&2(06u=iRzBpVs7sjv{EnIp#m-3$CPgPUP(+yE=7hW$#pnuEPcK`Ufzn27HXP zzIlDuX|(s`15I6*#$LPvPd~Y&^&%@O9uj+TZgNotI(BMo{`IXY30K7Os!n-G&o#}$H8zqRx zr7X6otYWS!5iPPWG(njQ^FSj&rbBogZgnrY}PETr9=|7HR(OfVDEt@mcD zq4V6@JD{NRq4iIA?5JDu@~|LI#5G_82Ijtb3*}bxt@-q#`JG;BO>cGSvw#Vm=hl0X ztn%M1E?<}-)U%^_sR11 z*|<_JMfLcD$Q}-=dyBU_HO#{vv%1q|^}Zc$3tG7U6}T}Rn$goX-}zd?jHlJ7i+bM# zCd)uKndlu>tAZxZVMmt#>Y!a}h$(sv)JQV6Bp#=}Q00mvdaUi`KD(pd;m2rjRQyM<254 z$Wfy}dr!)h_R+u0b<~ zQFM41seZt}DbRNDMPV)gaZNaF9=ac`A)qGOJibO`N2e0Uq%PAxCfWnsd%c5EmQs7G zqMSX4AQGMmZt7{^=??6k{-D!?=iW+okM02k-7h_YzPN_XSF3c{JvTW!U6C}}uc?Q+ z>HZ)g)E(XHz{0P?8s!-GN6X_+C8v}t_&cmov=}@J00vs&D%5FH?hWR|bpZu3Ji##4vu9nAl@Wa>D6U z*^S9`x-b*cxPjLeQAhARGh9eOycF7NBj97gfNbV-kfJ9$=3YFSayH*aWu=I)w6A+CmsPX`Q zV}VTx2`|n8^itCHzsOi9(#mf z*2uhI^Jv2Fb5}0MTYtjQRMC~^p?0E>ug*lRUlEJYXQwA8-A!`PFX=aDG4B9)=Gh*5 zLZ)2#@K0G(i0aMajJ(UT6X4xwC+ab(Q&Y!yIp#;B@yjP@SC7m)Bs+2TP7T$(%Q@Qkb+LhlTWPM4axFK)ci2Lb0j zXd>%0VNF_xH2(sErU;h{W(k+CChFTWm0xU=92_CQOdI_dkiX*vVJ^U&MxXtBi}aFS zHNjFPj?>M=orK91!Dx=MGX#KnP`oFD%MLLZ(P~2&U%XLb`cg97u7LI|dAKo{c|Hd8 zDPZlhQj5kk*mF@D`c!EL2CuYAidN{!U6Yez?sWG8n!!NV^QrAK+EyRpNGGm9pA* z9;0h8$c~YGvHA=c z2a)3N_6YB{Y}S}eeQ(i^4>wQUzV-Ni!ou_)-yM5$jbqx_-)4Wc^@rex>*Bvn*+2H| z*S~$~9Fo4|)Z_^Pp9Z9uw;cU+eZYuu`?mV-uN=KSG$tB5apH|!jf|2bKB$RtU10&jtUUj$ExSgj~H>a@ns zGD1}n#7!hcMB;F`ym{8=KuIoHUv~Di1i;V)%x5mJ!qqvGq5`*_?f3mz^X%h_>?51b z5!ZT?uaywnBT}hl71>#)FcRaKN}fofG&X)$VsglREZw91{#y82g5)O#+|=f-I+ht0 zYDv>s__|9yd9uIC?<8UVd14M&(u>$mTHG8EenF{axuN8ugOESU<#owCUP(#(M!>HD zrt6qF5Z#gj@L%%+Cb&xrEGQ)bME?o>#kpZ$oRyK-2LGPtJE2EHT%?CDr=dBQ%{Dbz zQ2?roKY*11nEjLa0y{w1dC}_PmAjc*Fbi{mUJE|64(;5-d&@$>Efamh1unNA1;-0c z8=R}*XQTikZiQujc0E(~pS5QT@jrVs0oHPZIGxM3+GME7fNc{P^{atkB1Z&5hfcs=uJm^w}@cQ5T zB+%N*slJ>R0=q>2>cm|mq%i8CY$8^+3eggklY(@QeVt#!VXJX$8R3>KjNWqBway=K zjYqKRc~lKwxeSaA$8(G%i78yGM;omD4q!5g)`6)5$HGOLasVs|QMFCqHZDk+CjyUc zv|xqlIGyQ3*|rb6e`TA4d#w-L8jua1%6vmi;V##Hwr{QlF5-@f+=>9E*j0*xUIkDY zF^Q%vf)4qjRnQSJxY!dGy3MmY()WgCeVk}tOGwe6O5f!M==ldB;$gyX1kHJIsSOn&3dLxyBSqa1yJrB1mgbBjUzID>Xq^S6 z?GypLU-U{nopJ@Z-h#p6n_&3?{s9(BhZ&a~J{sMFLHlTCNqkT%m7{@Z&Ma~v>6if? zec8^AM+Z3c1N0sdS_?P%z*8jq;^UVLkp6&4P9NYXH!LV+$wV*EX3`)3zGO&UqkjYB ztdOL;g%x6GQm6{N;p8#TyAP)Ck!SS7O%%%24BA5<6%i>fzddgDt37x!fhSMRP@aK7 ziwL*?{3T6#Sp2=c;Ddqk&?Nz>s8%HL3X*@$X{3HAdP(LJlfd144JaeE5OOmfXBWx z3arE*DdvM$;tR#K+5Y|xpyyy9=|2234?MOOE5pzp`AU+5v=f0p`|p9o(tw1s$KA)Y zAMgUz(}mi^bsC+zVuE(l$h=h9!u$UAW{t&@1z!pGZ#*;ieGry%$c?^; zJHXc1+Ea*5EI=t0TnSs`&UhP4GxGxyH-u{6mlPI`1P;MV9u5XlEDA^fkB|1_EH>9RreQJIq$1kdNab1gw!8Cb4OQY=8=fkN*M5m9ii8zdeTy4LR_Dt; za4KFDN}#D%OeRE4lWN)(aEL&NKy_rd^fUkll zHA!jC*z3=cZyyosU(01c4>Be7neK1QAQOrwuO?C)exF8<4hFK%9s!2*NPZt7_-bSx z&VNGFNA&0@;r_E{+{dJ@us6$CmUaD`8CjPvPcRev_64{-Xw=?JoyAuRv}qdRjioO_ zkvA)NvolFm0yXm1nKLt7!L1Qv=E(Sz2iJId9fg}yqk!&+Ym#AAyna}mA#3$KxW%+8 zH~dy}KzJ4h{qfW(pVEPNpJ(D@zLtS;+#mBL4AdHr%86Hvg{fz~D)vR^D9&U@HI6D* zU^fB>tw)`wG+A`bMw$ex4b}U>Ka((XSJ@93?E1=g3KKS4Eara<;@aYA`@K>b`hJ=Fjl_TA4or;%**C1#5SJ8_CQo zEI%V7eRd}7g&$E6Vurhc(Mg!qA9hig-5YtX#Qp^f(NB-l$hX!L53(Y3!DUGfb}I}h z*yq8%;E}n&FwI}%&xi@tj~^$@dT#m`9-~?P#vAs_y+57j*THF;UQAENY656hkS2$i z$Uy$Wh=!q9Jwqs^VKX!*A!(+3JYPBm_zzn8i@?B-T5{KK;9t&f)0qILzX=S0=_S9P zJr@VhUo+Ts^EC`49@29HO#R(n|C0v9>+=wK(S3Z#y?axF|2xKSX}|<(afUVt5u-&R zV~vUgwsoChGb>Vjs3NmvV-)%(gJI(iSbXm1)q?rtn%y>%i%`3qtj|O)X(JL}vsWmb zyiQ;Z%hObd3vy1j(5N2r`XkHNG67aP5xfY+ELW$0O1%?Q9R=pNB&mPADxOdv`|vZ_BApn~|iEoNT84AuM0`V9-Z0cjI4%}b5*u~jL>h};7dJ3fkGb>L4ftvI`$^Nj_o+D+ z@$0=u^AcqverTP`U1fiW{KsF7D^bQ--jB1m-v-i{b@uYa-?6#kfz5>4K)-!@cK zy4_*bDtlKq4-t9Gts6HyS}IlEiP6c)veNL}J$zk@jZ%Pt_7pvoB~E&dHlFAiGw3OY zQ^VMo>o?9>4KB19_nVqR z4;oyHPOTpmYo%{cH}0zJe`#*eX2`5Uod!dZF2`2$(HkUbA&&M_5vZ_(k2GwOtr_<` zoxqzzDRoV)DYf@z$L5Te_kBv z5&#-rfUpcZ^FrgF^4?OvU%Z^4->>pS`4HkaXuRZE|Ozc3_NNe^LZNdj>MNt4$ctSvCg>KE!NLddA{D{ zzuNBP+m7;0sRQ+?k%%&uUwy-o`Kz;c7MQENbAZtTm9Nuvw?4YFNL-aulnKjKS}6v; z8p)G8gixlNWdZ~R+RBVN?gF0YPRZ>;;r{bwVSD|{?M6B3Z!|vBC4-)OHx~9Dmyi6$ zSGTX%g5k{0z6AMV`}IC!^zmT8NV92jOXXMlqI-|kHM{7r8#K;AAjtRH7fEfbU`f58 z)-9<~?Nz{3+|ZoYsN#7$Oz0p1e~YiHAe6xBd}70SF2^?A*;`pQ*#2Il8z&SdE3g2C zun)ru9ZqTjNi0BV-!1;(XE(u73^&CgT(eb(B_SxgN&Vx#~S1Z&}Z)6~jq|BU&HrG@kQP4O_QIFeW=WDstGgzKRU zJfqh;v2plhGm`47Y52!cL6im+6%|7VNz~dhHmV~O>Sb&^Lg}^pikX7?W0jDSK;}m> z8%SuoNEraDHU&ksxu%k9qKouc&S(nQ>-QJh_d_W1t5JDYR9z#p9yHkf0ITDn{lTfU zU@(#Q#>5}j*CIM2yJ6RIxDKG1S`5&o>~in0YHOA`Y~ohWB{)v#V{@2VdCQcc>aVaX$E*$ zSHE&yo}4PMzzPd(_}l=!yV3`R;kE810YKb$ZS6-Ed!ET980 zaw#3oocn&4`x+v3-?0DHasx{Pc+$O@avQvQGZR7@s7-6RlFBfyWEMe=(A2ZAH@yhz mcWqZJ>3Hv9D48RUJd-=WEUa`n^9J~z_kzWrp7H$m_WuXiodER! diff --git a/doc/_static/funding/cds.svg b/doc/_static/funding/cds.svg new file mode 100644 index 00000000000..07b2482727d --- /dev/null +++ b/doc/_static/funding/cds.svg @@ -0,0 +1,27 @@ + +image/svg+xml diff --git a/doc/_static/style.css b/doc/_static/style.css index 25446d35659..fcbd7e6116d 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -308,7 +308,7 @@ div#contributor-avatars div.card img { border-radius: unset; } div#contributor-avatars div.card img { - width: 3em; + width: 2.5em; } .contributor-avatar { clip-path: circle(closest-side); diff --git a/doc/conf.py b/doc/conf.py index 204a2ccac1d..3759d6fe335 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -171,7 +171,7 @@ "seaborn": ("https://seaborn.pydata.org/", None), "statsmodels": ("https://www.statsmodels.org/dev", None), "patsy": ("https://patsy.readthedocs.io/en/latest", None), - "pyvista": ("https://docs.pyvista.org", None), + "pyvista": ("https://docs.pyvista.org/version/stable", None), "imageio": ("https://imageio.readthedocs.io/en/latest", None), "picard": ("https://pierreablin.github.io/picard/", None), "eeglabio": ("https://eeglabio.readthedocs.io/en/latest", None), @@ -841,7 +841,18 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): ), dict(img="doe.svg", size="3", title="US Department of Energy"), dict(img="anr.svg", size="3.5", title="Agence Nationale de la Recherche"), - dict(img="cds.png", size="2.25", title="Paris-Saclay Center for Data Science"), + dict( + img="cds.svg", + size="1.75", + title="Paris-Saclay Center for Data Science", + klass="only-light", + ), + dict( + img="cds-dark.svg", + size="1.75", + title="Paris-Saclay Center for Data Science", + klass="only-dark", + ), dict(img="google.svg", size="2.25", title="Google"), dict(img="amazon.svg", size="2.5", title="Amazon"), dict(img="czi.svg", size="2.5", title="Chan Zuckerberg Initiative"), diff --git a/doc/funding.rst b/doc/funding.rst index ddf37423b8c..bbf25a7165c 100644 --- a/doc/funding.rst +++ b/doc/funding.rst @@ -29,7 +29,7 @@ Development of MNE-Python has been supported by: `14-NEUC-0002-01 `_, **IDEX** Paris-Saclay `11-IDEX-0003-02 `_ -- |cds| **Paris-Saclay Center for Data Science:** +- |cds| |cdsdk| **Paris-Saclay Center for Data Science:** `PARIS-SACLAY `_ - |goo| **Google:** Summer of code (×7 years) @@ -61,7 +61,10 @@ institutions include: :class: only-dark .. |doe| image:: _static/funding/doe.svg .. |anr| image:: _static/funding/anr.svg -.. |cds| image:: _static/funding/cds.png +.. |cds| image:: _static/funding/cds.svg + :class: only-light +.. |cdsdk| image:: _static/funding/cds-dark.svg + :class: only-dark .. |goo| image:: _static/funding/google.svg .. |ama| image:: _static/funding/amazon.svg .. |czi| image:: _static/funding/czi.svg diff --git a/pyproject.toml b/pyproject.toml index 9abefee7ca9..00bfa549de1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,7 +145,7 @@ test_extra = [ doc = [ "sphinx>=6", "numpydoc", - "pydata_sphinx_theme==0.15.2", + "pydata_sphinx_theme>=0.15.2", "sphinx-gallery>=0.16", "sphinxcontrib-bibtex>=2.5", "sphinxcontrib-towncrier", From 79d54dca56e4cb8c1ed6c0eec76ee4a5f66739ce Mon Sep 17 00:00:00 2001 From: George O'Neill Date: Thu, 2 May 2024 17:49:31 +0100 Subject: [PATCH 031/153] FIX: `mne.io.read_raw_fil` handling of bad channels (#12597) --- doc/changes/devel/12597.bugfix.rst | 1 + mne/io/fil/fil.py | 4 +--- mne/io/fil/tests/test_fil.py | 32 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 doc/changes/devel/12597.bugfix.rst diff --git a/doc/changes/devel/12597.bugfix.rst b/doc/changes/devel/12597.bugfix.rst new file mode 100644 index 00000000000..77997893f0f --- /dev/null +++ b/doc/changes/devel/12597.bugfix.rst @@ -0,0 +1 @@ +Fix bug where :func:`mne.io.read_raw_fil` could not assign bad channels on import, by `George O'Neill`_. \ No newline at end of file diff --git a/mne/io/fil/fil.py b/mne/io/fil/fil.py index eba8662f342..286340fa0d0 100644 --- a/mne/io/fil/fil.py +++ b/mne/io/fil/fil.py @@ -266,16 +266,14 @@ def _convert_channel_info(chans): def _compose_meas_info(meg, chans): """Create info structure.""" info = _empty_info(meg["SamplingFrequency"]) - # Collect all the necessary data from the structures read info["meas_id"] = get_new_file_id() tmp = _convert_channel_info(chans) info["chs"] = _refine_sensor_orientation(tmp) - # info['chs'] = _convert_channel_info(chans) info["line_freq"] = meg["PowerLineFrequency"] + info._update_redundant() info["bads"] = _read_bad_channels(chans) info._unlocked = False - info._update_redundant() return info diff --git a/mne/io/fil/tests/test_fil.py b/mne/io/fil/tests/test_fil.py index 62d4a587d47..f3badae750f 100644 --- a/mne/io/fil/tests/test_fil.py +++ b/mne/io/fil/tests/test_fil.py @@ -25,6 +25,21 @@ ) +def _set_bads_tsv(chanfile, badchan): + """Update channels.tsv by setting target channel to bad.""" + data = [] + with open(chanfile, encoding="utf-8") as f: + for line in f: + columns = line.strip().split("\t") + data.append(columns) + + with open(chanfile, "w", encoding="utf-8") as f: + for row in data: + if badchan in row: + row[-1] = "bad" + f.write("\t".join(row) + "\n") + + def unpack_mat(matin): """Extract relevant entries from unstructred readmat.""" data = matin["data"] @@ -159,3 +174,20 @@ def test_fil_no_positions(tmp_path): chs = raw.info["chs"] locs = array([ch["loc"][:] for ch in chs]) assert isnan(locs).all() + + +@testing.requires_testing_data +def test_fil_bad_channel_spec(tmp_path): + """Test FIL reader when a bad channel is specified in channels.tsv.""" + test_path = tmp_path / "FIL" + shutil.copytree(fil_path, test_path) + + channame = test_path / "sub-noise_ses-001_task-noise220622_run-001_channels.tsv" + binname = test_path / "sub-noise_ses-001_task-noise220622_run-001_meg.bin" + bad_chan = "G2-OG-Y" + + _set_bads_tsv(channame, bad_chan) + + raw = read_raw_fil(binname) + bads = raw.info["bads"] + assert bad_chan in bads From 7a4706b078b2cd0b9ccf4b3c6be83039ddc88617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C3=A1k?= <66417283+michalrzak@users.noreply.github.com> Date: Fri, 3 May 2024 01:16:16 +0200 Subject: [PATCH 032/153] Updated dead links in bug report issue template (#12600) --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- doc/changes/devel/12600.other.rst | 1 + doc/changes/names.inc | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 doc/changes/devel/12600.other.rst diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6ec575d28e8..ddd5834e533 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -29,8 +29,8 @@ body: Paste here a code snippet or minimal working example ([MWE](https://en.wikipedia.org/wiki/Minimal_Working_Example)) to replicate your problem, using one of the - [datasets shipped with MNE-Python](https://mne.tools/dev/overview/datasets_index.html), - preferably the one called [sample](https://mne.tools/dev/overview/datasets_index.html#sample). + [datasets shipped with MNE-Python](https://mne.tools/stable/documentation/datasets.html#datasets), + preferably the one called [sample](https://mne.tools/stable/documentation/datasets.html#sample). render: Python validations: required: true diff --git a/doc/changes/devel/12600.other.rst b/doc/changes/devel/12600.other.rst new file mode 100644 index 00000000000..e31e0fa7030 --- /dev/null +++ b/doc/changes/devel/12600.other.rst @@ -0,0 +1 @@ +Fixed issue template links by :newcontrib:`Michal Žák` diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 112418f7e72..f9c67b65ce1 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -382,6 +382,8 @@ .. _Mauricio Cespedes Tenorio: https://github.com/mcespedes99 +.. _Michal Žák: https://github.com/michalrzak + .. _Michiru Kaneda: https://github.com/rcmdnk .. _Mikołaj Magnuski: https://github.com/mmagnuski From 1c5b39ff1d99bbcb2fc0e0071a989b3f3845ff30 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 6 May 2024 10:09:14 +0200 Subject: [PATCH 033/153] STY: Apply ruff/pyupgrade rule UP028 (#12603) --- tools/dev/ensure_headers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/dev/ensure_headers.py b/tools/dev/ensure_headers.py index d56f67ac32b..435376ace37 100644 --- a/tools/dev/ensure_headers.py +++ b/tools/dev/ensure_headers.py @@ -32,8 +32,7 @@ def get_paths_from_tree(root, level=0): for entry in root: if entry.type == "tree": - for x in get_paths_from_tree(entry, level + 1): - yield x + yield from get_paths_from_tree(entry, level + 1) else: yield Path(entry.path) # entry.type From 01a60d97a8cee2864dd81ac9c25d6ba126a4af2d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 14 May 2024 16:59:54 -0400 Subject: [PATCH 034/153] MAINT: Use intersphinx_registry (#12601) --- doc/conf.py | 30 ++++++++++++------------------ mne/viz/_brain/_brain.py | 6 ++---- pyproject.toml | 1 + 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 3759d6fe335..99e7a2326bd 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -17,6 +17,7 @@ import matplotlib import pyvista import sphinx +from intersphinx_registry import get_intersphinx_mapping from numpydoc import docscrape from sphinx.config import is_serializable from sphinx.domains.changeset import versionlabels @@ -153,32 +154,25 @@ # -- Intersphinx configuration ----------------------------------------------- intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "numpy": ("https://numpy.org/doc/stable", None), - "scipy": ("https://docs.scipy.org/doc/scipy", None), - "matplotlib": ("https://matplotlib.org/stable", None), - "sklearn": ("https://scikit-learn.org/stable", None), - "numba": ("https://numba.readthedocs.io/en/latest", None), - "joblib": ("https://joblib.readthedocs.io/en/latest", None), - "nibabel": ("https://nipy.org/nibabel", None), - "nilearn": ("http://nilearn.github.io/stable", None), + # More niche so didn't upstream to intersphinx_registry "nitime": ("https://nipy.org/nitime/", None), - "surfer": ("https://pysurfer.github.io/", None), "mne_bids": ("https://mne.tools/mne-bids/stable", None), "mne-connectivity": ("https://mne.tools/mne-connectivity/stable", None), "mne-gui-addons": ("https://mne.tools/mne-gui-addons", None), - "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), - "seaborn": ("https://seaborn.pydata.org/", None), - "statsmodels": ("https://www.statsmodels.org/dev", None), - "patsy": ("https://patsy.readthedocs.io/en/latest", None), - "pyvista": ("https://docs.pyvista.org/version/stable", None), - "imageio": ("https://imageio.readthedocs.io/en/latest", None), "picard": ("https://pierreablin.github.io/picard/", None), "eeglabio": ("https://eeglabio.readthedocs.io/en/latest", None), - "dipy": ("https://docs.dipy.org/stable", None), "pybv": ("https://pybv.readthedocs.io/en/latest/", None), - "pyqtgraph": ("https://pyqtgraph.readthedocs.io/en/latest/", None), } +intersphinx_mapping.update( + get_intersphinx_mapping( + only=set( + """ +imageio matplotlib numpy pandas python scipy statsmodels sklearn numba joblib nibabel +seaborn patsy pyvista dipy nilearn pyqtgraph +""".strip().split() + ), + ) +) # NumPyDoc configuration ----------------------------------------------------- diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 95ae75dc8d8..8691bffcfb8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1768,16 +1768,14 @@ def add_data( ): """Display data from a numpy array on the surface or volume. - This provides a similar interface to - :meth:`surfer.Brain.add_overlay`, but it displays + This provides a similar interface to PySurfer, but it displays it with a single colormap. It offers more flexibility over the colormap, and provides a way to display four-dimensional data (i.e., a timecourse) or five-dimensional data (i.e., a vector-valued timecourse). .. note:: ``fmin`` sets the low end of the colormap, and is separate - from thresh (this is a different convention from - :meth:`surfer.Brain.add_overlay`). + from thresh (this is a different convention from PySurfer). Parameters ---------- diff --git a/pyproject.toml b/pyproject.toml index 00bfa549de1..78615dc5568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -165,6 +165,7 @@ doc = [ "pyzmq!=24.0.0", "ipython!=8.7.0", "selenium", + "intersphinx_registry", ] dev = ["mne[test,doc]", "rcssmin"] From 6cad308dd83be5054a63ad0748ead08b641d6ac0 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 15 May 2024 12:02:07 -0400 Subject: [PATCH 035/153] FIX: Fixes for pip-pre (#12610) Co-authored-by: Daniel McCloy --- .git-blame-ignore-revs | 3 +++ examples/decoding/decoding_rsa_sgskip.py | 4 +++- examples/decoding/decoding_xdawn_eeg.py | 3 ++- mne/decoding/tests/test_base.py | 2 ++ mne/decoding/tests/test_search_light.py | 24 ++++++++++++++++++------ mne/stats/_adjacency.py | 2 +- mne/viz/backends/_pyvista.py | 17 +++++++---------- tools/install_pre_requirements.sh | 4 +++- 8 files changed, 39 insertions(+), 20 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c9248c01bb0..d4f5921e70c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -2,3 +2,6 @@ e81ec528a42ac687f3d961ed5cf8e25f236925b0 # black 12395f9d9cf6ea3c72b225b62e052dd0d17d9889 # YAML indentation d6d2f8c6a2ed4a0b27357da9ddf8e0cd14931b59 # isort e7dd1588013179013a50d3f6b8e8f9ae0a185783 # ruff format +e39995d9be6fc831c7a4a59f09b7a7c0a41ae315 # percent formatting +940ac9553ce42c15b4c16ecd013824ca3ea7244a # whitespace +1c5b39ff1d99bbcb2fc0e0071a989b3f3845ff30 # ruff UP028 diff --git a/examples/decoding/decoding_rsa_sgskip.py b/examples/decoding/decoding_rsa_sgskip.py index d25844dc1a5..bf6a5294624 100644 --- a/examples/decoding/decoding_rsa_sgskip.py +++ b/examples/decoding/decoding_rsa_sgskip.py @@ -37,6 +37,7 @@ from sklearn.manifold import MDS from sklearn.metrics import roc_auc_score from sklearn.model_selection import StratifiedKFold +from sklearn.multiclass import OneVsRestClassifier from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler @@ -122,7 +123,8 @@ # Classify using the average signal in the window 50ms to 300ms # to focus the classifier on the time interval with best SNR. clf = make_pipeline( - StandardScaler(), LogisticRegression(C=1, solver="liblinear", multi_class="auto") + StandardScaler(), + OneVsRestClassifier(LogisticRegression(C=1)), ) X = epochs.copy().crop(0.05, 0.3).get_data().mean(axis=2) y = epochs.events[:, 2] diff --git a/examples/decoding/decoding_xdawn_eeg.py b/examples/decoding/decoding_xdawn_eeg.py index 76817eb2850..ab274963f31 100644 --- a/examples/decoding/decoding_xdawn_eeg.py +++ b/examples/decoding/decoding_xdawn_eeg.py @@ -22,6 +22,7 @@ from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix from sklearn.model_selection import StratifiedKFold +from sklearn.multiclass import OneVsRestClassifier from sklearn.pipeline import make_pipeline from sklearn.preprocessing import MinMaxScaler @@ -73,7 +74,7 @@ Xdawn(n_components=n_filter), Vectorizer(), MinMaxScaler(), - LogisticRegression(penalty="l1", solver="liblinear", multi_class="auto"), + OneVsRestClassifier(LogisticRegression(penalty="l1", solver="liblinear")), ) # Get the labels diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index c26ca4f67b7..3ce1657e468 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -305,6 +305,8 @@ def test_get_coef_multiclass(n_features, n_targets): (3, 1, 2), ], ) +# TODO: Need to fix this properly in LinearModel +@pytest.mark.filterwarnings("ignore:'multi_class' was deprecated in.*:FutureWarning") def test_get_coef_multiclass_full(n_classes, n_channels, n_times): """Test a full example with pattern extraction.""" from sklearn.linear_model import LogisticRegression diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py index 21d4eda6d0f..a5fc53865cc 100644 --- a/mne/decoding/tests/test_search_light.py +++ b/mne/decoding/tests/test_search_light.py @@ -16,6 +16,8 @@ pytest.importorskip("sklearn") +NEW_MULTICLASS_SAMPLE_WEIGHT = check_version("sklearn", "1.4") + def make_data(): """Make data.""" @@ -36,13 +38,14 @@ def test_search_light(): pytest.skip("sklearn int_t / long long mismatch") from sklearn.linear_model import LogisticRegression, Ridge from sklearn.metrics import make_scorer, roc_auc_score + from sklearn.multiclass import OneVsRestClassifier from sklearn.pipeline import make_pipeline with _record_warnings(): # NumPy module import from sklearn.ensemble import BaggingClassifier from sklearn.base import is_classifier - logreg = LogisticRegression(solver="liblinear", multi_class="ovr", random_state=0) + logreg = OneVsRestClassifier(LogisticRegression(solver="liblinear", random_state=0)) X, y = make_data() n_epochs, _, n_time = X.shape @@ -158,9 +161,7 @@ class _LogRegTransformer(LogisticRegression): def transform(self, X): return super().predict_proba(X)[..., 1] - logreg_transformer = _LogRegTransformer( - random_state=0, multi_class="ovr", solver="liblinear" - ) + logreg_transformer = OneVsRestClassifier(_LogRegTransformer(random_state=0)) pipe = make_pipeline(SlidingEstimator(logreg_transformer), logreg) pipe.fit(X, y) pipe.predict(X) @@ -189,9 +190,17 @@ def test_generalization_light(): """Test GeneralizingEstimator.""" from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score + from sklearn.multiclass import OneVsRestClassifier from sklearn.pipeline import make_pipeline - logreg = LogisticRegression(solver="liblinear", multi_class="ovr", random_state=0) + if NEW_MULTICLASS_SAMPLE_WEIGHT: + logreg = OneVsRestClassifier(LogisticRegression(random_state=0)) + else: + logreg = LogisticRegression( + solver="liblinear", + random_state=0, + multi_class="ovr", + ) X, y = make_data() n_epochs, _, n_time = X.shape @@ -199,7 +208,10 @@ def test_generalization_light(): gl = GeneralizingEstimator(logreg) assert_equal(repr(gl)[:23], "") # transforms diff --git a/mne/stats/_adjacency.py b/mne/stats/_adjacency.py index 14e527a7428..551f9173a3b 100644 --- a/mne/stats/_adjacency.py +++ b/mne/stats/_adjacency.py @@ -55,7 +55,7 @@ def combine_adjacency(*structure): ... n_times, # regular lattice adjacency for times ... np.zeros((n_freqs, n_freqs)), # no adjacency between freq. bins ... chan_adj, # custom matrix, or use mne.channels.find_ch_adjacency - ... ) # doctest: +NORMALIZE_WHITESPACE + ... ) # doctest: +SKIP <5600x5600 sparse matrix of type '' with 27076 stored elements in COOrdinate format> """ diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 21526587707..da061b0a35b 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -19,6 +19,9 @@ from inspect import signature import numpy as np +import pyvista +from pyvista import Line, Plotter, PolyData, UnstructuredGrid, close_all +from pyvistaqt import BackgroundPlotter from ...fixes import _compare_version from ...transforms import _cart_to_sph, _sph_to_cart, apply_trans @@ -36,16 +39,10 @@ _init_mne_qtapp, ) -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - import pyvista - from pyvista import Line, Plotter, PolyData, UnstructuredGrid, close_all - from pyvistaqt import BackgroundPlotter - - try: - from pyvista.plotting.plotter import _ALL_PLOTTERS - except Exception: # PV < 0.40 - from pyvista.plotting.plotting import _ALL_PLOTTERS +try: + from pyvista.plotting.plotter import _ALL_PLOTTERS +except Exception: # PV < 0.40 + from pyvista.plotting.plotting import _ALL_PLOTTERS from vtkmodules.util.numpy_support import numpy_to_vtk from vtkmodules.vtkCommonCore import VTK_UNSIGNED_CHAR, vtkCommand, vtkLookupTable diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh index 47c7087ac8d..280c5f60867 100755 --- a/tools/install_pre_requirements.sh +++ b/tools/install_pre_requirements.sh @@ -40,7 +40,9 @@ echo "nilearn" python -m pip install $STD_ARGS git+https://github.com/nilearn/nilearn echo "VTK" -python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk +# No pre until PyVista fixes a bug +# python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk +python -m pip install $STD_ARGS vtk python -c "import vtk" echo "PyVista" From 44c69f5a5990ecb57f0f6590b37986a81f2bd325 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 12:16:56 -0400 Subject: [PATCH 036/153] [pre-commit.ci] pre-commit autoupdate (#12604) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a120e2b7326..4889f1b2e84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.2 + rev: v0.4.4 hooks: - id: ruff name: ruff lint mne From 4cffc343a3cb7c75101a11294c517d845ab423eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C3=A1k?= <66417283+michalrzak@users.noreply.github.com> Date: Wed, 15 May 2024 20:19:00 +0200 Subject: [PATCH 037/153] animate_topomap - CSD fix (#12605) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel/12605.bugfix.rst | 1 + mne/_fiff/pick.py | 2 +- mne/viz/tests/test_topomap.py | 39 ++++++++++++++++-------------- mne/viz/topomap.py | 17 ++----------- mne/viz/utils.py | 4 ++- 5 files changed, 28 insertions(+), 35 deletions(-) create mode 100644 doc/changes/devel/12605.bugfix.rst diff --git a/doc/changes/devel/12605.bugfix.rst b/doc/changes/devel/12605.bugfix.rst new file mode 100644 index 00000000000..0251eed410e --- /dev/null +++ b/doc/changes/devel/12605.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug where :meth:`mne.Evoked.animate_topomap` did not work with :func:`mne.preprocessing.compute_current_source_density` - modified data, by `Michal Žák`_. diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index 88d9e112b42..9024cf1c796 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -998,7 +998,7 @@ def _picks_by_type(info, meg_combined=False, ref_meg=False, exclude="bads"): exclude = _check_info_exclude(info, exclude) if meg_combined == "auto": meg_combined = _mag_grad_dependent(info) - picks_list = [] + picks_list = {ch_type: list() for ch_type in _DATA_CH_TYPES_SPLIT} for k in range(info["nchan"]): if info["chs"][k]["ch_name"] not in exclude: diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index eefe178516d..9fb13e8d56d 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -14,6 +14,7 @@ import matplotlib.pyplot as plt import numpy as np import pytest +from matplotlib.colors import PowerNorm, TwoSlopeNorm from matplotlib.patches import Circle from numpy.testing import assert_almost_equal, assert_array_equal, assert_equal @@ -43,7 +44,11 @@ ) from mne.datasets import testing from mne.io import RawArray, read_info, read_raw_fif -from mne.preprocessing import compute_bridged_electrodes +from mne.preprocessing import ( + ICA, + compute_bridged_electrodes, + compute_current_source_density, +) from mne.time_frequency.tfr import AverageTFRArray from mne.viz import plot_evoked_topomap, plot_projs_topomap, topomap from mne.viz.tests.test_raw import _proj_status @@ -179,7 +184,21 @@ def test_plot_topomap_animation(capsys): anim._func(1) # _animate has to be tested separately on 'Agg' backend. out, _ = capsys.readouterr() assert "extrapolation mode local to 0" in out - plt.close("all") + + +def test_plot_topomap_animation_csd(capsys): + """Test topomap plotting of CSD data.""" + # evoked + evoked = read_evokeds(evoked_fname, "Left Auditory", baseline=(None, 0)) + evoked_csd = compute_current_source_density(evoked) + + # Test animation + _, anim = evoked_csd.animate_topomap( + ch_type="csd", times=[0, 0.1], butterfly=False, time_unit="s", verbose="debug" + ) + anim._func(1) # _animate has to be tested separately on 'Agg' backend. + out, _ = capsys.readouterr() + assert "extrapolation mode head to 0" in out @pytest.mark.filterwarnings("ignore:.*No contour levels.*:UserWarning") @@ -190,7 +209,6 @@ def test_plot_topomap_animation_nirs(fnirs_evoked, capsys): out, _ = capsys.readouterr() assert "extrapolation mode head to 0" in out assert len(fig.axes) == 2 - plt.close("all") def test_plot_evoked_topomap_errors(evoked, monkeypatch): @@ -553,7 +571,6 @@ def patch(): orig_bads = evoked_grad.info["bads"] evoked_grad.plot_topomap(ch_type="grad", times=[0], time_unit="ms") assert_array_equal(evoked_grad.info["bads"], orig_bads) - plt.close("all") def test_plot_tfr_topomap(): @@ -685,8 +702,6 @@ def test_plot_topomap_neuromag122(): def test_plot_topomap_bads(): """Test plotting topomap with bad channels (gh-7213).""" - import matplotlib.pyplot as plt - data = np.random.RandomState(0).randn(3, 1000) raw = RawArray(data, create_info(3, 1000.0, "eeg")) ch_pos_dict = {name: pos for name, pos in zip(raw.ch_names, np.eye(3))} @@ -695,7 +710,6 @@ def test_plot_topomap_bads(): raw.info["bads"] = raw.ch_names[:count] raw.info._check_consistency() plot_topomap(data[:, 0], raw.info) - plt.close("all") def test_plot_topomap_channel_distance(): @@ -713,13 +727,10 @@ def test_plot_topomap_channel_distance(): evoked.set_montage(ten_five) evoked.plot_topomap(sphere=0.05, res=8) - plt.close("all") def test_plot_topomap_bads_grad(): """Test plotting topomap with bad gradiometer channels (gh-8802).""" - import matplotlib.pyplot as plt - data = np.random.RandomState(0).randn(203) info = read_info(evoked_fname) info["bads"] = ["MEG 2242"] @@ -727,21 +738,17 @@ def test_plot_topomap_bads_grad(): info = pick_info(info, picks) assert len(info["chs"]) == 203 plot_topomap(data, info, res=8) - plt.close("all") def test_plot_topomap_nirs_overlap(fnirs_epochs): """Test plotting nirs topomap with overlapping channels (gh-7414).""" fig = fnirs_epochs["A"].average(picks="hbo").plot_topomap() assert len(fig.axes) == 5 - plt.close("all") def test_plot_topomap_nirs_ica(fnirs_epochs): """Test plotting nirs ica topomap.""" pytest.importorskip("sklearn") - from mne.preprocessing import ICA - fnirs_epochs = fnirs_epochs.load_data().pick(picks="hbo") fnirs_epochs = fnirs_epochs.pick(picks=range(30)) @@ -754,7 +761,6 @@ def test_plot_topomap_nirs_ica(fnirs_epochs): ica = ICA().fit(fnirs_epochs) fig = ica.plot_components() assert len(fig[0].axes) == 20 - plt.close("all") def test_plot_cov_topomap(): @@ -763,13 +769,10 @@ def test_plot_cov_topomap(): info = read_info(evoked_fname) cov.plot_topomap(info) cov.plot_topomap(info, noise_cov=cov) - plt.close("all") def test_plot_topomap_cnorm(): """Test colormap normalization.""" - from matplotlib.colors import PowerNorm, TwoSlopeNorm - rng = np.random.default_rng(42) v = rng.uniform(low=-1, high=2.5, size=64) v[:3] = [-1, 0, 2.5] diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 20efbe79ab8..f92ae3c49f2 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -3249,21 +3249,8 @@ def _topomap_animation( from matplotlib import pyplot as plt if ch_type is None: - ch_type = _picks_by_type(evoked.info)[0][0] - if ch_type not in ( - "mag", - "grad", - "eeg", - "hbo", - "hbr", - "fnirs_od", - "fnirs_cw_amplitude", - ): - raise ValueError( - "Channel type not supported. Supported channel " - "types include 'mag', 'grad', 'eeg'. 'hbo', 'hbr', " - "'fnirs_cw_amplitude', and 'fnirs_od'." - ) + ch_type = _get_plot_ch_type(evoked, ch_type) + time_unit, _ = _check_time_unit(time_unit, evoked.times) if times is None: times = np.linspace(evoked.times[0], evoked.times[-1], 10) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index b524ec800b8..e86584b718d 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2821,5 +2821,7 @@ def _get_plot_ch_type(inst, ch_type, allow_ref_meg=False): ch_type = type_ break else: - raise RuntimeError("No plottable channel types found") + raise RuntimeError( + f"No plottable channel types found. Allowed types are: {allowed_types}" + ) return ch_type From 5b1c49e18a3a937c884cac529eebcc35a3e9ef04 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Wed, 15 May 2024 20:26:03 +0200 Subject: [PATCH 038/153] STY: Apply ruff/flake8-implicit-str-concat rule ISC001 (#12602) --- .../rename_towncrier/rename_towncrier.py | 20 ++++++++--------- examples/inverse/mixed_norm_inverse.py | 2 +- mne/_fiff/meas_info.py | 6 ++--- mne/_fiff/proj.py | 4 ++-- mne/_fiff/tests/test_constants.py | 2 +- mne/_fiff/write.py | 4 ++-- mne/bem.py | 2 +- mne/channels/channels.py | 10 ++++----- mne/channels/tests/test_montage.py | 4 ++-- mne/commands/mne_coreg.py | 2 +- mne/conftest.py | 6 ++--- mne/coreg.py | 2 +- mne/decoding/csp.py | 6 ++--- mne/decoding/transformer.py | 8 +++---- mne/epochs.py | 4 ++-- mne/evoked.py | 2 +- mne/export/_egimff.py | 2 +- mne/filter.py | 2 +- mne/forward/_field_interpolation.py | 2 +- mne/forward/_make_forward.py | 2 +- mne/gui/_coreg.py | 6 ++--- mne/inverse_sparse/mxne_inverse.py | 2 +- mne/inverse_sparse/mxne_optim.py | 4 +--- mne/io/base.py | 4 ++-- mne/io/besa/besa.py | 2 +- mne/io/brainvision/brainvision.py | 4 +--- mne/io/ctf/ctf.py | 2 +- mne/io/edf/edf.py | 2 +- mne/io/fieldtrip/utils.py | 2 +- mne/io/hitachi/hitachi.py | 4 +--- mne/io/kit/kit.py | 2 +- mne/io/persyst/persyst.py | 4 ++-- mne/label.py | 2 +- mne/minimum_norm/inverse.py | 2 +- mne/minimum_norm/resolution_matrix.py | 2 +- mne/morph_map.py | 2 +- mne/preprocessing/_csd.py | 6 ++--- mne/preprocessing/ica.py | 2 +- mne/preprocessing/ieeg/_volume.py | 4 ++-- mne/preprocessing/maxwell.py | 2 +- mne/preprocessing/nirs/nirs.py | 2 +- mne/preprocessing/realign.py | 2 +- mne/preprocessing/stim.py | 2 +- mne/report/report.py | 22 +++++++------------ mne/report/tests/test_report.py | 2 +- mne/simulation/metrics/metrics.py | 2 +- mne/source_estimate.py | 2 +- mne/source_space/_source_space.py | 2 +- mne/stats/cluster_level.py | 8 +++---- mne/surface.py | 2 +- mne/tests/test_annotations.py | 4 ++-- mne/tests/test_dipole.py | 6 ++--- mne/tests/test_docstring_parameters.py | 2 +- mne/tests/test_epochs.py | 4 +--- mne/tests/test_filter.py | 6 ++--- mne/time_frequency/tfr.py | 4 ++-- mne/transforms.py | 6 ++--- mne/utils/check.py | 8 +++---- mne/utils/config.py | 4 ++-- mne/utils/mixin.py | 2 +- mne/utils/numerics.py | 2 +- mne/utils/tests/test_logging.py | 2 +- mne/viz/_3d.py | 6 ++--- mne/viz/_brain/_brain.py | 6 ++--- mne/viz/_figure.py | 6 ++--- mne/viz/circle.py | 6 ++--- mne/viz/epochs.py | 2 +- mne/viz/evoked.py | 6 ++--- mne/viz/topomap.py | 2 +- mne/viz/utils.py | 4 +--- tutorials/intro/40_sensor_locations.py | 2 +- .../40_artifact_correction_ica.py | 4 +--- 72 files changed, 129 insertions(+), 163 deletions(-) diff --git a/.github/actions/rename_towncrier/rename_towncrier.py b/.github/actions/rename_towncrier/rename_towncrier.py index 68971d1c83f..e4efd27ef95 100755 --- a/.github/actions/rename_towncrier/rename_towncrier.py +++ b/.github/actions/rename_towncrier/rename_towncrier.py @@ -11,22 +11,22 @@ from github import Github from tomllib import loads -event_name = os.getenv('GITHUB_EVENT_NAME', 'pull_request') -if not event_name.startswith('pull_request'): - print(f'No-op for {event_name}') +event_name = os.getenv("GITHUB_EVENT_NAME", "pull_request") +if not event_name.startswith("pull_request"): + print(f"No-op for {event_name}") sys.exit(0) -if 'GITHUB_EVENT_PATH' in os.environ: - with open(os.environ['GITHUB_EVENT_PATH'], encoding='utf-8') as fin: +if "GITHUB_EVENT_PATH" in os.environ: + with open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8") as fin: event = json.load(fin) - pr_num = event['number'] - basereponame = event['pull_request']['base']['repo']['full_name'] + pr_num = event["number"] + basereponame = event["pull_request"]["base"]["repo"]["full_name"] real = True else: # local testing pr_num = 12318 # added some towncrier files basereponame = "mne-tools/mne-python" real = False -g = Github(os.environ.get('GITHUB_TOKEN')) +g = Github(os.environ.get("GITHUB_TOKEN")) baserepo = g.get_repo(basereponame) # Grab config from upstream's default branch @@ -45,9 +45,7 @@ assert directory.endswith("/"), directory file_re = re.compile(rf"^{directory}({type_pipe})\.rst$") -found_stubs = [ - f for f in modified_files if file_re.match(f) -] +found_stubs = [f for f in modified_files if file_re.match(f)] for stub in found_stubs: fro = stub to = file_re.sub(rf"{directory}{pr_num}.\1.rst", fro) diff --git a/examples/inverse/mixed_norm_inverse.py b/examples/inverse/mixed_norm_inverse.py index bc6b91bfeae..70764a53973 100644 --- a/examples/inverse/mixed_norm_inverse.py +++ b/examples/inverse/mixed_norm_inverse.py @@ -90,7 +90,7 @@ t = 0.083 tidx = evoked.time_as_index(t).item() for di, dip in enumerate(dipoles, 1): - print(f"Dipole #{di} GOF at {1000 * t:0.1f} ms: " f"{float(dip.gof[tidx]):0.1f}%") + print(f"Dipole #{di} GOF at {1000 * t:0.1f} ms: {float(dip.gof[tidx]):0.1f}%") # %% # Plot dipole activations diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 631c8149b1c..f3c1bfc5061 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -3205,9 +3205,7 @@ def create_info(ch_names, sfreq, ch_types="misc", verbose=None): _validate_type(ch_name, "str", "each entry in ch_names") _validate_type(ch_type, "str", "each entry in ch_types") if ch_type not in ch_types_dict: - raise KeyError( - f"kind must be one of {list(ch_types_dict)}, " f"not {ch_type}" - ) + raise KeyError(f"kind must be one of {list(ch_types_dict)}, not {ch_type}") this_ch_dict = ch_types_dict[ch_type] kind = this_ch_dict["kind"] # handle chpi, where kind is a *list* of FIFF constants: @@ -3352,7 +3350,7 @@ def _force_update_info(info_base, info_target): all_infos = np.hstack([info_base, info_target]) for ii in all_infos: if not isinstance(ii, Info): - raise ValueError("Inputs must be of type Info. " f"Found type {type(ii)}") + raise ValueError(f"Inputs must be of type Info. Found type {type(ii)}") for key, val in info_base.items(): if key in exclude_keys: continue diff --git a/mne/_fiff/proj.py b/mne/_fiff/proj.py index fd5887a4d20..6011f322cfc 100644 --- a/mne/_fiff/proj.py +++ b/mne/_fiff/proj.py @@ -491,7 +491,7 @@ def plot_projs_topomap( _projs.remove(_proj) if len(_projs) == 0: raise ValueError( - "Nothing to plot (no projectors for channel " f"type {ch_type})." + f"Nothing to plot (no projectors for channel type {ch_type})." ) # now we have non-empty _projs list with correct channel type(s) from ..viz.topomap import plot_projs_topomap @@ -1100,7 +1100,7 @@ def _has_eeg_average_ref_proj( missing = [name for name in want_names if name not in found_names] if missing: if found_names: # found some but not all: warn - warn(f"Incomplete {ch_type} projector, " f"missing channel(s) {missing}") + warn(f"Incomplete {ch_type} projector, missing channel(s) {missing}") return False return True diff --git a/mne/_fiff/tests/test_constants.py b/mne/_fiff/tests/test_constants.py index 55549b53974..703a32fd333 100644 --- a/mne/_fiff/tests/test_constants.py +++ b/mne/_fiff/tests/test_constants.py @@ -123,7 +123,7 @@ def test_constants(tmp_path): fname = "fiff.zip" dest = tmp_path / fname pooch.retrieve( - url="https://codeload.github.com/" f"{REPO}/fiff-constants/zip/{COMMIT}", + url=f"https://codeload.github.com/{REPO}/fiff-constants/zip/{COMMIT}", path=tmp_path, fname=fname, known_hash=None, diff --git a/mne/_fiff/write.py b/mne/_fiff/write.py index e68ffcff0b1..ea43d37562e 100644 --- a/mne/_fiff/write.py +++ b/mne/_fiff/write.py @@ -45,7 +45,7 @@ def _get_split_size(split_size): if isinstance(split_size, str): exp = dict(MB=20, GB=30).get(split_size[-2:], None) if exp is None: - raise ValueError("split_size has to end with either" '"MB" or "GB"') + raise ValueError('split_size has to end with either "MB" or "GB"') split_size = int(float(split_size[:-2]) * 2**exp) if split_size > 2147483648: @@ -77,7 +77,7 @@ def write_int(fid, kind, data): max_val = data.max() if data.size > 0 else 0 if max_val > INT32_MAX: raise TypeError( - f"Value {max_val} exceeds maximum allowed ({INT32_MAX}) for " f"tag {kind}" + f"Value {max_val} exceeds maximum allowed ({INT32_MAX}) for tag {kind}" ) data = data.astype(">i4").T _write(fid, data, kind, data_size, FIFF.FIFFT_INT, ">i4") diff --git a/mne/bem.py b/mne/bem.py index 9297cc773b2..351703d146b 100644 --- a/mne/bem.py +++ b/mne/bem.py @@ -1026,7 +1026,7 @@ def get_fitting_dig(info, dig_kinds="auto", exclude_frontal=True, verbose=None): _validate_type(info, "info") if info["dig"] is None: raise RuntimeError( - "Cannot fit headshape without digitization " ', info["dig"] is None' + 'Cannot fit headshape without digitization, info["dig"] is None' ) if isinstance(dig_kinds, str): if dig_kinds == "auto": diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 54ad772ba18..f9fbdf95477 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1902,7 +1902,7 @@ def combine_channels( # Instantiate channel info and data new_ch_names, new_ch_types, new_data = [], [], [] if not isinstance(keep_stim, bool): - raise TypeError('"keep_stim" must be of type bool, not ' f"{type(keep_stim)}.") + raise TypeError(f'"keep_stim" must be of type bool, not {type(keep_stim)}.') if keep_stim: stim_ch_idx = list(pick_types(inst.info, meg=False, stim=True)) if stim_ch_idx: @@ -1915,7 +1915,7 @@ def combine_channels( # Get indices of bad channels ch_idx_bad = [] if not isinstance(drop_bad, bool): - raise TypeError('"drop_bad" must be of type bool, not ' f"{type(drop_bad)}.") + raise TypeError(f'"drop_bad" must be of type bool, not {type(drop_bad)}.') if drop_bad and inst.info["bads"]: ch_idx_bad = pick_channels(ch_names, inst.info["bads"]) @@ -1937,7 +1937,7 @@ def combine_channels( this_picks = [idx for idx in this_picks if idx not in ch_idx_bad] if these_bads: logger.info( - "Dropped the following channels in group " f"{this_group}: {these_bads}" + f"Dropped the following channels in group {this_group}: {these_bads}" ) # Check if combining less than 2 channel if len(set(this_picks)) < 2: @@ -2130,9 +2130,7 @@ def read_vectorview_selection(name, fname=None, info=None, verbose=None): # get the name of the selection in the file pos = line.find(":") if pos < 0: - logger.info( - '":" delimiter not found in selections file, ' "skipping line" - ) + logger.info('":" delimiter not found in selections file, skipping line') continue sel_name_file = line[:pos] # search for substring match with name provided diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index e960a533eed..7f6af375ca9 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -1976,7 +1976,7 @@ def test_montage_add_fiducials(): # check that adding MNI fiducials fails because we're in MRI with pytest.raises( - RuntimeError, match="Montage should be in the " '"mni_tal" coordinate frame' + RuntimeError, match='Montage should be in the "mni_tal" coordinate frame' ): montage.add_mni_fiducials(subjects_dir=subjects_dir) @@ -1991,7 +1991,7 @@ def test_montage_add_fiducials(): # which is the FreeSurfer RAS montage = make_dig_montage(ch_pos=test_ch_pos, coord_frame="mni_tal") with pytest.raises( - RuntimeError, match="Montage should be in the " '"mri" coordinate frame' + RuntimeError, match='Montage should be in the "mri" coordinate frame' ): montage.add_estimated_fiducials(subject=subject, subjects_dir=subjects_dir) diff --git a/mne/commands/mne_coreg.py b/mne/commands/mne_coreg.py index 45c9e803697..c7c2b9287d8 100644 --- a/mne/commands/mne_coreg.py +++ b/mne/commands/mne_coreg.py @@ -73,7 +73,7 @@ def run(): type=str, default=None, dest="interaction", - help='Interaction style to use, can be "trackball" or ' '"terrain".', + help='Interaction style to use, can be "trackball" or "terrain".', ) _add_verbose_flag(parser) diff --git a/mne/conftest.py b/mne/conftest.py index acc4f792700..82d247e9ab2 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -514,7 +514,7 @@ def _check_pyqtgraph(request): qt_version, api = _check_qt_version(return_api=True) if (not qt_version) or _compare_version(qt_version, "<", "5.12"): pytest.skip( - f"Qt API {api} has version {qt_version} " f"but pyqtgraph needs >= 5.12!" + f"Qt API {api} has version {qt_version} but pyqtgraph needs >= 5.12!" ) try: import mne_qt_browser # noqa: F401 @@ -525,10 +525,10 @@ def _check_pyqtgraph(request): f_name = request.function.__name__ if lower_2_0 and m_name in pre_2_0_skip_modules: pytest.skip( - f'Test-Module "{m_name}" was skipped for' f" mne-qt-browser < 0.2.0" + f'Test-Module "{m_name}" was skipped for mne-qt-browser < 0.2.0' ) elif lower_2_0 and f_name in pre_2_0_skip_funcs: - pytest.skip(f'Test "{f_name}" was skipped for ' f"mne-qt-browser < 0.2.0") + pytest.skip(f'Test "{f_name}" was skipped for mne-qt-browser < 0.2.0') except Exception: pytest.skip("Requires mne_qt_browser") else: diff --git a/mne/coreg.py b/mne/coreg.py index 0b87023b50b..7de243c7874 100644 --- a/mne/coreg.py +++ b/mne/coreg.py @@ -1905,7 +1905,7 @@ def _orig_hsp_point_distance(self): def _log_dig_mri_distance(self, prefix): errs_nearest = self.compute_dig_mri_distances() logger.info( - f"{prefix} median distance: " f"{np.median(errs_nearest * 1000):6.2f} mm" + f"{prefix} median distance: {np.median(errs_nearest * 1000):6.2f} mm" ) @property diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index fd937193f21..88631f0bc81 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -139,13 +139,11 @@ def __init__( if transform_into == "average_power": if log is not None and not isinstance(log, bool): raise ValueError( - "log must be a boolean if transform_into == " '"average_power".' + 'log must be a boolean if transform_into == "average_power".' ) else: if log is not None: - raise ValueError( - "log must be a None if transform_into == " '"csp_space".' - ) + raise ValueError('log must be a None if transform_into == "csp_space".') self.log = log _validate_type(norm_trace, bool, "norm_trace") diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 90af1e22345..096a08ce38b 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -115,14 +115,14 @@ def __init__(self, info=None, scalings=None, with_mean=True, with_std=True): if not (scalings is None or isinstance(scalings, (dict, str))): raise ValueError( - "scalings type should be dict, str, or None, " f"got {type(scalings)}" + f"scalings type should be dict, str, or None, got {type(scalings)}" ) if isinstance(scalings, str): _check_option("scalings", scalings, ["mean", "median"]) if scalings is None or isinstance(scalings, dict): if info is None: raise ValueError( - 'Need to specify "info" if scalings is' f"{type(scalings)}" + f'Need to specify "info" if scalings is {type(scalings)}' ) self._scaler = _ConstantScaler(info, scalings, self.with_std) elif scalings == "mean": @@ -339,7 +339,7 @@ def inverse_transform(self, X): X = np.asarray(X) if X.ndim not in (2, 3): raise ValueError( - "X should be of 2 or 3 dimensions but has shape " f"{X.shape}" + f"X should be of 2 or 3 dimensions but has shape {X.shape}" ) return X.reshape(X.shape[:-1] + self.features_shape_) @@ -642,7 +642,7 @@ def __init__(self, estimator, average=False): if not isinstance(average, bool): raise ValueError( - "average parameter must be of bool type, got " f"{type(bool)} instead" + f"average parameter must be of bool type, got {type(bool)} instead" ) self.estimator = estimator diff --git a/mne/epochs.py b/mne/epochs.py index c893171612c..43f8baf70f3 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -623,7 +623,7 @@ def __init__( reject_tmin = self.tmin elif reject_tmin < tmin: raise ValueError( - f"reject_tmin needs to be None or >= tmin " f"(got {reject_tmin})" + f"reject_tmin needs to be None or >= tmin (got {reject_tmin})" ) if reject_tmax is not None: @@ -632,7 +632,7 @@ def __init__( reject_tmax = self.tmax elif reject_tmax > tmax: raise ValueError( - f"reject_tmax needs to be None or <= tmax " f"(got {reject_tmax})" + f"reject_tmax needs to be None or <= tmax (got {reject_tmax})" ) if (reject_tmin is not None) and (reject_tmax is not None): diff --git a/mne/evoked.py b/mne/evoked.py index f01eb6b4dc5..cc4f1e738c5 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -1769,7 +1769,7 @@ def _read_evoked(fname, condition=None, kind="average", allow_maxshield=False): # find string-based entry if isinstance(condition, str): if kind not in _aspect_dict.keys(): - raise ValueError('kind must be "average" or ' '"standard_error"') + raise ValueError('kind must be "average" or "standard_error"') comments, aspect_kinds, t = _get_entries(fid, evoked_node, allow_maxshield) goods = np.isin(comments, [condition]) & np.isin( diff --git a/mne/export/_egimff.py b/mne/export/_egimff.py index 70462a96841..427efe6b059 100644 --- a/mne/export/_egimff.py +++ b/mne/export/_egimff.py @@ -54,7 +54,7 @@ def export_evokeds_mff(fname, evoked, history=None, *, overwrite=False, verbose= info = evoked[0].info if np.round(info["sfreq"]) != info["sfreq"]: raise ValueError( - "Sampling frequency must be a whole number. " f'sfreq: {info["sfreq"]}' + f'Sampling frequency must be a whole number. sfreq: {info["sfreq"]}' ) sampling_rate = int(info["sfreq"]) diff --git a/mne/filter.py b/mne/filter.py index d872379c2b2..dc25776f980 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -1668,7 +1668,7 @@ def _mt_spectrum_proc( kind = "Detected" if line_freqs is None else "Removed" found_freqs = ( "\n".join( - f" {freq:6.2f} : " f"{counts[freq]:4d} window{_pl(counts[freq])}" + f" {freq:6.2f} : {counts[freq]:4d} window{_pl(counts[freq])}" for freq in sorted(counts) ) or " None" diff --git a/mne/forward/_field_interpolation.py b/mne/forward/_field_interpolation.py index 00ea5bc9e50..1b768b9a0cb 100644 --- a/mne/forward/_field_interpolation.py +++ b/mne/forward/_field_interpolation.py @@ -351,7 +351,7 @@ def _make_surface_mapping( raise KeyError('surf must have both "rr" and "nn"') if "coord_frame" not in surf: raise KeyError( - "The surface coordinate frame must be specified " 'in surf["coord_frame"]' + 'The surface coordinate frame must be specified in surf["coord_frame"]' ) _check_option("mode", mode, ["accurate", "fast"]) diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index 58c4c21ea89..dacd33785aa 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -127,7 +127,7 @@ def _read_coil_def_file(fname, use_registry=True): vals = np.fromstring(line, sep=" ") if len(vals) != 7: raise RuntimeError( - f"Could not interpret line {p + 1} as 7 points:\n" f"{line}" + f"Could not interpret line {p + 1} as 7 points:\n{line}" ) # Read and verify data for each integration point w.append(vals[0]) diff --git a/mne/gui/_coreg.py b/mne/gui/_coreg.py index 226bbbaa350..e16b8bfa4ba 100644 --- a/mne/gui/_coreg.py +++ b/mne/gui/_coreg.py @@ -689,7 +689,7 @@ def _lock_fids_changed(self, change=None): self._forward_widget_command(locked_widgets, "set_enabled", False) self._forward_widget_command(fits_widgets, "set_enabled", False) self._display_message( - "Placing MRI fiducials - " f"{self._current_fiducial.upper()}" + f"Placing MRI fiducials - {self._current_fiducial.upper()}" ) self._set_sensors_visibility(self._lock_fids) @@ -702,7 +702,7 @@ def _current_fiducial_changed(self, change=None): self._follow_fiducial_view() if not self._lock_fids: self._display_message( - "Placing MRI fiducials - " f"{self._current_fiducial.upper()}" + f"Placing MRI fiducials - {self._current_fiducial.upper()}" ) @observe("_info_file") @@ -953,7 +953,7 @@ def _omit_hsp(self): self._update_plot("hsp") self._update_distance_estimation() self._display_message( - f"{n_omitted} head shape points omitted, " f"{n_remaining} remaining." + f"{n_omitted} head shape points omitted, {n_remaining} remaining." ) def _reset_omit_hsp_filter(self): diff --git a/mne/inverse_sparse/mxne_inverse.py b/mne/inverse_sparse/mxne_inverse.py index cb49deaa213..d3a8be46907 100644 --- a/mne/inverse_sparse/mxne_inverse.py +++ b/mne/inverse_sparse/mxne_inverse.py @@ -460,7 +460,7 @@ def mixed_norm( _check_option("alpha", alpha, ("sure",)) elif not 0.0 <= alpha < 100: raise ValueError( - 'If not equal to "sure" alpha must be in [0, 100). ' f"Got alpha = {alpha}" + f'If not equal to "sure" alpha must be in [0, 100). Got alpha = {alpha}' ) if n_mxne_iter < 1: raise ValueError( diff --git a/mne/inverse_sparse/mxne_optim.py b/mne/inverse_sparse/mxne_optim.py index 5c77b611b51..f9142c89eab 100644 --- a/mne/inverse_sparse/mxne_optim.py +++ b/mne/inverse_sparse/mxne_optim.py @@ -755,9 +755,7 @@ def __call__(self, x): # noqa: D105 def norm(self, z, ord=2): # noqa: A002 """Squared L2 norm if ord == 2 and L1 norm if order == 1.""" if ord not in (1, 2): - raise ValueError( - "Only supported norm order are 1 and 2. " f"Got ord = {ord}" - ) + raise ValueError(f"Only supported norm order are 1 and 2. Got ord = {ord}") stft_norm = stft_norm1 if ord == 1 else stft_norm2 norm = 0.0 if len(self.n_coefs) > 1: diff --git a/mne/io/base.py b/mne/io/base.py index 625eeb54684..f10228a70cf 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -1721,7 +1721,7 @@ def save( data_test = self[0, 0][0] if fmt == "short" and np.iscomplexobj(data_test): raise ValueError( - 'Complex data must be saved as "single" or ' '"double", not "short"' + 'Complex data must be saved as "single" or "double", not "short"' ) # check for file existence and expand `~` if present @@ -3007,7 +3007,7 @@ def _write_raw_buffer(fid, buf, cals, fmt): write_function = write_complex128 else: raise ValueError( - 'only "single" and "double" supported for ' "writing complex data" + 'only "single" and "double" supported for writing complex data' ) buf = buf / np.ravel(cals)[:, None] diff --git a/mne/io/besa/besa.py b/mne/io/besa/besa.py index 47058688274..907129665c4 100644 --- a/mne/io/besa/besa.py +++ b/mne/io/besa/besa.py @@ -109,7 +109,7 @@ def _read_evoked_besa_avr(fname, verbose): fields["DI"] = float(fields["DI"]) else: raise RuntimeError( - 'No "DI" field present. Could not determine ' "sampling frequency." + 'No "DI" field present. Could not determine sampling frequency.' ) if "TSB" in fields: fields["TSB"] = float(fields["TSB"]) diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 3fdc0b49715..3a95f424f3e 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -350,9 +350,7 @@ def _check_bv_version(header, kind): ) # optional space, optional Core or V-Amp, optional Exchange, # Version/Header, optional comma, 1/2 - _data_re = ( - r"Brain ?Vision( Core| V-Amp)? Data( Exchange)? " r"%s File,? Version %s\.0" - ) + _data_re = r"Brain ?Vision( Core| V-Amp)? Data( Exchange)? %s File,? Version %s\.0" assert kind in ("header", "marker") diff --git a/mne/io/ctf/ctf.py b/mne/io/ctf/ctf.py index ed403025b03..f503f287a7c 100644 --- a/mne/io/ctf/ctf.py +++ b/mne/io/ctf/ctf.py @@ -113,7 +113,7 @@ def __init__( ) if not directory.endswith(".ds"): raise TypeError( - 'directory must be a directory ending with ".ds", ' f"got {directory}" + f'directory must be a directory ending with ".ds", got {directory}' ) _check_option("system_clock", system_clock, ["ignore", "truncate"]) logger.info(f"ds directory : {directory}") diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 023687ee74b..5c41a56f3e4 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -1547,7 +1547,7 @@ def _find_exclude_idx(ch_names, exclude, include=None): if include: # find other than include channels if exclude: raise ValueError( - "'exclude' must be empty if 'include' is assigned. " f"Got {exclude}." + f"'exclude' must be empty if 'include' is assigned. Got {exclude}." ) if isinstance(include, str): # regex for channel names indices_include = [] diff --git a/mne/io/fieldtrip/utils.py b/mne/io/fieldtrip/utils.py index 594451bfab2..cf8a9fc311d 100644 --- a/mne/io/fieldtrip/utils.py +++ b/mne/io/fieldtrip/utils.py @@ -98,7 +98,7 @@ def _remove_missing_channels_from_trial(trial, missing_chan_idx): trial = np.delete(trial, missing_chan_idx, axis=0) else: raise ValueError( - '"trial" field of the FieldTrip structure ' "has an unknown format." + '"trial" field of the FieldTrip structure has an unknown format.' ) return trial diff --git a/mne/io/hitachi/hitachi.py b/mne/io/hitachi/hitachi.py index d0b1ac5a187..ed34cfbc986 100644 --- a/mne/io/hitachi/hitachi.py +++ b/mne/io/hitachi/hitachi.py @@ -284,9 +284,7 @@ def _get_hitachi_info(fname, S_offset, D_offset, ignore_names): # nominal wavelength sidx, didx = pairs[ii // 2] nom_freq = fnirs_wavelengths[np.argmin(np.abs(acc_freq - fnirs_wavelengths))] - ch_names[idx] = ( - f"S{S_offset + sidx + 1}_" f"D{D_offset + didx + 1} " f"{nom_freq}" - ) + ch_names[idx] = f"S{S_offset + sidx + 1}_D{D_offset + didx + 1} {nom_freq}" offsets = np.array(pairs, int).max(axis=0) + 1 # figure out bounds diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index 5c795f55048..9a0b301087f 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -276,7 +276,7 @@ def _set_stimchannels(inst, info, stim, stim_code): stim = picks else: raise ValueError( - "stim needs to be list of int, '>' or " f"'<', not {str(stim)!r}" + f"stim needs to be list of int, '>' or '<', not {str(stim)!r}" ) else: stim = np.asarray(stim, int) diff --git a/mne/io/persyst/persyst.py b/mne/io/persyst/persyst.py index 7df91d5b503..d0f05893dab 100644 --- a/mne/io/persyst/persyst.py +++ b/mne/io/persyst/persyst.py @@ -77,7 +77,7 @@ def __init__(self, fname, preload=False, verbose=None): curr_path, lay_fname = op.dirname(fname), op.basename(fname) if not op.exists(fname): raise FileNotFoundError( - f"The path you specified, " f'"{lay_fname}",does not exist.' + f'The path you specified, "{lay_fname}",does not exist.' ) # sections and subsections currently unused @@ -222,7 +222,7 @@ def __init__(self, fname, preload=False, verbose=None): n_samples = f.tell() n_samples = n_samples // (dtype.itemsize * n_chs) - logger.debug(f"Loaded {n_samples} samples " f"for {n_chs} channels.") + logger.debug(f"Loaded {n_samples} samples for {n_chs} channels.") raw_extras = {"dtype": dtype, "n_chs": n_chs, "n_samples": n_samples} # create Raw object diff --git a/mne/label.py b/mne/label.py index ef3c08ee4c7..9cf7be95090 100644 --- a/mne/label.py +++ b/mne/label.py @@ -2693,7 +2693,7 @@ def write_labels_to_annot( for fname in annot_fname: if op.exists(fname): raise ValueError( - f'File {fname} exists. Use "overwrite=True" to ' "overwrite it" + f'File {fname} exists. Use "overwrite=True" to overwrite it' ) # prepare container for data to save: diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py index 63043757b9b..0e6c3deacb0 100644 --- a/mne/minimum_norm/inverse.py +++ b/mne/minimum_norm/inverse.py @@ -1764,7 +1764,7 @@ def _prepare_forward( exp = float(exp) if exp < 0: raise ValueError( - "depth exponent should be greater than or " f"equal to 0, got {exp}" + f"depth exponent should be greater than or equal to 0, got {exp}" ) exp = exp or None # alias 0. -> None diff --git a/mne/minimum_norm/resolution_matrix.py b/mne/minimum_norm/resolution_matrix.py index dccb08b3e04..28d453f20e4 100644 --- a/mne/minimum_norm/resolution_matrix.py +++ b/mne/minimum_norm/resolution_matrix.py @@ -195,7 +195,7 @@ def _check_get_psf_ctf_params(mode, n_comp, return_pca_vars): msg = f"n_comp must be 1 for mode={mode}." raise ValueError(msg) if mode != "pca" and return_pca_vars: - msg = "SVD variances can only be returned if mode=" "pca" "." + msg = "SVD variances can only be returned if mode=pca." raise ValueError(msg) diff --git a/mne/morph_map.py b/mne/morph_map.py index 618cacd3272..a0b50d6b395 100644 --- a/mne/morph_map.py +++ b/mne/morph_map.py @@ -102,7 +102,7 @@ def read_morph_map( return _read_morph_map(fname, subject_from, subject_to) # if file does not exist, make it logger.info( - f'Morph map "{fname}" does not exist, creating it and saving it to ' "disk" + f'Morph map "{fname}" does not exist, creating it and saving it to disk' ) logger.info(log_msg % (subject_from, subject_to)) mmap_1 = _make_morph_map(subject_from, subject_to, subjects_dir, xhemi) diff --git a/mne/preprocessing/_csd.py b/mne/preprocessing/_csd.py index 6edb254ea49..632a3421cf2 100644 --- a/mne/preprocessing/_csd.py +++ b/mne/preprocessing/_csd.py @@ -136,7 +136,7 @@ def compute_current_source_density( n_legendre_terms = _ensure_int(n_legendre_terms, "n_legendre_terms") if n_legendre_terms < 1: raise ValueError( - "n_legendre_terms must be greater than 0, " f"got {n_legendre_terms}" + f"n_legendre_terms must be greater than 0, got {n_legendre_terms}" ) if isinstance(sphere, str) and sphere == "auto": @@ -148,14 +148,14 @@ def compute_current_source_density( x, y, z, radius = sphere except Exception: raise ValueError( - f'sphere must be "auto" or array-like with shape (4,), ' f"got {sphere}" + f'sphere must be "auto" or array-like with shape (4,), got {sphere}' ) _validate_type(x, "numeric", "x") _validate_type(y, "numeric", "y") _validate_type(z, "numeric", "z") _validate_type(radius, "numeric", "radius") if radius <= 0: - raise ValueError("sphere radius must be greater than 0, " f"got {radius}") + raise ValueError("sphere radius must be greater than 0, got {radius}") pos = np.array([inst.info["chs"][pick]["loc"][:3] for pick in picks]) if not np.isfinite(pos).all() or np.isclose(pos, 0.0).all(1).any(): diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 6cdd95244ae..aff77c83c96 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -560,7 +560,7 @@ def __repr__(self): """ICA fit information.""" infos = self._get_infos_for_repr() - s = f'{infos.fit_on or "no"} decomposition, ' f"method: {infos.fit_method}" + s = f'{infos.fit_on or "no"} decomposition, method: {infos.fit_method}' if infos.fit_on is not None: s += ( diff --git a/mne/preprocessing/ieeg/_volume.py b/mne/preprocessing/ieeg/_volume.py index 4db6f4c29e5..26ed8632400 100644 --- a/mne/preprocessing/ieeg/_volume.py +++ b/mne/preprocessing/ieeg/_volume.py @@ -62,7 +62,7 @@ def warp_montage(montage, moving, static, reg_affine, sdr_morph, verbose=None): ] ) raise RuntimeError( - "Coordinate frame not supported, expected " f'"mri", got {bad_coord_frames}' + f'Coordinate frame not supported, expected "mri", got {bad_coord_frames}' ) ch_names = list(ch_dict["ch_pos"].keys()) ch_coords = np.array([ch_dict["ch_pos"][name] for name in ch_names]) @@ -192,7 +192,7 @@ def make_montage_volume( ] ) raise RuntimeError( - "Coordinate frame not supported, expected " f'"mri", got {bad_coord_frames}' + f'Coordinate frame not supported, expected "mri", got {bad_coord_frames}' ) ch_names = list(ch_dict["ch_pos"].keys()) diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 1a925dba528..d5cf8a58b6e 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -2591,7 +2591,7 @@ def find_bad_channels_maxwell( logger.info(msg) else: logger.info( - f"Applying low-pass filter with {h_freq} Hz cutoff " f"frequency ..." + f"Applying low-pass filter with {h_freq} Hz cutoff frequency ..." ) raw = raw.copy().load_data().filter(l_freq=None, h_freq=h_freq) diff --git a/mne/preprocessing/nirs/nirs.py b/mne/preprocessing/nirs/nirs.py index b6a69aac312..5a0e4b72199 100644 --- a/mne/preprocessing/nirs/nirs.py +++ b/mne/preprocessing/nirs/nirs.py @@ -127,7 +127,7 @@ def _check_channels_ordered(info, pair_vals, *, throw_errors=True, check_bads=Tr pair_vals = np.array(pair_vals) if pair_vals.shape != (2,): raise ValueError( - f"Exactly two {error_word} must exist in info, got " f"{list(pair_vals)}" + f"Exactly two {error_word} must exist in info, got {list(pair_vals)}" ) # In principle we do not need to require that these be sorted -- # all we need to do is change our sorted() below to make use of a diff --git a/mne/preprocessing/realign.py b/mne/preprocessing/realign.py index eee8947b0d2..0462c4dcef5 100644 --- a/mne/preprocessing/realign.py +++ b/mne/preprocessing/realign.py @@ -72,7 +72,7 @@ def realign_raw(raw, other, t_raw, t_other, verbose=None): converted = poly.convert(domain=(-1, 1)) [zero_ord, first_ord] = converted.coef logger.info( - f"Zero order coefficient: {zero_ord} \n" f"First order coefficient: {first_ord}" + f"Zero order coefficient: {zero_ord} \nFirst order coefficient: {first_ord}" ) r, p = pearsonr(t_other, t_raw) msg = f"Linear correlation computed as R={r:0.3f} and p={p:0.2e}" diff --git a/mne/preprocessing/stim.py b/mne/preprocessing/stim.py index 9b9a6a2db78..e19b781473f 100644 --- a/mne/preprocessing/stim.py +++ b/mne/preprocessing/stim.py @@ -82,7 +82,7 @@ def fix_stim_artifact( s_end = int(np.ceil(inst.info["sfreq"] * tmax)) if (mode == "window") and (s_end - s_start) < 4: raise ValueError( - "Time range is too short. Use a larger interval " 'or set mode to "linear".' + 'Time range is too short. Use a larger interval or set mode to "linear".' ) window = None if mode == "window": diff --git a/mne/report/report.py b/mne/report/report.py index 1786bb38078..ae66591481a 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -2570,9 +2570,7 @@ def _init_render(self, verbose=None): f"" ) elif inc_fname.endswith(".css"): - include.append( - f'" - ) + include.append(f'') self.include = "".join(include) def _iterate_files( @@ -2837,7 +2835,7 @@ def parse_folder( # render plots in parallel; check that n_jobs <= # of files logger.info( - f"Iterating over {len(fnames)} potential files " f"(this may take some " + f"Iterating over {len(fnames)} potential files (this may take some " ) parallel, p_fun, n_jobs = parallel_func( self._iterate_files, n_jobs, max_jobs=len(fnames) @@ -2947,7 +2945,7 @@ def save( if fname is None: if self.data_path is None: self.data_path = os.getcwd() - warn(f"`data_path` not provided. Using {self.data_path} " f"instead") + warn(f"`data_path` not provided. Using {self.data_path} instead") fname = op.join(self.data_path, "report.html") fname = str(_check_fname(fname, overwrite=overwrite, name=fname)) @@ -2957,9 +2955,7 @@ def save( self._sort(order=CONTENT_ORDER) if not overwrite and op.isfile(fname): - msg = ( - f"Report already exists at location {fname}. " f"Overwrite it (y/[n])? " - ) + msg = f"Report already exists at location {fname}. Overwrite it (y/[n])? " answer = _safe_input(msg, alt="pass overwrite=True") if answer.lower() == "y": overwrite = True @@ -3816,7 +3812,7 @@ def _add_epochs_psd(self, *, epochs, psd, image_format, tags, section, replace): _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) duration = round(epoch_duration * len(epochs_for_psd), 1) caption = ( - f"PSD calculated from {len(epochs_for_psd)} epochs " f"({duration:.1f} s)." + f"PSD calculated from {len(epochs_for_psd)} epochs ({duration:.1f} s)." ) self._add_figure( fig=fig, @@ -3927,7 +3923,7 @@ def _add_epochs( assert "eeg" in ch_type title_start = "ERP image" - title = f"{title_start} " f'({_handle_default("titles")[ch_type]})' + title = f'{title_start} ({_handle_default("titles")[ch_type]})' self._add_figure( fig=fig, @@ -4398,9 +4394,7 @@ def _df_bootstrap_table(*, df, data_id): continue elif "' - ) + htmls[idx] = f'{html}\n' continue col_headers = re.findall(pattern=header_pattern, string=html) @@ -4410,7 +4404,7 @@ def _df_bootstrap_table(*, df, data_id): col_header = col_headers[0] htmls[idx] = html.replace( "", - f'', + f'', ) html = "\n".join(htmls) diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index eaf7025e9db..30f4d14d814 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -285,7 +285,7 @@ def test_add_custom_js(tmp_path): report = Report() report.add_figure(fig=fig, title="Test section") - custom_js = "function hello() {\n" ' alert("Hello, report!");\n' "}" + custom_js = 'function hello() {\n alert("Hello, report!");\n}' report.add_custom_js(js=custom_js) assert custom_js in report.include diff --git a/mne/simulation/metrics/metrics.py b/mne/simulation/metrics/metrics.py index f8dddd055a8..37b969d83d1 100644 --- a/mne/simulation/metrics/metrics.py +++ b/mne/simulation/metrics/metrics.py @@ -179,7 +179,7 @@ def _check_threshold(threshold): if isinstance(threshold, str): if not threshold.endswith("%"): raise ValueError( - "Threshold if a string must end with " f'"%". Got {threshold}.' + f'Threshold if a string must end with "%". Got {threshold}.' ) threshold = float(threshold[:-1]) / 100.0 threshold = float(threshold) diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 4888441bac8..e6d1698be50 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -3950,7 +3950,7 @@ def stc_near_sensors( frames = set(ch["coord_frame"] for ch in evoked.info["chs"]) if not frames == {FIFF.FIFFV_COORD_HEAD}: raise RuntimeError( - "Channels must be in the head coordinate frame, " f"got {sorted(frames)}" + f"Channels must be in the head coordinate frame, got {sorted(frames)}" ) # get channel positions that will be used to pinpoint where diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index 87ec81a5ec7..0c7777b2862 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -1808,7 +1808,7 @@ def setup_volume_source_space( elif surface is not None: if isinstance(surface, dict): if not all(key in surface for key in ["rr", "tris"]): - raise KeyError('surface, if dict, must have entries "rr" ' 'and "tris"') + raise KeyError('surface, if dict, must have entries "rr" and "tris"') # let's make sure we have geom info complete_surface_info(surface, copy=False, verbose=False) surf_extra = "dict()" diff --git a/mne/stats/cluster_level.py b/mne/stats/cluster_level.py index 835c0d85427..7add61f6ae1 100644 --- a/mne/stats/cluster_level.py +++ b/mne/stats/cluster_level.py @@ -393,9 +393,7 @@ def _find_clusters( "threshold-free cluster enhancement" ) if not all(key in threshold for key in ["start", "step"]): - raise KeyError( - "threshold, if dict, must have at least " '"start" and "step"' - ) + raise KeyError('threshold, if dict, must have at least "start" and "step"') tfce = True use_x = x[np.isfinite(x)] if use_x.size == 0: @@ -404,9 +402,9 @@ def _find_clusters( ) if tail == -1: if threshold["start"] > 0: - raise ValueError('threshold["start"] must be <= 0 for ' "tail == -1") + raise ValueError('threshold["start"] must be <= 0 for tail == -1') if threshold["step"] >= 0: - raise ValueError('threshold["step"] must be < 0 for ' "tail == -1") + raise ValueError('threshold["step"] must be < 0 for tail == -1') stop = np.min(use_x) elif tail == 1: stop = np.max(use_x) diff --git a/mne/surface.py b/mne/surface.py index 24279e58f2c..61abb3511df 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -291,7 +291,7 @@ def _scale_helmet_to_sensors(system, surf, info): logger.info(f" 1. Affine: {rot:0.1f}°, {tr:0.1f} mm, {sc:0.2f}× scale") deltas = interp._last_deltas * 1000 mu, mx = np.mean(deltas), np.max(deltas) - logger.info(f" 2. Nonlinear displacement: " f"mean={mu:0.1f}, max={mx:0.1f} mm") + logger.info(f" 2. Nonlinear displacement: mean={mu:0.1f}, max={mx:0.1f} mm") surf["rr"] = new_rr complete_surface_info(surf, copy=False, verbose=False) return surf diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 3a95f4c75f5..2bcf50767d0 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -1035,7 +1035,7 @@ def test_io_annotation(dummy_annotation_file, tmp_path, fmt, ch_names): def test_broken_csv(tmp_path): """Test broken .csv that does not use timestamps.""" pytest.importorskip("pandas") - content = "onset,duration,description\n" "1.,1.0,AA\n" "3.,2.425,BB" + content = "onset,duration,description\n1.,1.0,AA\n3.,2.425,BB" fname = tmp_path / "annotations_broken.csv" with open(fname, "w") as f: f.write(content) @@ -1132,7 +1132,7 @@ def test_read_annotation_txt_header(tmp_path): def test_read_annotation_txt_one_segment(tmp_path): """Test empty TXT input/output.""" - content = "# MNE-Annotations\n" "# onset, duration, description\n" "3.14, 42, AA" + content = "# MNE-Annotations\n# onset, duration, description\n3.14, 42, AA" fname = tmp_path / "one-annotations.txt" with open(fname, "w") as f: f.write(content) diff --git a/mne/tests/test_dipole.py b/mne/tests/test_dipole.py index 8b4f398b2b0..30300572fa5 100644 --- a/mne/tests/test_dipole.py +++ b/mne/tests/test_dipole.py @@ -215,9 +215,9 @@ def test_dipole_fitting(tmp_path): # Sanity check: do our residuals have less power than orig data? data_rms = np.sqrt(np.sum(evoked.data**2, axis=0)) resi_rms = np.sqrt(np.sum(residual.data**2, axis=0)) - assert (data_rms > resi_rms * 0.95).all(), ( - f"{(data_rms / resi_rms).min()} " f"(factor: {0.95})" - ) + assert ( + data_rms > resi_rms * 0.95 + ).all(), f"{(data_rms / resi_rms).min()} (factor: {0.95})" # Compare to original points transform_surface_to(fwd["src"][0], "head", fwd["mri_head_t"]) diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index d5c4e6366f6..9d49e0c4e76 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -158,7 +158,7 @@ def check_parameters_match(func, *, cls=None, where): verbose_default = sig.parameters["verbose"].default if verbose_default is not None: incorrect += [ - f"{name} : verbose default is not None, " f"got: {verbose_default}" + f"{name} : verbose default is not None, got: {verbose_default}" ] return incorrect diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 8e5e1f488f3..ff5aca7530e 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -3449,9 +3449,7 @@ def test_drop_epochs_mult(preload): for di, (d1, d2) in enumerate(zip(epochs1.drop_log, epochs2.drop_log)): assert isinstance(d1, tuple) assert isinstance(d2, tuple) - msg = ( - f"\nepochs1.drop_log[{di}] = {d1}, " f"\nepochs2.drop_log[{di}] = {d2}" - ) + msg = f"\nepochs1.drop_log[{di}] = {d1}, \nepochs2.drop_log[{di}] = {d2}" if "IGNORED" in d1: assert "IGNORED" in d2, msg if "IGNORED" not in d1 and d1 != (): diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py index 00dce484a08..b68e40ba097 100644 --- a/mne/tests/test_filter.py +++ b/mne/tests/test_filter.py @@ -88,9 +88,9 @@ def test_estimate_ringing(): (0.0001, (30000, 60000)), ): # 37993 n_ring = estimate_ringing_samples(butter(3, thresh, output=kind)) - assert lims[0] <= n_ring <= lims[1], ( - f"{kind} {thresh}: {lims[0]} " f"<= {n_ring} <= {lims[1]}" - ) + assert ( + lims[0] <= n_ring <= lims[1] + ), f"{kind} {thresh}: {lims[0]} <= {n_ring} <= {lims[1]}" with pytest.warns(RuntimeWarning, match="properly estimate"): assert estimate_ringing_samples(butter(4, 0.00001)) == 100000 diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 4a36c78ad0f..f8c95ad7c04 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -648,7 +648,7 @@ def _check_tfr_param( decim = slice(None, None, decim) if not isinstance(decim, slice): raise ValueError( - "decim must be an integer or a slice, " f"got {type(decim)} instead." + f"decim must be an integer or a slice, got {type(decim)} instead." ) # Check output @@ -3950,7 +3950,7 @@ def combine_tfr(all_tfr, weights="nave"): tfr = all_tfr[0].copy() if isinstance(weights, str): if weights not in ("nave", "equal"): - raise ValueError('Weights must be a list of float, or "nave" or ' '"equal"') + raise ValueError('Weights must be a list of float, or "nave" or "equal"') if weights == "nave": weights = np.array([e.nave for e in all_tfr], float) weights /= weights.sum() diff --git a/mne/transforms.py b/mne/transforms.py index b27d0ce6055..3fa582dbe5f 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -444,7 +444,7 @@ def _ensure_trans(trans, fro="mri", to="head"): to_str = _frame_to_str[to] to_const = to del to - err_str = "trans must be a Transform between " f"{from_str}<->{to_str}, got" + err_str = f"trans must be a Transform between {from_str}<->{to_str}, got" if not isinstance(trans, (list, tuple)): trans = [trans] # Ensure that we have exactly one match @@ -1159,7 +1159,7 @@ def fit( del match_rr # 2. Compute spherical harmonic coefficients for all points logger.info( - " Computing spherical harmonic approximation with " f"order {order}" + f" Computing spherical harmonic approximation with order {order}" ) src_sph = _compute_sph_harm(order, *src_rad_az_pol[1:]) dest_sph = _compute_sph_harm(order, *dest_rad_az_pol[1:]) @@ -1583,7 +1583,7 @@ def _read_fs_xfm(fname): break else: raise ValueError( - 'Failed to find "Linear_Transform" string in ' f"xfm file:\n{fname}" + f'Failed to find "Linear_Transform" string in xfm file:\n{fname}' ) xfm = list() diff --git a/mne/utils/check.py b/mne/utils/check.py index 9538ed12c3e..8276bf26711 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -155,7 +155,7 @@ def _require_version(lib, what, version="0.0"): if not ok: extra = f" (version >= {version})" if version != "0.0" else "" why = "package was not found" if got is None else f"got {repr(got)}" - raise ImportError(f"The {lib} package{extra} is required to {what}, " f"{why}") + raise ImportError(f"The {lib} package{extra} is required to {what}, {why}") def _import_h5py(): @@ -261,12 +261,12 @@ def _check_fname( if need_dir: if not fname.is_dir(): raise OSError( - f"Need a directory for {name} but found a file " f"at {fname}" + f"Need a directory for {name} but found a file at {fname}" ) else: if not fname.is_file(): raise OSError( - f"Need a file for {name} but found a directory " f"at {fname}" + f"Need a file for {name} but found a directory at {fname}" ) if not os.access(fname, os.R_OK): raise PermissionError(f"{name} does not have read permissions: {fname}") @@ -1252,5 +1252,5 @@ def _check_method_kwargs(func, kwargs, msg=None): if msg is None: msg = f'function "{func}"' raise TypeError( - f'Got unexpected keyword argument{s} {", ".join(invalid_kw)} ' f"for {msg}." + f'Got unexpected keyword argument{s} {", ".join(invalid_kw)} for {msg}.' ) diff --git a/mne/utils/config.py b/mne/utils/config.py index 271d55b35a3..627660b2b32 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -314,7 +314,7 @@ def get_config(key=None, default=None, raise_error=False, home_dir=None, use_env elif raise_error is True and key not in config: loc_env = "the environment or in the " if use_env else "" meth_env = ( - (f'either os.environ["{key}"] = VALUE for a temporary ' "solution, or ") + (f'either os.environ["{key}"] = VALUE for a temporary solution, or ') if use_env else "" ) @@ -324,7 +324,7 @@ def get_config(key=None, default=None, raise_error=False, home_dir=None, use_env else "" ) meth_file = ( - f'mne.utils.set_config("{key}", VALUE, set_env=True) ' "for a permanent one" + f'mne.utils.set_config("{key}", VALUE, set_env=True) for a permanent one' ) raise KeyError( f'Key "{key}" not found in {loc_env}' diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 02b7eaffd17..26187dbc000 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -465,7 +465,7 @@ def _check_decim(info, decim, offset, check_filter=True): offset = int(offset) if not 0 <= offset < decim: raise ValueError( - f"decim must be at least 0 and less than {decim}, " f"got {offset}" + f"decim must be at least 0 and less than {decim}, got {offset}" ) if check_filter: lowpass = info["lowpass"] diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py index 508c019983b..9dbb17fa485 100644 --- a/mne/utils/numerics.py +++ b/mne/utils/numerics.py @@ -832,7 +832,7 @@ def object_diff(a, b, pre="", *, allclose=False): # sparsity and sparse type of b vs a already checked above by type() if b.shape != a.shape: out += pre + ( - " sparse matrix a and b shape mismatch" f"({a.shape} vs {b.shape})" + f" sparse matrix a and b shape mismatch ({a.shape} vs {b.shape})" ) else: c = a - b diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py index 25668a1de37..4343b9d22de 100644 --- a/mne/utils/tests/test_logging.py +++ b/mne/utils/tests/test_logging.py @@ -52,7 +52,7 @@ def test_frame_info(capsys, monkeypatch): out = out.replace("\n", " ") assert ( re.match( - ".*pytest" ".*test_logging:[2-9][0-9] " ".*test_logging:[1-9][0-9] :.*Test", + ".*pytest.*test_logging:[2-9][0-9] .*test_logging:[1-9][0-9] :.*Test", out, ) is not None diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index e5cf7ed108f..f8fd6f37932 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -1800,7 +1800,7 @@ def _process_clim(clim, colormap, transparent, data=0.0, allow_pos_lims=True): f"Exactly one of lims and pos_lims must be specified in clim, got {clim}" ) if "pos_lims" in clim and not allow_pos_lims: - raise ValueError('Cannot use "pos_lims" for clim, use "lims" ' "instead") + raise ValueError('Cannot use "pos_lims" for clim, use "lims" instead') diverging = "pos_lims" in clim ctrl_pts = np.array(clim["pos_lims" if diverging else "lims"], float) ctrl_pts = np.array(ctrl_pts, float) @@ -2193,7 +2193,7 @@ def link_brains(brains, time=True, camera=False, colorbar=True, picking=False): raise ValueError("The collection of brains is empty.") for brain in brains: if not isinstance(brain, Brain): - raise TypeError("Expected type is Brain but" f" {type(brain)} was given.") + raise TypeError(f"Expected type is Brain but {type(brain)} was given.") # enable time viewer if necessary brain.setup_time_viewer() subjects = [brain._subject for brain in brains] @@ -3378,7 +3378,7 @@ def plot_sparse_source_estimates( if not isinstance(modes, (list, tuple)) or not all( mode in known_modes for mode in modes ): - raise ValueError("mode must be a list containing only " '"cone" or "sphere"') + raise ValueError('mode must be a list containing only "cone" or "sphere"') if not isinstance(stcs, list): stcs = [stcs] if labels is not None and not isinstance(labels, list): diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 8691bffcfb8..8c891969ad8 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -351,7 +351,7 @@ def __init__( size = tuple(np.atleast_1d(size).round(0).astype(int).flat) if len(size) not in (1, 2): raise ValueError( - '"size" parameter must be an int or length-2 ' "sequence of ints." + '"size" parameter must be an int or length-2 sequence of ints.' ) size = size if len(size) == 2 else size * 2 # 1-tuple to 2-tuple subjects_dir = get_subjects_dir(subjects_dir) @@ -1862,7 +1862,7 @@ def add_data( time_label_size = float(time_label_size) if time_label_size < 0: raise ValueError( - "time_label_size must be positive, got " f"{time_label_size}" + f"time_label_size must be positive, got {time_label_size}" ) hemi = self._check_hemi(hemi, extras=["vol"]) @@ -2360,7 +2360,7 @@ def add_forward(self, fwd, trans, alpha=1, scale=None): if scale is None: scale = 1.5 if self._units == "mm" else 1.5e-3 error_msg = ( - "Unexpected forward model coordinate frame " '{}, must be "head" or "mri"' + 'Unexpected forward model coordinate frame {}, must be "head" or "mri"' ) if fwd["coord_frame"] in _frame_to_str: fwd_frame = _frame_to_str[fwd["coord_frame"]] diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 5ccc486dd84..5a1b38d0aba 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -73,7 +73,7 @@ def __init__(self, **kwargs): self.mne.instance_type = "epochs" else: raise TypeError( - "Expected an instance of Raw, Epochs, or ICA, " f"got {type(inst)}." + f"Expected an instance of Raw, Epochs, or ICA, got {type(inst)}." ) logger.debug(f"Opening {self.mne.instance_type} browser...") @@ -435,9 +435,7 @@ def _close(self, event): # proj checkboxes are for viz only and shouldn't modify the instance) if self.mne.instance_type in ("raw", "epochs"): self.mne.inst.info["bads"] = self.mne.info["bads"] - logger.info( - f"Channels marked as bad:\n" f"{self.mne.info['bads'] or 'none'}" - ) + logger.info(f"Channels marked as bad:\n{self.mne.info['bads'] or 'none'}") # ICA excludes elif self.mne.instance_type == "ica": self.mne.ica.exclude = [ diff --git a/mne/viz/circle.py b/mne/viz/circle.py index 2e9578cf4c9..47b94953aa8 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -59,11 +59,9 @@ def circular_layout( if group_boundaries is not None: boundaries = np.array(group_boundaries, dtype=np.int64) if np.any(boundaries >= n_nodes) or np.any(boundaries < 0): - raise ValueError( - '"group_boundaries" has to be between 0 and ' "n_nodes - 1." - ) + raise ValueError('"group_boundaries" has to be between 0 and n_nodes - 1.') if len(boundaries) > 1 and np.any(np.diff(boundaries) <= 0): - raise ValueError('"group_boundaries" must have non-decreasing ' "values.") + raise ValueError('"group_boundaries" must have non-decreasing values.') n_group_sep = len(group_boundaries) else: n_group_sep = 0 diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index 472874e6062..6b927c9ba7f 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -719,7 +719,7 @@ def plot_drop_log( counts = np.array(list(scores.values())) # init figure, handle easy case (no drops) fig, ax = plt.subplots(layout="constrained") - title = f"{absolute} of {n_epochs_before_drop} epochs removed " f"({percent:.1f}%)" + title = f"{absolute} of {n_epochs_before_drop} epochs removed ({percent:.1f}%)" if subject is not None: title = f"{subject}: {title}" ax.set_title(title) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index dad723d6c5a..186da5c2f44 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -478,7 +478,7 @@ def _plot_evoked( _check_option("proj", proj, (True, False, "interactive", "reconstruct")) noise_cov = _check_cov(noise_cov, info) if proj == "reconstruct" and noise_cov is not None: - raise ValueError('Cannot use proj="reconstruct" when noise_cov is not ' "None") + raise ValueError('Cannot use proj="reconstruct" when noise_cov is not None') projector, whitened_ch_names = _setup_plot_projector( info, noise_cov, proj=proj is True, nave=evoked.nave ) @@ -691,9 +691,7 @@ def _plot_lines( elif zorder == "unsorted": z_ord = list(range(D.shape[0])) elif not callable(zorder): - error = ( - '`zorder` must be a function, "std" ' 'or "unsorted", not {0}.' - ) + error = '`zorder` must be a function, "std" or "unsorted", not {0}.' raise TypeError(error.format(type(zorder))) else: z_ord = zorder(D) diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index f92ae3c49f2..45bb167c997 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -3598,7 +3598,7 @@ def plot_arrowmap( ch_type = ch_type[0][0] if ch_type != "mag": - raise ValueError("only 'mag' channel type is supported. " f"Got {ch_type}") + raise ValueError(f"only 'mag' channel type is supported. Got {ch_type}") if info_to is not info_from: info_to = pick_info(info_to, pick_types(info_to, meg=True)) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index e86584b718d..c4e02c55c61 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -392,9 +392,7 @@ def _get_channel_plotting_order(order, ch_types, picks=None): if order_type == pick_type ] elif not isinstance(order, (np.ndarray, list, tuple)): - raise ValueError( - "order should be array-like; got " f'"{order}" ({type(order)}).' - ) + raise ValueError(f'order should be array-like; got "{order}" ({type(order)}).') if picks is not None: order = [ch for ch in order if ch in picks] return np.asarray(order, int) diff --git a/tutorials/intro/40_sensor_locations.py b/tutorials/intro/40_sensor_locations.py index e41ca2cff21..a0a03152eeb 100644 --- a/tutorials/intro/40_sensor_locations.py +++ b/tutorials/intro/40_sensor_locations.py @@ -257,7 +257,7 @@ layout_dir = Path(mne.__file__).parent / "channels" / "data" / "layouts" layouts = sorted(path.name for path in layout_dir.iterdir()) -print("\n" "BUILT-IN LAYOUTS\n" "================") +print("\nBUILT-IN LAYOUTS\n================") print("\n".join(layouts)) # %% diff --git a/tutorials/preprocessing/40_artifact_correction_ica.py b/tutorials/preprocessing/40_artifact_correction_ica.py index 7c7c872ff70..fc3e8865ec2 100644 --- a/tutorials/preprocessing/40_artifact_correction_ica.py +++ b/tutorials/preprocessing/40_artifact_correction_ica.py @@ -275,9 +275,7 @@ explained_var_ratio = ica.get_explained_variance_ratio(filt_raw) for channel_type, ratio in explained_var_ratio.items(): - print( - f"Fraction of {channel_type} variance explained by all components: " f"{ratio}" - ) + print(f"Fraction of {channel_type} variance explained by all components: {ratio}") # %% # The values were calculated for all ICA components jointly, but separately for From e43b5d897228df7de41799583df2e38adbcffc6e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 15 May 2024 15:01:02 -0400 Subject: [PATCH 039/153] MAINT: Cleaner 3D vol viewer code (#12570) --- mne/viz/_3d.py | 412 ++++++++++++------------ mne/viz/tests/test_3d_mpl.py | 4 +- tutorials/clinical/20_seeg.py | 7 +- tutorials/inverse/50_beamformer_lcmv.py | 2 - 4 files changed, 218 insertions(+), 207 deletions(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index f8fd6f37932..d2097b219fc 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -2630,7 +2630,9 @@ def _glass_brain_crosshairs(params, x, y, z): def _cut_coords_to_ijk(cut_coords, img): ijk = apply_trans(np.linalg.inv(img.affine), cut_coords) - ijk = np.clip(np.round(ijk).astype(int), 0, np.array(img.shape[:3]) - 1) + ijk = np.round(ijk).astype(int) + logger.debug(f"{cut_coords} -> {ijk}") + np.clip(ijk, 0, np.array(img.shape[:3]) - 1, out=ijk) return ijk @@ -2649,6 +2651,184 @@ def _load_subject_mri(mri, stc, subject, subjects_dir, name): return mri +_AX_NAME = dict(x="X (sagittal)", y="Y (coronal)", z="Z (axial)") + + +def _click_to_cut_coords(event, params): + """Get voxel coordinates from mouse click.""" + import nibabel as nib + + if event.inaxes is params["ax_x"]: + ax = "x" + x = params["ax_z"].lines[0].get_xdata()[0] + y, z = event.xdata, event.ydata + elif event.inaxes is params["ax_y"]: + ax = "y" + y = params["ax_x"].lines[0].get_xdata()[0] + x, z = event.xdata, event.ydata + elif event.inaxes is params["ax_z"]: + ax = "z" + x, y = event.xdata, event.ydata + z = params["ax_x"].lines[1].get_ydata()[0] + else: + logger.debug(" Click outside axes") + return None + cut_coords = np.array((x, y, z)) + logger.debug("") + + if params["mode"] == "glass_brain": # find idx for MIP + # Figure out what XYZ in world coordinates is in our voxel data + codes = "".join(nib.aff2axcodes(params["img_idx"].affine)) + assert len(codes) == 3 + # We don't care about directionality, just which is which dim + codes = codes.replace("L", "R").replace("P", "A").replace("I", "S") + idx = codes.index(dict(x="R", y="A", z="S")[ax]) + img_data = np.abs(_get_img_fdata(params["img_idx"])) + ijk = _cut_coords_to_ijk(cut_coords, params["img_idx"]) + if idx == 0: + ijk[0] = np.argmax(img_data[:, ijk[1], ijk[2]]) + logger.debug(f" MIP: i = {ijk[0]:d} idx") + elif idx == 1: + ijk[1] = np.argmax(img_data[ijk[0], :, ijk[2]]) + logger.debug(f" MIP: j = {ijk[1]:d} idx") + else: + ijk[2] = np.argmax(img_data[ijk[0], ijk[1], :]) + logger.debug(f" MIP: k = {ijk[2]} idx") + cut_coords = _ijk_to_cut_coords(ijk, params["img_idx"]) + + logger.debug(f" Cut coords for {_AX_NAME[ax]}: {_str_ras(cut_coords)}") + return cut_coords + + +def _str_ras(xyz): + x, y, z = xyz + return f"({x:0.1f}, {y:0.1f}, {z:0.1f}) mm" + + +def _str_vox(ijk): + i, j, k = ijk + return f"[{i:d}, {j:d}, {k:d}] vox" + + +def _press(event, params): + """Manage keypress on the plot.""" + pos = params["lx"].get_xdata() + idx = params["stc"].time_as_index(pos)[0] + if event.key == "left": + idx = max(0, idx - 2) + elif event.key == "shift+left": + idx = max(0, idx - 10) + elif event.key == "right": + idx = min(params["stc"].shape[1] - 1, idx + 2) + elif event.key == "shift+right": + idx = min(params["stc"].shape[1] - 1, idx + 10) + _update_timeslice(idx, params) + params["fig"].canvas.draw() + + +def _update_timeslice(idx, params): + from nilearn.image import index_img + + params["lx"].set_xdata([idx / params["stc"].sfreq + params["stc"].tmin]) + ax_x, ax_y, ax_z = params["ax_x"], params["ax_y"], params["ax_z"] + # Crosshairs are the first thing plotted in stat_map, and the last + # in glass_brain + idxs = [0, 0, 1] if params["mode"] == "stat_map" else [-2, -2, -1] + cut_coords = ( + ax_y.lines[idxs[0]].get_xdata()[0], + ax_x.lines[idxs[1]].get_xdata()[0], + ax_x.lines[idxs[2]].get_ydata()[0], + ) + ax_x.clear() + ax_y.clear() + ax_z.clear() + params.update({"img_idx": index_img(params["img"], idx)}) + params.update({"title": f"Activation (t={params['stc'].times[idx]:.3f} s.)"}) + _plot_and_correct(params=params, cut_coords=cut_coords) + + +def _update_vertlabel(loc_idx, params): + params["vert_legend"].get_texts()[0].set_text(f"{params['vertices'][loc_idx]}") + + +@verbose_dec +def _onclick(event, params, verbose=None): + """Manage clicks on the plot.""" + ax_x, ax_y, ax_z = params["ax_x"], params["ax_y"], params["ax_z"] + if event.inaxes is params["ax_time"]: + idx = params["stc"].time_as_index(event.xdata, use_rounding=True)[0] + _update_timeslice(idx, params) + + cut_coords = _click_to_cut_coords(event, params) + if cut_coords is None: + return # not in any axes + + ax_x.clear() + ax_y.clear() + ax_z.clear() + _plot_and_correct(params=params, cut_coords=cut_coords) + loc_idx = _cut_coords_to_idx(cut_coords, params["dist_to_verts"]) + ydata = params["stc"].data[loc_idx] + if loc_idx is not None: + params["ax_time"].lines[0].set_ydata(ydata) + else: + params["ax_time"].lines[0].set_ydata([0.0]) + _update_vertlabel(loc_idx, params) + params["fig"].canvas.draw() + + +def _cut_coords_to_idx(cut_coords, dist_to_verts): + """Convert voxel coordinates to index in stc.data.""" + logger.debug(f" Starting coords: {cut_coords}") + cut_coords = list(cut_coords) + (dist,), (loc_idx,) = dist_to_verts.query([cut_coords]) + logger.debug(f"Mapped {cut_coords=} to vertices[{loc_idx}] {dist:0.1f} mm away") + return loc_idx + + +def _plot_and_correct(*, params, cut_coords): + # black_bg = True is needed because of some matplotlib + # peculiarity. See: https://stackoverflow.com/a/34730204 + # Otherwise, event.inaxes does not work for ax_x and ax_z + from nilearn.plotting import plot_glass_brain, plot_stat_map + + mode = params["mode"] + nil_func = dict(stat_map=plot_stat_map, glass_brain=plot_glass_brain)[mode] + plot_kwargs = dict( + threshold=None, + axes=params["axes"], + resampling_interpolation="nearest", + vmax=params["vmax"], + figure=params["fig"], + colorbar=params["colorbar"], + bg_img=params["bg_img"], + cmap=params["colormap"], + black_bg=True, + symmetric_cbar=True, + title="", + ) + params["axes"].clear() + if params.get("fig_anat") is not None and plot_kwargs["colorbar"]: + params["fig_anat"]._cbar.ax.clear() + with warnings.catch_warnings(record=True): # nilearn bug; ax recreated + warnings.simplefilter("ignore", DeprecationWarning) + params["fig_anat"] = nil_func( + params["img_idx"], cut_coords=cut_coords, **plot_kwargs + ) + params["fig_anat"]._cbar.outline.set_visible(False) + for key in "xyz": + params.update({"ax_" + key: params["fig_anat"].axes[key].ax}) + # Fix nilearn bug w/cbar background being white + if plot_kwargs["colorbar"]: + params["fig_anat"]._cbar.ax.set_facecolor("0.5") + # adjust one-sided colorbars + if not params["diverging"]: + _crop_colorbar(params["fig_anat"]._cbar, *params["scale_pts"][[0, -1]]) + params["fig_anat"]._cbar.set_ticks(params["cbar_ticks"]) + if params["mode"] == "glass_brain": + _glass_brain_crosshairs(params, *cut_coords) + + @verbose def plot_volume_source_estimates( stc, @@ -2756,10 +2936,8 @@ def plot_volume_source_estimates( raise RuntimeError("This function requires nilearn >= 0.4") from nilearn.image import index_img - from nilearn.plotting import plot_glass_brain, plot_stat_map _check_option("mode", mode, ("stat_map", "glass_brain")) - plot_func = dict(stat_map=plot_stat_map, glass_brain=plot_glass_brain)[mode] _validate_type(stc, VolSourceEstimate, "stc") if isinstance(src, SourceMorph): img = src.apply(stc, "nifti1", mri_resolution=False, mri_space=False) @@ -2777,136 +2955,6 @@ def plot_volume_source_estimates( level="debug", ) subject = _check_subject(src_subject, subject, first_kind=kind) - vertices = np.hstack(stc.vertices) - stc_ijk = np.array(np.unravel_index(vertices, img.shape[:3], order="F")).T - assert stc_ijk.shape == (vertices.size, 3) - del kind - - # XXX this assumes zooms are uniform, should probably mult by zooms... - dist_to_verts = _DistanceQuery(stc_ijk) - - def _cut_coords_to_idx(cut_coords, img): - """Convert voxel coordinates to index in stc.data.""" - ijk = _cut_coords_to_ijk(cut_coords, img) - del cut_coords - logger.debug(" Affine remapped cut coords to [%d, %d, %d] idx", tuple(ijk)) - dist, loc_idx = dist_to_verts.query(ijk[np.newaxis]) - dist, loc_idx = dist[0], loc_idx[0] - logger.debug( - " Using vertex %d at a distance of %d voxels", (vertices[loc_idx], dist) - ) - return loc_idx - - ax_name = dict(x="X (sagittal)", y="Y (coronal)", z="Z (axial)") - - def _click_to_cut_coords(event, params): - """Get voxel coordinates from mouse click.""" - if event.inaxes is params["ax_x"]: - ax = "x" - x = params["ax_z"].lines[0].get_xdata()[0] - y, z = event.xdata, event.ydata - elif event.inaxes is params["ax_y"]: - ax = "y" - y = params["ax_x"].lines[0].get_xdata()[0] - x, z = event.xdata, event.ydata - elif event.inaxes is params["ax_z"]: - ax = "z" - x, y = event.xdata, event.ydata - z = params["ax_x"].lines[1].get_ydata()[0] - else: - logger.debug(" Click outside axes") - return None - cut_coords = np.array((x, y, z)) - logger.debug("") - - if params["mode"] == "glass_brain": # find idx for MIP - # Figure out what XYZ in world coordinates is in our voxel data - codes = "".join(nib.aff2axcodes(params["img_idx"].affine)) - assert len(codes) == 3 - # We don't care about directionality, just which is which dim - codes = codes.replace("L", "R").replace("P", "A").replace("I", "S") - idx = codes.index(dict(x="R", y="A", z="S")[ax]) - img_data = np.abs(_get_img_fdata(params["img_idx"])) - ijk = _cut_coords_to_ijk(cut_coords, params["img_idx"]) - if idx == 0: - ijk[0] = np.argmax(img_data[:, ijk[1], ijk[2]]) - logger.debug(" MIP: i = %d idx" % (ijk[0],)) - elif idx == 1: - ijk[1] = np.argmax(img_data[ijk[0], :, ijk[2]]) - logger.debug(" MIP: j = %d idx" % (ijk[1],)) - else: - ijk[2] = np.argmax(img_data[ijk[0], ijk[1], :]) - logger.debug(" MIP: k = %d idx" % (ijk[2],)) - cut_coords = _ijk_to_cut_coords(ijk, params["img_idx"]) - - logger.debug( - " Cut coords for %s: (%0.1f, %0.1f, %0.1f) mm" - % ((ax_name[ax],) + tuple(cut_coords)) - ) - return cut_coords - - def _press(event, params): - """Manage keypress on the plot.""" - pos = params["lx"].get_xdata() - idx = params["stc"].time_as_index(pos)[0] - if event.key == "left": - idx = max(0, idx - 2) - elif event.key == "shift+left": - idx = max(0, idx - 10) - elif event.key == "right": - idx = min(params["stc"].shape[1] - 1, idx + 2) - elif event.key == "shift+right": - idx = min(params["stc"].shape[1] - 1, idx + 10) - _update_timeslice(idx, params) - params["fig"].canvas.draw() - - def _update_timeslice(idx, params): - params["lx"].set_xdata([idx / params["stc"].sfreq + params["stc"].tmin]) - ax_x, ax_y, ax_z = params["ax_x"], params["ax_y"], params["ax_z"] - plot_map_callback = params["plot_func"] - # Crosshairs are the first thing plotted in stat_map, and the last - # in glass_brain - idxs = [0, 0, 1] if mode == "stat_map" else [-2, -2, -1] - cut_coords = ( - ax_y.lines[idxs[0]].get_xdata()[0], - ax_x.lines[idxs[1]].get_xdata()[0], - ax_x.lines[idxs[2]].get_ydata()[0], - ) - ax_x.clear() - ax_y.clear() - ax_z.clear() - params.update({"img_idx": index_img(img, idx)}) - params.update({"title": f"Activation (t={params['stc'].times[idx]:.3f} s.)"}) - plot_map_callback(params["img_idx"], title="", cut_coords=cut_coords) - - def _update_vertlabel(loc_idx): - vert_legend.get_texts()[0].set_text(f"{vertices[loc_idx]}") - - @verbose_dec - def _onclick(event, params, verbose=None): - """Manage clicks on the plot.""" - ax_x, ax_y, ax_z = params["ax_x"], params["ax_y"], params["ax_z"] - plot_map_callback = params["plot_func"] - if event.inaxes is params["ax_time"]: - idx = params["stc"].time_as_index(event.xdata, use_rounding=True)[0] - _update_timeslice(idx, params) - - cut_coords = _click_to_cut_coords(event, params) - if cut_coords is None: - return # not in any axes - - ax_x.clear() - ax_y.clear() - ax_z.clear() - plot_map_callback(params["img_idx"], title="", cut_coords=cut_coords) - loc_idx = _cut_coords_to_idx(cut_coords, params["img_idx"]) - ydata = stc.data[loc_idx] - if loc_idx is not None: - ax_time.lines[0].set_ydata(ydata) - else: - ax_time.lines[0].set_ydata([0.0]) - _update_vertlabel(loc_idx) - params["fig"].canvas.draw() if mode == "glass_brain": subject = _check_subject(stc.subject, subject) @@ -2925,6 +2973,20 @@ def _onclick(event, params, verbose=None): bg_img = "T1.mgz" bg_img = _load_subject_mri(bg_img, stc, subject, subjects_dir, "bg_img") + params = dict( + stc=stc, + mode=mode, + img=img, + bg_img=bg_img, + colorbar=colorbar, + ) + vertices = np.hstack(stc.vertices) + stc_ijk = np.array(np.unravel_index(vertices, img.shape[:3], order="F")).T + assert stc_ijk.shape == (vertices.size, 3) + params["dist_to_verts"] = _DistanceQuery(apply_trans(img.affine, stc_ijk)) + params["vertices"] = vertices + del kind, stc_ijk + if initial_time is None: time_sl = slice(0, None) else: @@ -2946,26 +3008,22 @@ def _onclick(event, params, verbose=None): ) initial_pos *= 1000 logger.info(f"Fixing initial position: {initial_pos.tolist()} mm") - loc_idx = _cut_coords_to_idx(initial_pos, img) + loc_idx = _cut_coords_to_idx(initial_pos, params["dist_to_verts"]) if initial_time is not None: # time also specified time_idx = time_sl.start else: # find the max time_idx = np.argmax(np.abs(stc.data[loc_idx])) - img_idx = index_img(img, time_idx) + img_idx = params["img_idx"] = index_img(img, time_idx) assert img_idx.shape == img.shape[:3] del initial_time, initial_pos - ijk = stc_ijk[loc_idx] + ijk = np.unravel_index(vertices[loc_idx], img.shape[:3], order="F") cut_coords = _ijk_to_cut_coords(ijk, img_idx) np.testing.assert_allclose(_cut_coords_to_ijk(cut_coords, img_idx), ijk) logger.info( - "Showing: t = %0.3f s, (%0.1f, %0.1f, %0.1f) mm, " - "[%d, %d, %d] vox, %d vertex" - % ( - (stc.times[time_idx],) - + tuple(cut_coords) - + tuple(ijk) - + (vertices[loc_idx],) - ) + f"Showing: t = {stc.times[time_idx]:0.3f} s, " + f"{_str_ras(cut_coords)}, " + f"{_str_vox(ijk)}, " + f"{vertices[loc_idx]:d} vertex" ) del ijk @@ -2978,15 +3036,17 @@ def _onclick(event, params, verbose=None): if len(stc.times) > 1: ax_time.set(xlim=stc.times[[0, -1]]) ax_time.set(xlabel="Time (s)", ylabel="Activation") - vert_legend = ax_time.legend([h], [""], title="Vertex") - _update_vertlabel(loc_idx) + params["vert_legend"] = ax_time.legend([h], [""], title="Vertex") + _update_vertlabel(loc_idx, params) lx = ax_time.axvline(stc.times[time_idx], color="g") + params.update(fig=fig, ax_time=ax_time, lx=lx, axes=axes) allow_pos_lims = mode != "glass_brain" mapdata = _process_clim(clim, colormap, transparent, stc.data, allow_pos_lims) _separate_map(mapdata) diverging = "pos_lims" in mapdata["clim"] ticks = _get_map_ticks(mapdata) + params.update(cbar_ticks=ticks, diverging=diverging) colormap, scale_pts = _linearize_map(mapdata) del mapdata @@ -3026,56 +3086,9 @@ def _onclick(event, params, verbose=None): np.interp(np.linspace(-1, 1, 256), scale_pts / scale_pts[2], [0, 0.5, 1]) ) colormap = colors.ListedColormap(colormap) - vmax = scale_pts[-1] - - # black_bg = True is needed because of some matplotlib - # peculiarity. See: https://stackoverflow.com/a/34730204 - # Otherwise, event.inaxes does not work for ax_x and ax_z - plot_kwargs = dict( - threshold=None, - axes=axes, - resampling_interpolation="nearest", - vmax=vmax, - figure=fig, - colorbar=colorbar, - bg_img=bg_img, - cmap=colormap, - black_bg=True, - symmetric_cbar=True, - ) + params.update(vmax=scale_pts[-1], scale_pts=scale_pts, colormap=colormap) - def plot_and_correct(*args, **kwargs): - axes.clear() - if params.get("fig_anat") is not None and plot_kwargs["colorbar"]: - params["fig_anat"]._cbar.ax.clear() - with warnings.catch_warnings(record=True): # nilearn bug; ax recreated - warnings.simplefilter("ignore", DeprecationWarning) - params["fig_anat"] = partial(plot_func, **plot_kwargs)(*args, **kwargs) - params["fig_anat"]._cbar.outline.set_visible(False) - for key in "xyz": - params.update({"ax_" + key: params["fig_anat"].axes[key].ax}) - # Fix nilearn bug w/cbar background being white - if plot_kwargs["colorbar"]: - params["fig_anat"]._cbar.ax.set_facecolor("0.5") - # adjust one-sided colorbars - if not diverging: - _crop_colorbar(params["fig_anat"]._cbar, *scale_pts[[0, -1]]) - params["fig_anat"]._cbar.set_ticks(params["cbar_ticks"]) - if mode == "glass_brain": - _glass_brain_crosshairs(params, *kwargs["cut_coords"]) - - params = dict( - stc=stc, - ax_time=ax_time, - plot_func=plot_and_correct, - img_idx=img_idx, - fig=fig, - lx=lx, - mode=mode, - cbar_ticks=ticks, - ) - - plot_and_correct(stat_map_img=params["img_idx"], title="", cut_coords=cut_coords) + _plot_and_correct(params=params, cut_coords=cut_coords) plt_show(show) fig.canvas.mpl_connect( @@ -3993,10 +4006,11 @@ def _plot_dipole( coord_frame_name = "Head" if coord_frame == "head" else "MRI" if title is None: - title = f"Dipole #{idx + 1} / {len(dipole.times)} @ {dipole.times[idx]:.3f}s, " - f"GOF: {dipole.gof[idx]:.1f}%, {dipole.amplitude[idx] * 1e9:.1f}nAm\n" - f"{coord_frame_name}: " + f"({xyz[idx][0]:0.1f}, {xyz[idx][1]:0.1f}, " - f"{xyz[idx][2]:0.1f}) mm" + title = ( + f"Dipole #{idx + 1} / {len(dipole.times)} @ {dipole.times[idx]:.3f}s, " + f"GOF: {dipole.gof[idx]:.1f}%, {dipole.amplitude[idx] * 1e9:.1f}nAm\n" + f"{coord_frame_name}: {_str_ras(xyz[idx])}" + ) ax.get_figure().suptitle(title) diff --git a/mne/viz/tests/test_3d_mpl.py b/mne/viz/tests/test_3d_mpl.py index a96d41fefb8..082541d10f5 100644 --- a/mne/viz/tests/test_3d_mpl.py +++ b/mne/viz/tests/test_3d_mpl.py @@ -49,7 +49,7 @@ ("stat_map", "s", 1, 1, (-10, 5, 10), (-12.3, 2.0, 7.7), "brain.mgz"), ], ) -def test_plot_volume_source_estimates( +def test_plot_volume_source_estimates_basic( mode, stype, init_t, want_t, init_p, want_p, bg_img ): """Test interactive plotting of volume source estimates.""" @@ -75,7 +75,7 @@ def test_plot_volume_source_estimates( stc = VolSourceEstimate(data, vertices, 1, 1) # sometimes get scalars/index warning with _record_warnings(): - with catch_logging() as log: + with catch_logging(verbose="debug") as log: fig = stc.plot( sample_src, subject="sample", diff --git a/tutorials/clinical/20_seeg.py b/tutorials/clinical/20_seeg.py index 6166001c075..ea56ea8a688 100644 --- a/tutorials/clinical/20_seeg.py +++ b/tutorials/clinical/20_seeg.py @@ -21,10 +21,9 @@ for your dataset. You can take a look at :ref:`tut-freesurfer-mne` for more information. -For an example that involves ECoG data, channel locations in a -subject-specific MRI, or projection into a surface, see -:ref:`tut-working-with-ecog`. In the ECoG example, we show -how to visualize surface grid channels on the brain. +For an example that involves ECoG data, channel locations in a subject-specific MRI, or +projection into a surface, see :ref:`tut-working-with-ecog`. In the ECoG example, we +show how to visualize surface grid channels on the brain. Please note that this tutorial requires 3D plotting dependencies, see :ref:`manual-install`. diff --git a/tutorials/inverse/50_beamformer_lcmv.py b/tutorials/inverse/50_beamformer_lcmv.py index d9027f32560..7cf952649a8 100644 --- a/tutorials/inverse/50_beamformer_lcmv.py +++ b/tutorials/inverse/50_beamformer_lcmv.py @@ -14,8 +14,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -# %% - import matplotlib.pyplot as plt import mne From 823e25deb03ee23b2df1fde9c03944c14d25ef94 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 16 May 2024 17:54:57 -0400 Subject: [PATCH 040/153] MAINT: Workaround hook bug (#12615) --- tools/install_pre_requirements.sh | 4 +++- tutorials/time-freq/50_ssvep.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh index 280c5f60867..ac90a38612c 100755 --- a/tools/install_pre_requirements.sh +++ b/tools/install_pre_requirements.sh @@ -67,7 +67,9 @@ echo "joblib" pip install $STD_ARGS git+https://github.com/joblib/joblib echo "edfio" -pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio +# Disable protection for Azure, see +# https://github.com/mne-tools/mne-python/pull/12609#issuecomment-2115639369 +GIT_CLONE_PROTECTION_ACTIVE=false pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio if [[ "${PLATFORM}" == "Linux" ]]; then echo "h5io" diff --git a/tutorials/time-freq/50_ssvep.py b/tutorials/time-freq/50_ssvep.py index 706841fefac..96a2bf39ca8 100644 --- a/tutorials/time-freq/50_ssvep.py +++ b/tutorials/time-freq/50_ssvep.py @@ -641,7 +641,7 @@ def snr_spectrum(psd, noise_n_neighbor_freqs=1, noise_skip_neighbor_freqs=1): ].mean(axis=1) fig, ax = plt.subplots(1) -ax.boxplot(window_snrs, labels=window_lengths, vert=True) +ax.boxplot(window_snrs, tick_labels=window_lengths, vert=True) ax.set( title="Effect of trial duration on 12 Hz SNR", ylabel="Average SNR", From 5a20b82e7d8e1ae2976bd41e7a92ba68daca171d Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Fri, 17 May 2024 02:17:15 +0300 Subject: [PATCH 041/153] Fix Brain slider ranges (#12612) --- doc/changes/devel/12612.bugfix.rst | 1 + mne/viz/_brain/_brain.py | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 doc/changes/devel/12612.bugfix.rst diff --git a/doc/changes/devel/12612.bugfix.rst b/doc/changes/devel/12612.bugfix.rst new file mode 100644 index 00000000000..5868fb93a2a --- /dev/null +++ b/doc/changes/devel/12612.bugfix.rst @@ -0,0 +1 @@ +Fix overflow when plotting source estimates where data is all zero (or close to zero), and fix the range of allowed values for the colorbar sliders, by `Marijn van Vliet`_. diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 8c891969ad8..1832c1b74ff 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -869,8 +869,8 @@ def set_orientation(value, orientation_data=orientation_data): ) def _configure_dock_colormap_widget(self, name): - fmin, fmax, fscale, fscale_power = _get_range(self) - rng = [fmin * fscale, fmax * fscale] + fmax, fscale, fscale_power = _get_range(self) + rng = [0, fmax * fscale] self._data["fscale"] = fscale layout = self._renderer._dock_add_group_box(name) @@ -4157,16 +4157,15 @@ def _get_range(brain): multiplied by the scaling factor and when getting a value, this value should be divided by the scaling factor. """ - val = np.abs(np.concatenate(list(brain._current_act_data.values()))) - fmin, fmax = np.min(val), np.max(val) + fmax = brain._data["fmax"] if 1e-02 <= fmax <= 1e02: fscale_power = 0 else: - fscale_power = int(np.log10(fmax)) + fscale_power = int(np.log10(max(fmax, np.finfo("float32").min))) if fscale_power < 0: fscale_power -= 1 fscale = 10**-fscale_power - return fmin, fmax, fscale, fscale_power + return fmax, fscale, fscale_power class _FakeIren: From 15fee8910b6b18938464e22caedec238b8f5334d Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Fri, 17 May 2024 16:14:39 +0200 Subject: [PATCH 042/153] Clean-up __contains__ code and add comment for Info (#12617) --- mne/_fiff/meas_info.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index f3c1bfc5061..2293cfbd550 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -874,13 +874,16 @@ def __contains__(self, ch_type): False """ - info = self if isinstance(self, Info) else self.info + # this method is not supported by Info object. An Info object inherits from a + # dictionary and the 'key' in Info call is present all across MNE codebase, e.g. + # to check for the presence of a key: + # >>> 'bads' in info if ch_type == "meg": - has_ch_type = _contains_ch_type(info, "mag") or _contains_ch_type( - info, "grad" + has_ch_type = _contains_ch_type(self.info, "mag") or _contains_ch_type( + self.info, "grad" ) else: - has_ch_type = _contains_ch_type(info, ch_type) + has_ch_type = _contains_ch_type(self.info, ch_type) return has_ch_type @property From cf0e12d9e329440408b19caef974ab95a1439686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fourcaud-Trocm=C3=A9?= Date: Fri, 17 May 2024 16:24:34 +0200 Subject: [PATCH 043/153] Permutation cluster test with TFCE: improvement of speed and memory usage in 2D (#12609) Co-authored-by: Eric Larson --- doc/changes/devel/12609.bugfix.rst | 1 + doc/changes/names.inc | 2 ++ mne/stats/cluster_level.py | 40 +++++++++++++-------------- mne/stats/tests/test_cluster_level.py | 17 +++++++++--- 4 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 doc/changes/devel/12609.bugfix.rst diff --git a/doc/changes/devel/12609.bugfix.rst b/doc/changes/devel/12609.bugfix.rst new file mode 100644 index 00000000000..1cd7e50d664 --- /dev/null +++ b/doc/changes/devel/12609.bugfix.rst @@ -0,0 +1 @@ +Fix bug where :func:`mne.stats.permutation_cluster_test` (and related functions) uses excessive amount of memory for large 2D data when TFCE method is selected, by :newcontrib:`Nicolas Fourcaud-Trocmé`. \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index f9c67b65ce1..cdb9a62b855 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -412,6 +412,8 @@ .. _Nicolas Barascud: https://github.com/nbara +.. _Nicolas Fourcaud-Trocmé: https://www.crnl.fr/fr/user/316 + .. _Niels Focke: https://neurologie.umg.eu/forschung/arbeitsgruppen/epilepsie-und-bildgebungsforschung .. _Niklas Wilming: https://github.com/nwilming diff --git a/mne/stats/cluster_level.py b/mne/stats/cluster_level.py index 7add61f6ae1..5f319d338a8 100644 --- a/mne/stats/cluster_level.py +++ b/mne/stats/cluster_level.py @@ -378,7 +378,7 @@ def _find_clusters( ------- clusters : list of slices or list of arrays (boolean masks) We use slices for 1D signals and mask to multidimensional - arrays. + arrays. None is returned if threshold is a dict (TFCE) sums : array Sum of x values in clusters. """ @@ -485,18 +485,9 @@ def _find_clusters( # turn sums into array sums = np.concatenate(sums) if sums else np.array([]) if tfce: - # each point gets treated independently - clusters = np.arange(x.size) - if adjacency is None or adjacency is False: - if x.ndim == 1: - # slices - clusters = [slice(c, c + 1) for c in clusters] - else: - # boolean masks (raveled) - clusters = [(clusters == ii).ravel() for ii in range(len(clusters))] - else: - clusters = [np.array([c]) for c in clusters] sums = scores + clusters = None # clusters construction is made in _permutation_cluster_test + return clusters, sums @@ -570,11 +561,16 @@ def _find_clusters_1dir(x, x_in, adjacency, max_step, t_power, ndimage): return clusters, np.atleast_1d(sums) -def _cluster_indices_to_mask(components, n_tot): - """Convert to the old format of clusters, which were bool arrays.""" +def _cluster_indices_to_mask(components, n_tot, slice_out): + """Convert to the old format of clusters, which were bool arrays (or slices in 1D).""" # noqa: E501 for ci, c in enumerate(components): - components[ci] = np.zeros((n_tot), dtype=bool) - components[ci][c] = True + if not slice_out: + # boolean array + components[ci] = np.zeros((n_tot), dtype=bool) + components[ci][c] = True + else: + # slice (similar as ndimage.find_object output) + components[ci] = (slice(c.min(), c.max() + 1),) return components @@ -1007,18 +1003,22 @@ def _permutation_cluster_test( t_obs.shape = sample_shape # For TFCE, return the "adjusted" statistic instead of raw scores - if isinstance(threshold, dict): + # and for clusters, each point gets treated independently + tfce = isinstance(threshold, dict) + if tfce: t_obs = cluster_stats.reshape(t_obs.shape) * np.sign(t_obs) + clusters = [np.array([c]) for c in range(t_obs.size)] logger.info(f"Found {len(clusters)} cluster{_pl(clusters)}") # convert clusters to old format - if adjacency is not None and adjacency is not False: + if (adjacency is not None and adjacency is not False) or tfce: # our algorithms output lists of indices by default if out_type == "mask": - clusters = _cluster_indices_to_mask(clusters, n_tests) + slice_out = (adjacency is None) & (len(sample_shape) == 1) + clusters = _cluster_indices_to_mask(clusters, n_tests, slice_out) else: - # ndimage outputs slices or boolean masks by default + # ndimage outputs slices or boolean masks by default, if out_type == "indices": clusters = _cluster_mask_to_indices(clusters, t_obs.shape) diff --git a/mne/stats/tests/test_cluster_level.py b/mne/stats/tests/test_cluster_level.py index 1b020d11d28..693cdc66b75 100644 --- a/mne/stats/tests/test_cluster_level.py +++ b/mne/stats/tests/test_cluster_level.py @@ -824,7 +824,8 @@ def test_tfce_thresholds(numba_conditional): @pytest.mark.parametrize("shape", ((11,), (11, 3), (11, 1, 2))) @pytest.mark.parametrize("out_type", ("mask", "indices")) @pytest.mark.parametrize("adjacency", (None, "sparse")) -def test_output_equiv(shape, out_type, adjacency): +@pytest.mark.parametrize("threshold", (None, dict(start=0, step=0.1))) +def test_output_equiv(shape, out_type, adjacency, threshold): """Test equivalence of output types.""" rng = np.random.RandomState(0) n_subjects = 10 @@ -832,14 +833,22 @@ def test_output_equiv(shape, out_type, adjacency): data -= data.mean(axis=0, keepdims=True) data[:, 2:4] += 2 data[:, 6:9] += 2 + tfce = isinstance(threshold, dict) want_mask = np.zeros(shape, int) - want_mask[2:4] = 1 - want_mask[6:9] = 2 + if not tfce: + want_mask[2:4] = 1 + want_mask[6:9] = 2 + else: + want_mask = np.arange(want_mask.size).reshape(shape) + 1 if adjacency is not None: assert adjacency == "sparse" adjacency = combine_adjacency(*shape) clusters = permutation_cluster_1samp_test( - X=data, n_permutations=1, adjacency=adjacency, out_type=out_type + X=data, + n_permutations=1, + adjacency=adjacency, + out_type=out_type, + threshold=threshold, )[1] got_mask = np.zeros_like(want_mask) for n, clu in enumerate(clusters, 1): From 5c2a25567e5e1613c204ad40be58bf483db6a99e Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Sat, 18 May 2024 00:19:33 +0300 Subject: [PATCH 044/153] Brain sliders hotfix (#12619) Co-authored-by: Eric Larson --- examples/inverse/source_space_snr.py | 1 + mne/viz/_brain/_brain.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/inverse/source_space_snr.py b/examples/inverse/source_space_snr.py index 04b429fe218..965c57d86ca 100644 --- a/examples/inverse/source_space_snr.py +++ b/examples/inverse/source_space_snr.py @@ -8,6 +8,7 @@ This example shows how to compute and plot source space SNR as in :footcite:`GoldenholzEtAl2009`. """ + # Author: Padma Sundaram # Kaisu Lankinen # diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 1832c1b74ff..207385bb07c 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -4157,11 +4157,11 @@ def _get_range(brain): multiplied by the scaling factor and when getting a value, this value should be divided by the scaling factor. """ - fmax = brain._data["fmax"] + fmax = abs(brain._data["fmax"]) if 1e-02 <= fmax <= 1e02: fscale_power = 0 else: - fscale_power = int(np.log10(max(fmax, np.finfo("float32").min))) + fscale_power = int(np.log10(max(fmax, np.finfo("float32").smallest_normal))) if fscale_power < 0: fscale_power -= 1 fscale = 10**-fscale_power From b12396dfe051139682bdf566687b384789c034e7 Mon Sep 17 00:00:00 2001 From: Alexandre Gramfort Date: Mon, 20 May 2024 14:04:41 +0200 Subject: [PATCH 045/153] fix for sklearn metadata (#12620) Co-authored-by: Eric Larson --- doc/changes/devel/12620.bugfix.rst | 1 + mne/decoding/tests/test_search_light.py | 24 ++++++++++++++++-------- tools/vulture_allowlist.py | 1 + 3 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 doc/changes/devel/12620.bugfix.rst diff --git a/doc/changes/devel/12620.bugfix.rst b/doc/changes/devel/12620.bugfix.rst new file mode 100644 index 00000000000..0e8d53f02b1 --- /dev/null +++ b/doc/changes/devel/12620.bugfix.rst @@ -0,0 +1 @@ +Fix for new sklearn metadata routing protocol in decoding search_light, by `Alex Gramfort`_ diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py index a5fc53865cc..d78b123f746 100644 --- a/mne/decoding/tests/test_search_light.py +++ b/mne/decoding/tests/test_search_light.py @@ -14,7 +14,7 @@ from mne.decoding.transformer import Vectorizer from mne.utils import _record_warnings, check_version, use_log_level -pytest.importorskip("sklearn") +sklearn = pytest.importorskip("sklearn") NEW_MULTICLASS_SAMPLE_WEIGHT = check_version("sklearn", "1.4") @@ -186,7 +186,17 @@ def transform(self, X): assert isinstance(pipe.estimators_[0], BaggingClassifier) -def test_generalization_light(): +@pytest.fixture() +def metadata_routing(): + """Temporarily enable metadata routing for new sklearn.""" + if NEW_MULTICLASS_SAMPLE_WEIGHT: + sklearn.set_config(enable_metadata_routing=True) + yield + if NEW_MULTICLASS_SAMPLE_WEIGHT: + sklearn.set_config(enable_metadata_routing=False) + + +def test_generalization_light(metadata_routing): """Test GeneralizingEstimator.""" from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score @@ -194,7 +204,9 @@ def test_generalization_light(): from sklearn.pipeline import make_pipeline if NEW_MULTICLASS_SAMPLE_WEIGHT: - logreg = OneVsRestClassifier(LogisticRegression(random_state=0)) + clf = LogisticRegression(random_state=0) + clf.set_fit_request(sample_weight=True) + logreg = OneVsRestClassifier(clf) else: logreg = LogisticRegression( solver="liblinear", @@ -208,10 +220,7 @@ def test_generalization_light(): gl = GeneralizingEstimator(logreg) assert_equal(repr(gl)[:23], "") # transforms @@ -346,7 +355,6 @@ def predict_proba(self, X): @pytest.mark.slowtest def test_sklearn_compliance(): """Test LinearModel compliance with sklearn.""" - pytest.importorskip("sklearn") from sklearn.linear_model import LogisticRegression from sklearn.utils.estimator_checks import check_estimator diff --git a/tools/vulture_allowlist.py b/tools/vulture_allowlist.py index 5c3d41c356e..c0ac3317e09 100644 --- a/tools/vulture_allowlist.py +++ b/tools/vulture_allowlist.py @@ -15,6 +15,7 @@ verbose_debug few_surfaces disabled_event_channels +metadata_routing # Others exc_value From 4c9a176de185efd2ca3da466c81bca6094873beb Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 21 May 2024 13:12:54 +0200 Subject: [PATCH 046/153] Fix `EpochsTFR.add_channels()` (#12616) --- doc/changes/devel/12616.bugfix.rst | 1 + mne/channels/channels.py | 4 ++++ mne/time_frequency/tests/test_tfr.py | 17 +++++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 doc/changes/devel/12616.bugfix.rst diff --git a/doc/changes/devel/12616.bugfix.rst b/doc/changes/devel/12616.bugfix.rst new file mode 100644 index 00000000000..b7c5fc7fced --- /dev/null +++ b/doc/changes/devel/12616.bugfix.rst @@ -0,0 +1 @@ +Fix adding channels to :class:`~mne.time_frequency.EpochsTFR` objects, by `Clemens Brunner`_. \ No newline at end of file diff --git a/mne/channels/channels.py b/mne/channels/channels.py index f9fbdf95477..c89aa09e213 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -696,6 +696,7 @@ def add_channels(self, add_list, force_update_info=False): # avoid circular imports from ..epochs import BaseEpochs from ..io import BaseRaw + from ..time_frequency import EpochsTFR _validate_type(add_list, (list, tuple), "Input") @@ -708,6 +709,9 @@ def add_channels(self, add_list, force_update_info=False): elif isinstance(self, BaseEpochs): con_axis = 1 comp_class = BaseEpochs + elif isinstance(self, EpochsTFR): + con_axis = 1 + comp_class = EpochsTFR else: con_axis = 0 comp_class = type(self) diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 37a5fdc7724..bec82665d1c 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -943,6 +943,23 @@ def test_add_channels(): pytest.raises(ValueError, tfr_meg.add_channels, [tfr_meg]) pytest.raises(TypeError, tfr_meg.add_channels, tfr_badsf) + # Test for EpochsTFR(Array) + tfr1 = EpochsTFRArray( + info=mne.create_info(["EEG 001"], 1000, "eeg"), + data=np.zeros((5, 1, 2, 3)), # epochs, channels, freqs, times + times=[0.1, 0.2, 0.3], + freqs=[0.1, 0.2], + ) + tfr2 = EpochsTFRArray( + info=mne.create_info(["EEG 002", "EEG 003"], 1000, "eeg"), + data=np.zeros((5, 2, 2, 3)), # epochs, channels, freqs, times + times=[0.1, 0.2, 0.3], + freqs=[0.1, 0.2], + ) + tfr1.add_channels([tfr2]) + assert tfr1.ch_names == ["EEG 001", "EEG 002", "EEG 003"] + assert tfr1.data.shape == (5, 3, 2, 3) + def test_compute_tfr(): """Test _compute_tfr function.""" From 07aecf85877621a37fd83b69a78d837f81df81e2 Mon Sep 17 00:00:00 2001 From: Stefan Appelhoff Date: Tue, 21 May 2024 16:06:52 +0200 Subject: [PATCH 047/153] DOC: blink interpolation changes annot descs (#12622) --- mne/preprocessing/eyetracking/_pupillometry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/preprocessing/eyetracking/_pupillometry.py b/mne/preprocessing/eyetracking/_pupillometry.py index 8da124b2e1f..2aaaefd2b17 100644 --- a/mne/preprocessing/eyetracking/_pupillometry.py +++ b/mne/preprocessing/eyetracking/_pupillometry.py @@ -29,7 +29,8 @@ def interpolate_blinks(raw, buffer=0.05, match="BAD_blink", interpolate_gaze=Fal match : str | list of str The description of annotations to interpolate over. If a list, the data within all annotations that match any of the strings in the list will be interpolated - over. Defaults to ``'BAD_blink'``. + over. If a ``match`` starts with ``'BAD_'``, that part will be removed from the + annotation description after interpolation. Defaults to ``'BAD_blink'``. interpolate_gaze : bool If False, only apply interpolation to ``'pupil channels'``. If True, interpolate over ``'eyegaze'`` channels as well. Defaults to False, because eye position can From c55c44af394b611abcb4eb8553935d0b582f6e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Tue, 21 May 2024 16:10:12 +0200 Subject: [PATCH 048/153] MRG: Revamp HTML reprs of `Raw`, `Epochs`, `Evoked`, and `Info` (#12583) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel McCloy --- doc/changes/devel/12583.apichange.rst | 2 + doc/changes/devel/12583.newfeature.rst | 4 + mne/_fiff/meas_info.py | 82 ++---------- mne/_fiff/tests/test_meas_info.py | 15 ++- mne/epochs.py | 17 +-- mne/evoked.py | 16 +-- mne/forward/forward.py | 10 +- mne/html_templates/_templates.py | 118 ++++++++++++++++++ .../repr/_acquisition.html.jinja | 100 +++++++++++++++ mne/html_templates/repr/_channels.html.jinja | 51 ++++++++ mne/html_templates/repr/_filters.html.jinja | 48 +++++++ mne/html_templates/repr/_general.html.jinja | 68 ++++++++++ .../repr/_js_and_css.html.jinja | 7 ++ mne/html_templates/repr/epochs.html.jinja | 32 ++--- mne/html_templates/repr/evoked.html.jinja | 40 ++---- mne/html_templates/repr/forward.html.jinja | 33 +++-- mne/html_templates/repr/info.html.jinja | 111 ++-------------- mne/html_templates/repr/raw.html.jinja | 11 +- mne/html_templates/repr/static/repr.css | 105 ++++++++++++++++ mne/html_templates/repr/static/repr.js | 35 ++++++ mne/io/base.py | 16 +-- mne/io/tests/test_raw.py | 2 +- mne/report/report.py | 5 +- mne/report/tests/test_report.py | 4 +- mne/utils/tests/test_misc.py | 6 +- tutorials/intro/70_report.py | 55 ++++---- 26 files changed, 683 insertions(+), 310 deletions(-) create mode 100644 doc/changes/devel/12583.apichange.rst create mode 100644 doc/changes/devel/12583.newfeature.rst create mode 100644 mne/html_templates/repr/_acquisition.html.jinja create mode 100644 mne/html_templates/repr/_channels.html.jinja create mode 100644 mne/html_templates/repr/_filters.html.jinja create mode 100644 mne/html_templates/repr/_general.html.jinja create mode 100644 mne/html_templates/repr/_js_and_css.html.jinja create mode 100644 mne/html_templates/repr/static/repr.css create mode 100644 mne/html_templates/repr/static/repr.js diff --git a/doc/changes/devel/12583.apichange.rst b/doc/changes/devel/12583.apichange.rst new file mode 100644 index 00000000000..92d64f5caf3 --- /dev/null +++ b/doc/changes/devel/12583.apichange.rst @@ -0,0 +1,2 @@ +``mne.Info.ch_names`` will now return an empty list instead of raising a ``KeyError`` if no channels +are present, by `Richard Höchenberger`_. \ No newline at end of file diff --git a/doc/changes/devel/12583.newfeature.rst b/doc/changes/devel/12583.newfeature.rst new file mode 100644 index 00000000000..70a5bce0c9e --- /dev/null +++ b/doc/changes/devel/12583.newfeature.rst @@ -0,0 +1,4 @@ +The HTML representations of :class:`~mne.io.Raw`, :class:`~mne.Epochs`, +and :class:`~mne.Evoked` (which you will see e.g. when working with Jupyter Notebooks or +:class:`~mne.Report`) have been updated to be more consistent and contain +slightly more information, by `Richard Höchenberger`_. \ No newline at end of file diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 2293cfbd550..ddedbddfe9a 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -10,7 +10,7 @@ import datetime import operator import string -from collections import Counter, OrderedDict, defaultdict +from collections import Counter, OrderedDict from collections.abc import Mapping from copy import deepcopy from io import BytesIO @@ -1838,84 +1838,18 @@ def _update_redundant(self): @property def ch_names(self): - return self["ch_names"] - - def _get_chs_for_repr(self): - titles = _handle_default("titles") - - # good channels - good_names = defaultdict(lambda: list()) - for ci, ch_name in enumerate(self["ch_names"]): - if ch_name in self["bads"]: - continue - ch_type = channel_type(self, ci) - good_names[ch_type].append(ch_name) - good_channels = ", ".join( - [f"{len(v)} {titles.get(k, k.upper())}" for k, v in good_names.items()] - ) - for key in ("ecg", "eog"): # ensure these are present - if key not in good_names: - good_names[key] = list() - for key, val in good_names.items(): - good_names[key] = ", ".join(val) or "Not available" - - # bad channels - bad_channels = ", ".join(self["bads"]) or "None" + try: + ch_names = self["ch_names"] + except KeyError: + ch_names = [] - return good_channels, bad_channels, good_names["ecg"], good_names["eog"] + return ch_names @repr_html - def _repr_html_(self, caption=None, duration=None, filenames=None): + def _repr_html_(self): """Summarize info for HTML representation.""" - if isinstance(caption, str): - html = f"

{caption}

" - else: - html = "" - - good_channels, bad_channels, ecg, eog = self._get_chs_for_repr() - - # TODO - # Most of the following checks are to ensure that we get a proper repr - # for Forward['info'] (and probably others like - # InverseOperator['info']??), which doesn't seem to follow our standard - # Info structure used elsewhere. - # Proposed solution for a future refactoring: - # Forward['info'] should get its own Info subclass (with respective - # repr). - - # meas date - meas_date = self.get("meas_date") - if meas_date is not None: - meas_date = meas_date.strftime("%B %d, %Y %H:%M:%S") + " GMT" - - projs = self.get("projs") - if projs: - projs = [ - f'{p["desc"]} : {"on" if p["active"] else "off"}' for p in self["projs"] - ] - else: - projs = None - info_template = _get_html_template("repr", "info.html.jinja") - sections = ("General", "Channels", "Data") - return html + info_template.render( - sections=sections, - caption=caption, - meas_date=meas_date, - projs=projs, - ecg=ecg, - eog=eog, - good_channels=good_channels, - bad_channels=bad_channels, - dig=self.get("dig"), - subject_info=self.get("subject_info"), - lowpass=self.get("lowpass"), - highpass=self.get("highpass"), - sfreq=self.get("sfreq"), - experimenter=self.get("experimenter"), - duration=duration, - filenames=filenames, - ) + return info_template.render(info=self) def save(self, fname): """Write measurement info in fif file. diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index fb9488ce1a7..6c979bf3648 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -896,18 +896,17 @@ def test_repr_html(): info["projs"] = [] assert "Projections" not in info._repr_html_() info["bads"] = [] - assert "None" in info._repr_html_() + assert "bad" not in info._repr_html_() info["bads"] = ["MEG 2443", "EEG 053"] - assert "MEG 2443" in info._repr_html_() - assert "EEG 053" in info._repr_html_() + assert "1 bad" in info._repr_html_() # 1 for each channel type html = info._repr_html_() for ch in [ # good channel counts - "203 Gradiometers", - "102 Magnetometers", - "9 Stimulus", - "59 EEG", - "1 EOG", + "203", # grad + "102", # mag + "9", # stim + "59", # eeg + "1", # eog ]: assert ch in html diff --git a/mne/epochs.py b/mne/epochs.py index 43f8baf70f3..90687cb418e 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -17,6 +17,7 @@ from copy import deepcopy from functools import partial from inspect import getfullargspec +from pathlib import Path import numpy as np from scipy.interpolate import interp1d @@ -2062,12 +2063,6 @@ def __repr__(self): @repr_html def _repr_html_(self): - if self.baseline is None: - baseline = "off" - else: - baseline = tuple([f"{b:.3f}" for b in self.baseline]) - baseline = f"{baseline[0]} – {baseline[1]} s" - if isinstance(self.event_id, dict): event_strings = [] for k, v in sorted(self.event_id.items()): @@ -2085,7 +2080,15 @@ def _repr_html_(self): event_strings = None t = _get_html_template("repr", "epochs.html.jinja") - t = t.render(epochs=self, baseline=baseline, events=event_strings) + t = t.render( + inst=self, + filenames=( + [Path(self.filename).name] + if getattr(self, "filename", None) is not None + else None + ), + event_counts=event_strings, + ) return t @verbose diff --git a/mne/evoked.py b/mne/evoked.py index cc4f1e738c5..024a6d14f73 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -10,6 +10,7 @@ from copy import deepcopy from inspect import getfullargspec +from pathlib import Path from typing import Union import numpy as np @@ -467,14 +468,15 @@ def __repr__(self): # noqa: D105 @repr_html def _repr_html_(self): - if self.baseline is None: - baseline = "off" - else: - baseline = tuple([f"{b:.3f}" for b in self.baseline]) - baseline = f"{baseline[0]} – {baseline[1]} s" - t = _get_html_template("repr", "evoked.html.jinja") - t = t.render(evoked=self, baseline=baseline) + t = t.render( + inst=self, + filenames=( + [Path(self.filename).name] + if getattr(self, "filename", None) is not None + else None + ), + ) return t @property diff --git a/mne/forward/forward.py b/mne/forward/forward.py index 96ae003b805..af47fe0b7fa 100644 --- a/mne/forward/forward.py +++ b/mne/forward/forward.py @@ -225,17 +225,11 @@ def __repr__(self): @repr_html def _repr_html_(self): - ( - good_chs, - bad_chs, - _, - _, - ) = self["info"]._get_chs_for_repr() src_descr, src_ori = self._get_src_type_and_ori_for_repr() + t = _get_html_template("repr", "forward.html.jinja") html = t.render( - good_channels=good_chs, - bad_channels=bad_chs, + info=self["info"], source_space_descr=src_descr, source_orientation=src_ori, ) diff --git a/mne/html_templates/_templates.py b/mne/html_templates/_templates.py index a54547679d2..525c794e849 100644 --- a/mne/html_templates/_templates.py +++ b/mne/html_templates/_templates.py @@ -1,10 +1,118 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import datetime import functools +import uuid +from dataclasses import dataclass +from typing import Any, Literal, Union + +from .._fiff.pick import channel_type +from ..defaults import _handle_default _COLLAPSED = False # will override in doc build +def _format_number(value: Union[int, float]) -> str: + """Insert thousand separators.""" + return f"{value:,}" + + +def _append_uuid(string: str, sep: str = "-") -> str: + """Append a UUID to a string.""" + return f"{string}{sep}{uuid.uuid4()}" + + +def _data_type(obj) -> str: + """Return the qualified name of a class.""" + return obj.__class__.__qualname__ + + +def _dt_to_str(dt: datetime.datetime) -> str: + """Convert a datetime object to a human-readable string representation.""" + return dt.strftime("%Y-%m-%d at %H:%M:%S %Z") + + +def _format_baseline(inst) -> str: + """Format the baseline time period.""" + if inst.baseline is None: + baseline = "off" + else: + baseline = ( + f"{round(inst.baseline[0], 3):.3f} – {round(inst.baseline[1], 3):.3f} s" + ) + + return baseline + + +def _format_time_range(inst) -> str: + """Format evoked and epochs time range.""" + tr = f"{round(inst.tmin, 3):.3f} – {round(inst.tmax, 3):.3f} s" + return tr + + +def _format_projs(info) -> list[str]: + """Format projectors.""" + projs = [f'{p["desc"]} ({"on" if p["active"] else "off"})' for p in info["projs"]] + return projs + + +@dataclass +class _Channel: + """A channel in a recording.""" + + index: int + name: str + name_html: str + type: str + type_pretty: str + status: Literal["good", "bad"] + + +def _format_channels(info) -> dict[str, dict[Literal["good", "bad"], list[str]]]: + """Format channel names.""" + ch_types_pretty: dict[str, str] = _handle_default("titles") + channels = [] + + if info.ch_names: + for ch_index, ch_name in enumerate(info.ch_names): + ch_type = channel_type(info, ch_index) + ch_type_pretty = ch_types_pretty.get(ch_type, ch_type.upper()) + ch_status = "bad" if ch_name in info["bads"] else "good" + channel = _Channel( + index=ch_index, + name=ch_name, + name_html=ch_name.replace(" ", " "), + type=ch_type, + type_pretty=ch_type_pretty, + status=ch_status, + ) + channels.append(channel) + + # Extract unique channel types and put them in the desired order. + ch_types = list(set([c.type_pretty for c in channels])) + ch_types = [c for c in ch_types_pretty.values() if c in ch_types] + + channels_formatted = {} + for ch_type in ch_types: + goods = [c for c in channels if c.type_pretty == ch_type and c.status == "good"] + bads = [c for c in channels if c.type_pretty == ch_type and c.status == "bad"] + if ch_type not in channels_formatted: + channels_formatted[ch_type] = {"good": [], "bad": []} + channels_formatted[ch_type]["good"] = goods + channels_formatted[ch_type]["bad"] = bads + + return channels_formatted + + +def _has_attr(obj: Any, attr: str) -> bool: + """Check if an object has an attribute `obj.attr`. + + This is needed because on dict-like objects, Jinja2's `obj.attr is defined` would + check for `obj["attr"]`, which may not be what we want. + """ + return hasattr(obj, attr) + + @functools.lru_cache(maxsize=2) def _get_html_templates_env(kind): # For _html_repr_() and mne.Report @@ -19,6 +127,16 @@ def _get_html_templates_env(kind): ) if kind == "report": templates_env.filters["zip"] = zip + + templates_env.filters["format_number"] = _format_number + templates_env.filters["append_uuid"] = _append_uuid + templates_env.filters["data_type"] = _data_type + templates_env.filters["dt_to_str"] = _dt_to_str + templates_env.filters["format_baseline"] = _format_baseline + templates_env.filters["format_time_range"] = _format_time_range + templates_env.filters["format_projs"] = _format_projs + templates_env.filters["format_channels"] = _format_channels + templates_env.filters["has_attr"] = _has_attr return templates_env diff --git a/mne/html_templates/repr/_acquisition.html.jinja b/mne/html_templates/repr/_acquisition.html.jinja new file mode 100644 index 00000000000..90201ef03c8 --- /dev/null +++ b/mne/html_templates/repr/_acquisition.html.jinja @@ -0,0 +1,100 @@ +{% set section = "Acquisition" %} +{% set section_class_name = section | lower | append_uuid %} + +{# Collapse content during documentation build. #} +{% if collapsed %} +{% set collapsed_row_class = "repr-element-faded repr-element-collapsed" %} +{% else %} +{% set collapsed_row_class = "" %} +{% endif %} + + + + + + + {{ section }} + + +{% if duration %} + + + Duration + {{ duration }} (HH:MM:SS) + +{% endif %} +{% if inst is defined and inst | has_attr("kind") and inst | has_attr("nave") %} + + + Aggregation + {% if inst.kind == "average" %} + average of {{ inst.nave }} epochs + {% elif inst.kind == "standard_error" %} + standard error of {{ inst.nave }} epochs + {% else %} + {{ inst.kind }} ({{ inst.nave }} epochs) + {% endif %} + +{% endif %} +{% if inst is defined and inst | has_attr("comment") %} + + + Condition + {{inst.comment}} + +{% endif %} +{% if inst is defined and inst | has_attr("events") %} + + + Total number of events + {{ inst.events | length }} + +{% endif %} +{% if event_counts is defined %} + + + Events counts + {% if events is not none %} + + {% for e in event_counts %} + {{ e }} + {% if not loop.last %}
{% endif %} + {% endfor %} + + {% else %} + Not available + {% endif %} + +{% endif %} +{% if inst is defined and inst | has_attr("tmin") and inst | has_attr("tmax") %} + + + Time range + {{ inst | format_time_range }} + +{% endif %} +{% if inst is defined and inst | has_attr("baseline") %} + + + Baseline + {{ inst | format_baseline }} + +{% endif %} +{% if info["sfreq"] is defined and info["sfreq"] is not none %} + + + Sampling frequency + {{ "%0.2f" | format(info["sfreq"]) }} Hz + +{% endif %} +{% if inst is defined and inst.times is defined %} + + + Time points + {{ inst.times | length | format_number }} + +{% endif %} \ No newline at end of file diff --git a/mne/html_templates/repr/_channels.html.jinja b/mne/html_templates/repr/_channels.html.jinja new file mode 100644 index 00000000000..0821f1525a3 --- /dev/null +++ b/mne/html_templates/repr/_channels.html.jinja @@ -0,0 +1,51 @@ +{% set section = "Channels" %} +{% set section_class_name = section | lower | append_uuid %} + +{# Collapse content during documentation build. #} +{% if collapsed %} +{% set collapsed_row_class = "repr-element-faded repr-element-collapsed" %} +{% else %} +{% set collapsed_row_class = "" %} +{% endif %} + + + + + + + {{ section }} + + +{% for channel_type, channels in (info | format_channels).items() %} +{% set channel_names_good = channels["good"] | map(attribute='name_html') | join(', ') %} + + + {{ channel_type }} + + + + {% if channels["bad"] %} + {% set channel_names_bad = channels["bad"] | map(attribute='name_html') | join(', ') %} + and + {% endif %} + + +{% endfor %} + + + + Head & sensor digitization + {% if info["dig"] is not none %} + {{ info["dig"] | length }} points + {% else %} + Not available + {% endif %} + \ No newline at end of file diff --git a/mne/html_templates/repr/_filters.html.jinja b/mne/html_templates/repr/_filters.html.jinja new file mode 100644 index 00000000000..b01841cf137 --- /dev/null +++ b/mne/html_templates/repr/_filters.html.jinja @@ -0,0 +1,48 @@ +{% set section = "Filters" %} +{% set section_class_name = section | lower | append_uuid %} + +{# Collapse content during documentation build. #} +{% if collapsed %} +{% set collapsed_row_class = "repr-element-faded repr-element-collapsed" %} +{% else %} +{% set collapsed_row_class = "" %} +{% endif %} + + + + + + + {{ section }} + + +{% if info["highpass"] is defined and info["highpass"] is not none %} + + + Highpass + {{ "%0.2f" | format(info["highpass"]) }} Hz + +{% endif %} +{% if info["lowpass"] is defined and info["lowpass"] is not none %} + + + Lowpass + {{ "%0.2f" | format(info["lowpass"]) }} Hz + +{% endif %} +{% if info.projs is defined and info.projs %} + + + Projections + + {% for p in (info | format_projs) %} + {{ p }} + {% if not loop.last %}
{% endif %} + {% endfor %} + + +{% endif %} \ No newline at end of file diff --git a/mne/html_templates/repr/_general.html.jinja b/mne/html_templates/repr/_general.html.jinja new file mode 100644 index 00000000000..c9ad8310e64 --- /dev/null +++ b/mne/html_templates/repr/_general.html.jinja @@ -0,0 +1,68 @@ +{% set section = "General" %} +{% set section_class_name = section | lower | append_uuid %} + +{# Collapse content during documentation build. #} +{% if collapsed %} +{% set collapsed_row_class = "repr-element-faded repr-element-collapsed" %} +{% else %} +{% set collapsed_row_class = "" %} +{% endif %} + + + + + + + {{ section }} + + +{% if filenames %} + + + Filename(s) + + {% for f in filenames %} + {{ f }} + {% if not loop.last %}
{% endif %} + {% endfor %} + + +{% endif %} + + + MNE object type + {{ inst | data_type }} + + + + Measurement date + {% if info["meas_date"] is defined and info["meas_date"] is not none %} + {{ info["meas_date"] | dt_to_str }} + {% else %} + Unknown + {% endif %} + + + + Participant + {% if info["subject_info"] is defined and info["subject_info"] is not none %} + {% if info["subject_info"]["his_id"] is defined %} + {{ info["subject_info"]["his_id"] }} + {% endif %} + {% else %} + Unknown + {% endif %} + + + + Experimenter + {% if info["experimenter"] is defined and info["experimenter"] is not none %} + {{ info["experimenter"] }} + {% else %} + Unknown + {% endif %} + \ No newline at end of file diff --git a/mne/html_templates/repr/_js_and_css.html.jinja b/mne/html_templates/repr/_js_and_css.html.jinja new file mode 100644 index 00000000000..f185cfbe00a --- /dev/null +++ b/mne/html_templates/repr/_js_and_css.html.jinja @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/mne/html_templates/repr/epochs.html.jinja b/mne/html_templates/repr/epochs.html.jinja index f2894a599e2..991aa8de0e3 100644 --- a/mne/html_templates/repr/epochs.html.jinja +++ b/mne/html_templates/repr/epochs.html.jinja @@ -1,22 +1,10 @@ - - - - - - - - {% if events is not none %} - - {% else %} - - {% endif %} - - - - - - - - - -
Number of events{{ epochs.events|length }}
Events{{ events|join('
') | safe }}
Not available
Time range{{ '%.3f'|format(epochs.tmin) }} – {{ '%.3f'|format(epochs.tmax) }} s
Baseline{{ baseline }}
+{%include '_js_and_css.html.jinja' %} + +{% set info = inst.info %} + + + {%include '_general.html.jinja' %} + {%include '_acquisition.html.jinja' %} + {%include '_channels.html.jinja' %} + {%include '_filters.html.jinja' %} +
\ No newline at end of file diff --git a/mne/html_templates/repr/evoked.html.jinja b/mne/html_templates/repr/evoked.html.jinja index bb9ef0e5f97..991aa8de0e3 100644 --- a/mne/html_templates/repr/evoked.html.jinja +++ b/mne/html_templates/repr/evoked.html.jinja @@ -1,30 +1,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Condition{{evoked.comment}}
Data kind{{evoked.kind}}
Timepoints{{ evoked.data.shape[1] }} samples
Channels{{ evoked.data.shape[0] }} channels
Number of averaged epochs{{evoked.nave}}
Time range (secs){{ evoked.times[0] }} – {{ evoked.times[-1] }}
Baseline (secs){{baseline}}
+{%include '_js_and_css.html.jinja' %} + +{% set info = inst.info %} + + + {%include '_general.html.jinja' %} + {%include '_acquisition.html.jinja' %} + {%include '_channels.html.jinja' %} + {%include '_filters.html.jinja' %} +
\ No newline at end of file diff --git a/mne/html_templates/repr/forward.html.jinja b/mne/html_templates/repr/forward.html.jinja index f7294cf2cdd..22be9248ecc 100644 --- a/mne/html_templates/repr/forward.html.jinja +++ b/mne/html_templates/repr/forward.html.jinja @@ -1,12 +1,29 @@ +{%include '_js_and_css.html.jinja' %} + - - - - - - - + {% for channel_type, channels in (info | format_channels).items() %} + {% set channel_names_good = channels["good"] | map(attribute='name_html') | join(', ') %} + + + + {% endfor %} + @@ -15,4 +32,4 @@ -
Good channels{{ good_channels }}
Bad channels{{ bad_channels }}
{{ channel_type }} + + + {% if channels["bad"] %} + {% set channel_names_bad = channels["bad"] | map(attribute='name_html') | join(', ') %} + and + {% endif %} +
Source space {{ source_space_descr }}Source orientation {{ source_orientation }}
+ \ No newline at end of file diff --git a/mne/html_templates/repr/info.html.jinja b/mne/html_templates/repr/info.html.jinja index 5b787cbfe31..d30c21ff9bf 100644 --- a/mne/html_templates/repr/info.html.jinja +++ b/mne/html_templates/repr/info.html.jinja @@ -1,101 +1,10 @@ - - {{sections[0]}} - - - - {% if meas_date is not none %} - - {% else %} - - {% endif %} - - - - {% if experimenter is not none %} - - {% else %} - - {% endif %} - - - - {% if subject_info is defined and subject_info is not none %} - {% if 'his_id' in subject_info.keys() %} - - {% endif %} - {% else %} - - {% endif %} - -
Measurement date{{ meas_date }}Unknown
Experimenter{{ experimenter }}Unknown
Participant{{ subject_info['his_id'] }}Unknown
- - - {{sections[1]}} - - - - {% if dig is not none %} - - {% else %} - - {% endif %} - - - - - - - - - - - - - - - - - -
Digitized points{{ dig|length }} pointsNot available
Good channels{{ good_channels }}
Bad channels{{ bad_channels }}
EOG channels{{ eog }}
ECG channels{{ ecg }}
- - - {{sections[2]}} - - {% if sfreq is not none %} - - - - - {% endif %} - {% if highpass is not none %} - - - - - {% endif %} - {% if lowpass is not none %} - - - - - {% endif %} - {% if projs is not none %} - - - - - {% endif %} - {% if filenames %} - - - - - {% endif %} - {% if duration %} - - - - - {% endif %} -
Sampling frequency{{ '%0.2f'|format(sfreq) }} Hz
Highpass{{ '%0.2f'|format(highpass) }} Hz
Lowpass{{ '%0.2f'|format(lowpass) }} Hz
Projections{{ projs|join('
') | safe }}
Filenames{{ filenames|join('
') }}
Duration{{ duration }} (HH:MM:SS)
- +{%include '_js_and_css.html.jinja' %} + +{%set inst = info %} + + + {%include '_general.html.jinja' %} + {%include '_acquisition.html.jinja' %} + {%include '_channels.html.jinja' %} + {%include '_filters.html.jinja' %} +
\ No newline at end of file diff --git a/mne/html_templates/repr/raw.html.jinja b/mne/html_templates/repr/raw.html.jinja index 9cba46f43bf..991aa8de0e3 100644 --- a/mne/html_templates/repr/raw.html.jinja +++ b/mne/html_templates/repr/raw.html.jinja @@ -1 +1,10 @@ -{{ info_repr | safe }} +{%include '_js_and_css.html.jinja' %} + +{% set info = inst.info %} + + + {%include '_general.html.jinja' %} + {%include '_acquisition.html.jinja' %} + {%include '_channels.html.jinja' %} + {%include '_filters.html.jinja' %} +
\ No newline at end of file diff --git a/mne/html_templates/repr/static/repr.css b/mne/html_templates/repr/static/repr.css new file mode 100644 index 00000000000..cee7e3224f8 --- /dev/null +++ b/mne/html_templates/repr/static/repr.css @@ -0,0 +1,105 @@ +table.repr.table.table-hover.table-striped.table-sm.table-responsive.small { + /* Don't make rows wider than they need to be. */ + display: inline; +} + +table > tbody > tr.repr-element > td { + /* Apply a tighter layout to the table cells. */ + padding-top: 0.1rem; + padding-bottom: 0.1rem; + padding-right: 1rem; +} + +table > tbody > tr > td.repr-section-toggle-col { + /* Remove background and border of the first cell in every row + (this row is only used for the collapse / uncollapse caret) + + TODO: Need to find a good solution for VS Code that works in both + light and dark mode. */ + border-color: transparent; + --bs-table-accent-bg: transparent; +} + +tr.repr-section-header { + /* Remove stripes from section header rows */ + background-color: transparent; + border-color: transparent; + --bs-table-striped-bg: transparent; + cursor: pointer; +} + +tr.repr-section-header > th { + text-align: left !important; + vertical-align: middle; +} + +.repr-element, tr.repr-element > td { + opacity: 1; + text-align: left !important; +} + +.repr-element-faded { + transition: 0.3s ease; + opacity: 0.2; +} + +.repr-element-collapsed { + display: none; +} + +/* Collapse / uncollapse button and the caret it contains. */ +.repr-section-toggle-col button { + cursor: pointer; + width: 1rem; + background-color: transparent; + border-color: transparent; +} + +span.collapse-uncollapse-caret { + width: 1rem; + height: 1rem; + display: block; + background-repeat: no-repeat; + background-position: left; + background-size: contain; +} + +/* The collapse / uncollapse carets were copied from the free Font Awesome collection and adjusted. */ + +/* Default to black carets for light mode */ +.repr-section-toggle-col > button.collapsed > span.collapse-uncollapse-caret { + background-image: url('data:image/svg+xml;charset=utf8,'); +} + +.repr-section-toggle-col + > button:not(.collapsed) + > span.collapse-uncollapse-caret { + background-image: url('data:image/svg+xml;charset=utf8,'); +} + +/* Use white carets for dark mode */ +@media (prefers-color-scheme: dark) { + .repr-section-toggle-col > button.collapsed > span.collapse-uncollapse-caret { + background-image: url('data:image/svg+xml;charset=utf8,'); + } + + .repr-section-toggle-col + > button:not(.collapsed) + > span.collapse-uncollapse-caret { + background-image: url('data:image/svg+xml;charset=utf8,'); + } +} + +.channel-names-btn { + padding: 0; + border: none; + background: none; + text-decoration: underline; + text-decoration-style: dashed; + cursor: pointer; + color: #0d6efd; +} + +.channel-names-btn:hover { + color: #0a58ca; +} diff --git a/mne/html_templates/repr/static/repr.js b/mne/html_templates/repr/static/repr.js new file mode 100644 index 00000000000..78ed06808b7 --- /dev/null +++ b/mne/html_templates/repr/static/repr.js @@ -0,0 +1,35 @@ +const toggleVisibility = (className) => { + + const elements = document.querySelectorAll(`.${className}`) + + elements.forEach(element => { + if (element.classList.contains('repr-section-header')) { + // Don't collapse the section header row. + return + } + if (element.classList.contains('repr-element-collapsed')) { + // Force a reflow to ensure the display change takes effect before removing the class + element.classList.remove('repr-element-collapsed') + element.offsetHeight // This forces the browser to recalculate layout + element.classList.remove('repr-element-faded') + } else { + // Start transition to hide the element + element.classList.add('repr-element-faded') + element.addEventListener('transitionend', handler = (e) => { + if (e.propertyName === 'opacity' && getComputedStyle(element).opacity === '0.2') { + element.classList.add('repr-element-collapsed') + element.removeEventListener('transitionend', handler) + } + }); + } + }); + + // Take care of button (adjust caret) + const button = document.querySelectorAll(`.repr-section-header.${className} > th.repr-section-toggle-col > button`)[0] + button.classList.toggle('collapsed') + + // Take care of the tooltip of the section header row + const sectionHeaderRow = document.querySelectorAll(`tr.repr-section-header.${className}`)[0] + sectionHeaderRow.classList.toggle('collapsed') + sectionHeaderRow.title = sectionHeaderRow.title === 'Hide section' ? 'Show section' : 'Hide section' +} diff --git a/mne/io/base.py b/mne/io/base.py index f10228a70cf..607fe50f651 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -19,6 +19,7 @@ from dataclasses import dataclass, field from datetime import timedelta from inspect import getfullargspec +from pathlib import Path import numpy as np @@ -2095,7 +2096,7 @@ def copy(self): def __repr__(self): # noqa: D105 name = self.filenames[0] - name = "" if name is None else op.basename(name) + ", " + name = "" if name is None else Path(name).name + ", " size_str = str(sizeof_fmt(self._size)) # str in case it fails -> None size_str += f", data{'' if self.preload else ' not'} loaded" s = ( @@ -2105,8 +2106,8 @@ def __repr__(self): # noqa: D105 return f"<{self.__class__.__name__} | {s}>" @repr_html - def _repr_html_(self, caption=None): - basenames = [os.path.basename(f) for f in self._filenames if f is not None] + def _repr_html_(self): + basenames = [Path(f).name for f in self._filenames if f is not None] # https://stackoverflow.com/a/10981895 duration = timedelta(seconds=self.times[-1]) @@ -2116,13 +2117,12 @@ def _repr_html_(self, caption=None): seconds = np.ceil(seconds) # always take full seconds duration = f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}" + raw_template = _get_html_template("repr", "raw.html.jinja") return raw_template.render( - info_repr=self.info._repr_html_( - caption=caption, - filenames=basenames, - duration=duration, - ) + inst=self, + filenames=basenames, + duration=duration, ) def add_events(self, events, stim_channel=None, replace=False): diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index b478ed59c46..f68a86317dc 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -334,7 +334,7 @@ def _test_raw_reader( assert meas_date is None or meas_date >= _stamp_to_dt((0, 0)) # test repr_html - assert "Good channels" in raw._repr_html_() + assert "Channels" in raw._repr_html_() # test resetting raw if test_kwargs: diff --git a/mne/report/report.py b/mne/report/report.py index ae66591481a..eb40c8be405 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -4296,11 +4296,10 @@ def _recursive_search(path, pattern): .. container:: row - .. rubric:: The `HTML document <{0}>`__ written by :meth:`mne.Report.save`: - .. raw:: html - + The generated HTML document. + """ # noqa: E501 # Adapted from fa-file-code diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 30f4d14d814..7cf69bb4c66 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -353,7 +353,7 @@ def test_report_raw_psd_and_date(tmp_path): assert isinstance(report.html, list) assert "PSD" in "".join(report.html) assert "Unknown" not in "".join(report.html) - assert "GMT" in "".join(report.html) + assert "UTC" in "".join(report.html) # test kwargs passed through to underlying array func Report(raw_psd=dict(window="boxcar")) @@ -821,7 +821,7 @@ def test_scraper(tmp_path): assert not out_html.is_file() scraper.copyfiles() assert out_html.is_file() - assert rst.count('"') == 6 + assert rst.count('"') == 8 assert "") + assert r.startswith("